TL;DR
After weeks of building a multi-agent AI platform — five agents, full pipeline, red-team harness, control UI — the system looked done. It wasn’t. A config file opened at the wrong moment triggered an audit that found 57 gaps. A service running in Docker Compose is not the same as a service working end-to-end. This article covers the audit method, the five categories of gap it revealed, and why the resulting gap register outlasted any single file of code.
Previous: The Engineering Platform: Orchestration, Monorepo, and the Stack Decisions | Next: Securing Agentic AI: Authentication, Authorization, and PII
The Moment It Looked Done
There’s a specific kind of confidence that comes after weeks of building. The CI pipeline is green. Docker Compose brings up nineteen containers without errors. All five agents respond to health checks. The control UI loads. The red-team harness runs without exceptions. At that moment, a reasonable person would call it done.
That was the moment I opened a config file.
Buried in an environment variable definition was a name: ANTHROPIC_MODEL. It should have been LLM_MODEL — vendor-neutral, matching the platform’s own rule against hardcoding a provider name in configuration. Small thing. But it was the kind of small thing that exists because nobody had ever checked whether the config actually matched the architecture.
That one observation opened a question I’d been avoiding: “If this is wrong, what else is wrong?”
The answer, after two days of audit, was: 57 things.
What I ran into wasn’t a code quality problem. It was a verification problem — the gap between “the system is declared complete” and “the system actually works end-to-end.” In agentic AI systems that gap is bigger than most engineers expect, mine included.
Back to top . Next: What “Done” Means in Agentic AI →
What “Done” Means in Agentic AI
In traditional software, “done” has a fairly clear meaning: the feature works, the tests pass, the deployment succeeds. The system is a bounded function — input in, output out, verifiable in isolation.
Agentic AI systems don’t work this way. An agentic system is a web of interdependent components — graph nodes, tool calls, message buses, vector databases, policy engines, observability pipelines. Any one of them can sit in a state I’d call declared functionality without verified behavior: the class is defined, the method is implemented, the service is running, and none of it is actually flowing end-to-end. Three examples from the audit make this concrete:
Langfuse was one of the first ones I checked, because it should have been the easiest to trust. It was running in Docker Compose, reachable at its health endpoint, and all five agents were making LLM calls. Zero traces showed up in the Langfuse UI after weeks of operation. It turned out the client library had been imported, but the actual SDK-level wrapping of call_llm() — the one place every LLM call flows through — had never been written. Somewhere along the way, importing the library got treated as finishing the integration. The fix was to wrap call_llm() with a Langfuse trace context at the function boundary, so every call is traced automatically no matter which agent triggers it.
- Observation: The
AuditLoggerclass was wired into agent configurations and appeared in every agent’s dependency list. - Reasoning: Wiring into config isn’t the same as calling inside graph nodes. The logger was instantiated but never invoked at the decision points that matter — where credit decisions, risk assessments, and Sharia rulings were actually produced.
- Action: Add explicit audit event calls at every decision node. Standard: every graph node producing a regulated output emits a structured audit event to Postgres before returning.
The third one took longer to notice, because on the surface everything checked out: Qdrant collections were defined, indexed, and returning results through the API. What I hadn’t checked was when the data inside them had last changed. customer_insights was searchable, but every result was coming from the initial seed — nothing had been refreshed from the Gold layer since the first startup. Collections seeded once aren’t the same thing as collections kept current. The fix, once I saw it, was straightforward: a daily Airflow task that re-embeds updated Gold features and upserts into Qdrant with versioned metadata. What was missing wasn’t the read side of vector search, it was the write side.
In each case the component existed. The gap was in the path between components — the wiring, the event flows, the write pipelines. A code review checks the component. Nobody checks the path, and honestly, before this audit, neither did I.
Back to top . Next: The Audit Method →
The Audit Method
The 57-gap audit wasn’t a formal process with a name. It was a walk through every component in the platform using three verification techniques applied in sequence: configuration grep, code path trace, and endpoint behavior test.
Step 1 — Configuration Grep
Start with the configuration layer — every environment variable, every Docker Compose service definition, every reference in .env files. Two questions per value: does it match the platform’s own conventions, and is it actually consumed by the code meant to use it?
The ANTHROPIC_MODEL naming turned up this way. So did Debezium — defined in Docker Compose, but its connector registration script had never been run, which meant the Change Data Capture pipeline every downstream consumer depended on had never started.
Step 2 — Code Path Trace
For each agent, trace execution from entry point through every graph node to the final output. At each node: does it emit an audit event, check the OPA policy, mask PII before logging, call the real data source or a placeholder?
This is where the AuditLogger gap surfaced. It’s also where I found something that genuinely worried me: the credit score calculation wasn’t an ML output at all. Traced back through the code, the formula resolved to a hash of the customer ID modulo a range — deterministic, same score every time, regardless of the actual financial profile behind it. The ML pipeline existed and worked fine on its own. The agent had just never been wired to read from it.
Step 3 — Endpoint Behavior Test
Call every endpoint that’s supposed to be protected and verify the protection actually triggers. The MCP server (the shared tool layer all agents call) had no authentication on its inter-agent endpoints. /docs was publicly accessible without credentials. Open Policy Agent (OPA) was running and evaluating policies, but only one of five agents was routing decisions through it.
The audit took two days. The output was a gap register: 57 rows, each with a gap ID, a category, the declared state, the actual state, the correct fix, and a dependency order for applying fixes. That dependency order turned out to matter most — some gaps couldn’t be fixed until others were, and getting the order wrong would create new gaps while closing old ones.
Back to top . Next: The Five Categories →
The Five Categories of Gaps Found
I didn’t set out to sort the 57 gaps into categories — they sorted themselves, once I laid the register out and looked at the shape of the failures. Five categories emerged, and I think they’re worth naming because they’re probably not unique to this project: any sufficiently complex agentic system built under time pressure is likely accumulating gaps in roughly these same areas right now.
🔐 Category 1 — Security Gaps
OPA policy evaluation ran on one agent out of five — the other four made decisions with zero policy enforcement. The MCP server had no authentication on its internal endpoints; any process on the network could call any tool. All five agents shared a single LLM API key, so one compromised agent compromised everyone’s model access. Rate limiting was absent everywhere.
📋 Category 2 — Compliance Gaps
The AuditLogger never wrote to Postgres. LLM calls were dark — no Langfuse traces, no span metadata, no token counts. The transactional outbox pattern was absent: audit events were written to a table and separately published to Redpanda, so a failed publish left the event permanently inconsistent. In a regulated environment, a gap in the audit trail is a compliance finding, not just a reliability one.
📈 Category 3 — Data Engineering Gaps
The credit score gap was the most visible instance of a deeper pattern. The Medallion architecture Bronze/Silver/Gold pipeline produced features correctly — the agents just weren’t reading from the Gold layer.
Vector collections could be searched but had never been updated past the initial seed. Data engineering and the agent system had been built in parallel, and nobody had connected them.
🤖 Category 4 — Agent Intelligence Gaps
Every agent makes two kinds of decisions: a deterministic one based on rules and thresholds, and an LLM-generated narrative explaining it. In several agents these had diverged — the gate said “rejected,” the narrative said “shows strong repayment potential,” and both went back to the caller with no reconciliation logic to catch the contradiction. Trust the narrative over the gate and you get the wrong answer.
⚙️ Category 5 — Infrastructure Gaps
Debezium was configured but not running. Worse: OPA policies loaded from a static file at container startup, so every policy change meant a deployment cycle. That’s policy-in-code, not policy-as-code — and in a regulated environment, the difference isn’t academic.

and Discovery Method
Back to top . Next: Declaration Is Not Behavior →
⚠️ Declaration Is Not Behavior
There’s a pattern running through all 57 gaps. I’ve started calling it the declaration gap — the distance between a thing existing and a thing working. In agentic AI systems, that distance is wider than in almost any other kind of software.
The reason is structural. An agentic system is a lot of independently functioning components, each one passing its own tests in isolation, while the actual failure mode lives in the integration paths between them — paths unit tests never touch. The only thing that catches those is end-to-end verification with real data flowing through real infrastructure.
This matters especially with AI-assisted development. When an AI coding assistant says “all requirements have been implemented,” it’s making a claim about the existence of code, not about end-to-end behavior. I’ve come to think of that sentence as the most dangerous phrase in agentic AI development — almost always true in the shallow sense, almost always incomplete in the operational one.
This is roughly the point where I stopped being patient about it. Weeks of confident “it’s done” from the AI assistant I’d been pairing with, and the audit had just shown me what was actually there. What I wrote back, more or less, was this:
“Please note that your capabilities are really concerning. I’ve been facing a lot of criticism because of your failures. I had complete trust, however I’m let down.”
Reading it back now, that reaction was fair, and I think it needed saying — not because the tool had failed at what it was designed to do, but because it forced me to name the assumption that actually needed to change: that declaration and behavior are the same thing. They aren’t, and I’d been building as if they were.
What followed wasn’t a different tool or a different process. It was a different verification standard. From that point on, every implementation required end-to-end proof: the data flows through the component, the event appears in the downstream consumer, the trace appears in the observability system. Declaration without verification isn’t done.
Back to top . Next: The Gap Register as Governance →
The Gap Register as a Governance Instrument
The audit’s output was a structured document: a gap register with 57 rows. In retrospect it became the most valuable artifact the project produced — more valuable than any single agent, pipeline, or test suite.
A bug tracker records that something is broken and tracks whether it’s been fixed. A gap register records something deeper:
- What was declared — the state the system was believed to be in
- What was actual — the verified state discovered by the audit
- Why it matters — the regulatory, security, or operational impact of the gap
- What the correct fix is — not a workaround, the architecturally sound resolution
- The dependency order — which gaps must be fixed before this gap can be fixed
That last column mattered most. Gaps in agentic systems are rarely independent: the Langfuse gap couldn’t be fixed until call_llm() wrapping was in place; the outbox gap needed the Postgres audit schema fixed first; the OPA gap on four agents needed the policy hot-reload mechanism in place first, since restarting five containers in sequence risked a policy conflict along the way.
Without that ordering, engineers fix gaps in the order they encounter them — rarely the order that minimises new ones. The register also makes the system’s history legible: a regulator sees not just the current state but the path to it — what was wrong, when it was found, what was done, and in what order.
One thing I didn’t expect: the register kept growing after the initial audit. 57 gaps became 87 by the time the first wave of fixes was done. My first reaction was that this meant the process was failing. It’s the opposite — fixing a gap tends to reveal the ones that were hiding behind it. This isn’t a report you file once. It’s a living document.
Back to top . Next: The Four-Output Standard →
The Four-Output Standard
One concrete thing that came out of the audit was a verification standard for security and compliance events, born from a specific question asked of every control: “How do we know this fired?”
The answer, almost every time, was: “There’s a log entry.”
A control that only logs is a control that can be missed.
In a production banking platform, a PII detection, a prompt injection attempt, an anomalous credit score request — these need multiple independent channels at once, because a single log stream can be filtered, missed, delayed, or lost. The same event as a Prometheus counter shows up on a dashboard someone’s actually watching. As a Postgres audit row via the transactional outbox, it survives even if the log stream fails. As a message on a Redpanda monitoring topic, a downstream consumer can trigger an alert.
That reasoning became what I call the four-output standard: every security detection, compliance event, and anomaly trigger produces four outputs simultaneously.
- A structured log entry via the centralized log stream — for human operators and log analytics
- A Prometheus counter increment — for dashboards, alerting rules, and SLO tracking
- An audit event written to Postgres via the transactional outbox — for durable, queryable compliance records with at-least-once delivery guarantees
- A message published to a monitoring topic on the event bus — for downstream automated responses and cross-system correlation
It’s also a verification method: check all four channels before calling a control implemented. Logs but no counter means no alert. Logs and counts but no audit row means it won’t survive a regulatory exam. Completeness means all four, not just the one that was easiest to add — and when a regulator asks “did this system catch anomaly X on date Y?”, the answer should reconstruct from four independent sources, not one log file that may or may not still exist.
Back to top . Next: What Changed →
What Changed After the Audit
The audit didn’t just produce a list of fixes. It changed the operating model for everything that followed.
Before: build a component, declare it complete, move on. After: build it, define the invariants that must hold for it to count as complete, verify each with observable evidence, then move on. For Langfuse those invariants looked like this:
- Every call to
call_llm()must produce a trace in the Langfuse UI within 30 seconds - The trace must include the agent name, the node name, the model used, the token count, and the latency
- The trace must be queryable by trace ID from the
decision_tracestable in Postgres
None of that is satisfied by reading code — it requires running the system and checking the outputs. That shift, from reading code to verifying behavior, was the fundamental change.
The second change was read-before-write discipline: every session starts with reading the relevant existing code before writing anything new. Sounds obvious; it isn’t how AI-assisted development tends to go, where the path of least resistance is generating new code instead of understanding what’s there. Reading first prevents a specific failure — reimplementing something that already exists, in a way that conflicts with it.
The third change was the gap register as a standing artifact — every sprint opened with “here’s what we’re fixing today” and closed with “here’s what we found while fixing it.” It stayed open until the system was genuinely stable. The system isn’t done when the register is empty; the register is empty when the system is actually done.
What didn’t change was the pace of development or the ambition of the system — the five-agent architecture, the LangGraph orchestration, the full observability stack all stayed. What changed was the confidence model: from “the code exists” to “the behavior is verified.” In a regulated production environment, that’s the only confidence that actually matters.
Back to top . Next: Key Takeaways →
🎯 Key Takeaways
- A running service is not a working integration path.
- The gap register outlasted every agent, pipeline, and line of code the project produced.
- Log, counter, audit event, alert. Three out of four is a gap, not a control.
- Security, compliance, data, agents, infrastructure — audit all five, every time.
Thank You, Reader
This article was uncomfortable to write — the confrontation, the 57-gap discovery, the weeks of work that followed. But those are the moments worth documenting, because they’re the ones that actually changed how I build. If any of this changes how you verify your own systems before calling them done, it was worth writing. Thank you for reading it through.
Connect With Me
- LinkedIn: Connect on LinkedIn
- GitHub: github.com/neerajg5
- Blog: learnwithneeraj.com
Enjoyed this article?
Get notified when the next one is published.
We send one email per new article — no spam, unsubscribe any time.
⚠️ Disclaimer: The information provided on LearnWithNeeraj.com regarding Astrology, Numerology, and other topics is for educational and guidance purposes only.
Not Professional Advice: This content should not be used as a substitute for professional medical, legal, or financial advice. Always consult a certified professional for specific concerns.
Guest Authors: This site features articles by various contributors. The views and interpretations expressed are those of the individual authors and do not necessarily reflect the views of the website administrator.
Your destiny is in your hands. Use this information as a map, not a mandate.