<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><generator uri="https://jekyllrb.com/" version="4.3.4">Jekyll</generator><link href="https://thinkinginsidethebox.ai/feed.xml" rel="self" type="application/atom+xml" /><link href="https://thinkinginsidethebox.ai/" rel="alternate" type="text/html" hreflang="en" /><updated>2026-10-11T18:44:38+08:00</updated><id>https://thinkinginsidethebox.ai/feed.xml</id><title type="html">Thinking Inside the Box</title><subtitle>Agentic AI articles by Vincent Caldeira and collaborating authors — on safety, metacognition, evaluation-driven development, and open projects including MCP and agentgateway.</subtitle><author><name>Vincent Caldeira</name></author><entry><title type="html">Decision Models in Enterprise Agentic Architecture: Performance, Safety, and MCP</title><link href="https://thinkinginsidethebox.ai/aaif/engineering/2026/10/11/decision-models-enterprise-agentic-architecture.html" rel="alternate" type="text/html" title="Decision Models in Enterprise Agentic Architecture: Performance, Safety, and MCP" /><published>2026-10-11T09:00:00+08:00</published><updated>2026-10-11T09:00:00+08:00</updated><id>https://thinkinginsidethebox.ai/aaif/engineering/2026/10/11/decision-models-enterprise-agentic-architecture</id><content type="html" xml:base="https://thinkinginsidethebox.ai/aaif/engineering/2026/10/11/decision-models-enterprise-agentic-architecture.html"><![CDATA[<p><em>A short note for developers and software architects building agents. It draws on the survey in <a href="https://github.com/caldeirav/finagent-mesh-mcp/blob/main/Architectural%20Foundations%20for%20Open%20Decision%20Models.md">Architectural Foundations for Open Decision Models</a> and on the concrete agent and benchmark in <a href="https://github.com/caldeirav/finagent-mesh-mcp">finagent-mesh-mcp</a>.</em></p>

<hr />

<h2 id="the-control-plane-problem-in-agent-stacks">The control-plane problem in agent stacks</h2>

<p>Most agent frameworks use a general-purpose language model for almost everything: picking a tool, deciding which document to read, checking whether a step succeeded, then writing the final answer. That works in demos. In enterprise systems it is expensive and hard to govern.</p>

<p>When the model must <strong>route</strong>—choose among a known set of filing types, tools, or policies—it often spends hundreds of milliseconds writing prose or JSON just to emit a label. The “confidence” it prints in text is not a contract you can threshold in production. Wrong routes waste tool calls, leak the wrong data into the context window, and create audit gaps.</p>

<p><strong>Decision models</strong> (sometimes called System-1 decision engines) attack that gap. Instead of generating chat text to pick an option, they take a shared context and a typed question—<strong>Choice</strong> (pick one of N options), <strong>Score</strong> (rank or grade candidates), or a yes/no check—and return <strong>structured probabilities</strong> in one forward pass. Open-weight families (Decision-2.0, CLM, AnyJev wrappers, Laya, and others) make this pattern self-hostable. The longer survey covers the landscape; this note focuses on why that matters for enterprise agents and how we exercised it here.</p>

<hr />

<h2 id="what-we-built">What we built</h2>

<p><strong>finagent-mesh-mcp</strong> is a hybrid financial agent and evaluation harness aimed at a realistic enterprise pattern: answer questions that depend on <strong>SEC filings</strong>, with clear stages and observable decisions.</p>

<p>Per question, a LangGraph agent roughly does:</p>

<ol>
  <li><strong>Stage 1 — Choice.</strong> Given the user question and available material, rank which filing <em>type</em> to use (for example 10-K, 10-Q, 8-K, DEF14A, Earnings).</li>
  <li><strong>Stage 2 — Score (or a passage ranker).</strong> Inside the top filing type, rank the passages that should ground the answer.</li>
  <li><strong>Optional System-2.</strong> A frontier model (Gemini Flash in our stack) may write a final answer from the top-ranked passages only—and <strong>fails closed</strong> if that call cannot run (no invented extractive answer).</li>
  <li><strong>Tools via MCP.</strong> Filing access and financial calculations go through MCP servers behind an agent gateway—not ad-hoc code in the prompt.</li>
</ol>

<p>Local open decision engines serve Stage 1 and Stage 2 over a shared <code class="language-plaintext highlighter-rouge">/v1/systemone</code>-style API. Heavy models run on the workstation sequentially; everything important is traced in MLflow and checkpointed in a resume-safe ledger.</p>

<p>So the product shape is familiar to architects: <strong>typed routing → evidence ranking → optional generative answer → governed tools</strong>, with decision models on the hot path and a large language model only where generation is actually required.</p>

<hr />

<h2 id="why-finagentbench">Why FinAgentBench</h2>

<p>We evaluate on <strong>FinAgentBench</strong> (Choi et al., ACM ICAIF 2025): expert-labeled questions over S&amp;P 500–related filings with two retrieval steps that map cleanly onto Choice then Score.</p>

<p>That matters more than picking a generic chatbot scoreboard:</p>

<ul>
  <li><strong>The task matches the architecture.</strong> Stage 1 is “which document class?” Stage 2 is “which passages?” Those are the same decisions a production agent must get right before it calls tools or writes to a user.</li>
  <li><strong>Labels exist for routing quality.</strong> We can measure whether the agent picked the right filing type and whether the right passages rose to the top—without pretending free-form answers have gold text when they do not.</li>
  <li><strong>It is enterprise-shaped.</strong> Wrong filing type is a <em>routing</em> failure: the agent may never see the right evidence. That shows up as empty or useless Stage-2 pools, not as a mysterious “model was dumb” story.</li>
</ul>

<p>The harness sweeps a <strong>matrix</strong> of open decision engines (and strong search baselines such as BM25 and E5): vary Choice with Score fixed, then vary Score with Choice fixed. The point for architects is not a single leaderboard number; it is evidence about <strong>where latency and quality should be spent</strong>—on the router, on the passage ranker, or on the generative step.</p>

<hr />

<h2 id="what-the-benchmark-showed-developer-takeaways">What the benchmark showed (developer takeaways)</h2>

<p>We ran the required matrix on <strong>200 labeled questions</strong> (seeded sample from the ICAIF challenge dump), ranking-only—no generative answer required for these headlines. Full tables live under <a href="https://github.com/caldeirav/finagent-mesh-mcp/tree/main/artifacts/benchmarks"><code class="language-plaintext highlighter-rouge">artifacts/benchmarks/paper-n200-full*</code></a>; here is what matters if you are designing an agent.</p>

<h3 id="1-open-decision-models-are-already-good-filing-type-routers">1. Open decision models are already good filing-type routers</h3>

<p>When we held passage ranking fixed and only swapped the <strong>Choice</strong> engine:</p>

<ul>
  <li><strong>Order-robust AnyJev</strong> (a training-free wrapper that reduces “first option wins” bias) was the best type router.</li>
  <li>A strong open decision head (<strong>Decision-2.0 Lux</strong>) was close behind.</li>
  <li>A <strong>chat-style JSON baseline</strong> (ask a general 8B model to emit a ranked list) sat clearly below those two.</li>
  <li>Tiny edge routers (<strong>Kai</strong>, <strong>Laya</strong>) were faster to think about for high-frequency gates, but weaker at this filing-type job.</li>
</ul>

<p>In plain terms: for “which document class should we open?”, <strong>specialized decision Choice beat “just ask the LLM for JSON.”</strong> That is the control-plane win you care about before any MCP call or user-facing paragraph.</p>

<p>We also re-ran the core routers on <strong>three different 200-question draws</strong>. AnyJev stayed ahead of Lux on type routing every time—so that ranking is not a one-draw fluke.</p>

<h3 id="2-the-binding-constraint-is-often-the-route-not-the-prose-model">2. The binding constraint is often the route, not the prose model</h3>

<p>With Lux as Choice, roughly <strong>four in ten</strong> questions never produced a usable Stage-2 passage list (“empty pool” after the type filter). Only about <strong>six in ten</strong> questions got a full Choice→Score path.</p>

<p>Importantly, many empties were <strong>not</strong> “wrong filing type.” Quite often the router picked a type that looks right on the label, but <strong>that example had no chunks of that type in the dump</strong>—so Stage 2 had nothing to rank. For architects, that is the same failure mode as a tool catalog or RAG index that does not contain the object you routed to: <strong>the decision layer cannot recover what the filter discarded.</strong></p>

<p>So if your agent “feels broken,” measure <strong>yield</strong> (fraction of requests that reach a ranked evidence set) separately from “was the answer eloquent?”</p>

<h3 id="3-after-type-routing-classical-search-still-set-the-pace">3. After type routing, classical search still set the pace</h3>

<p>When we held Choice fixed at Lux and only swapped Stage-2:</p>

<ul>
  <li>General <strong>dense retrieval (E5)</strong> ranked passages best among the required rows.</li>
  <li>Simple <strong>BM25</strong> was close behind—and essentially free at runtime.</li>
  <li>Lux’s own <strong>Score</strong> head was competitive but behind E5/BM25 on this sample.</li>
  <li>Zero-shot <strong>CLM Action Cache</strong> was clearly weaker until you shortlist (an optional BM25→CLM path improved CLM but still did not beat E5/BM25).</li>
</ul>

<p>Translation for product design: <strong>do not assume the same decision family that wins Choice also wins long passage lists.</strong> A strong pattern we are exploring next is “best router × proven search” (for example AnyJev Choice with E5 or BM25 Score), plus shortlisting before an expensive Score head.</p>

<p>A one-shot generative ranker that skips the two-stage design and ranks all chunks in one go can look competitive on passage metrics—but it <strong>collapses</strong> the typed Choice→Score contract we want for MCP gating and audit. Treat it as a baseline, not as the production architecture.</p>

<h3 id="4-generation-is-optionaland-answer-accuracy-needs-its-own-labels">4. Generation is optional—and answer “accuracy” needs its own labels</h3>

<p>We attached Gemini Flash to saved rankings for the best pairs (no re-routing). Synthesis ran cleanly when evidence existed. The challenge dump we use, however, <strong>does not ship free-form gold answers</strong>, so classic exact-match answer scores stay empty. That is a feature of honest evaluation: <strong>prove routing and evidence first</strong>; judge prose when you have human answers or a review loop (for example MLflow evaluation later).</p>

<hr />

<h2 id="opportunity-performance">Opportunity: performance</h2>

<p>Decision models are a <strong>control plane</strong> optimization:</p>

<table>
  <thead>
    <tr>
      <th>Concern</th>
      <th>LLM-only control loop</th>
      <th>Decision-model control loop</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Pick among known options</td>
      <td>Generate text / JSON, then parse</td>
      <td>One pass → ranked options + probabilities</td>
    </tr>
    <tr>
      <td>Latency</td>
      <td>Dominated by decoding</td>
      <td>Often tens of milliseconds for compact routers; larger long-context heads still avoid full answer generation</td>
    </tr>
    <tr>
      <td>Cost</td>
      <td>Tokens on every route</td>
      <td>Local inference on discrete decisions; escalate to a large model only when needed</td>
    </tr>
    <tr>
      <td>Tool catalogs</td>
      <td>Prompt bloat and brittle parsing</td>
      <td>Choice / Score over an explicit candidate list (tools, filing types, policies)</td>
    </tr>
  </tbody>
</table>

<p>In our agent, Stage 1 and Stage 2 are exactly those discrete decisions. Generative System-2 runs <strong>after</strong> evidence is selected, on a short list of passages. That is the enterprise performance pattern: <strong>cheap, local, typed decisions on the loop; expensive generation at the edge.</strong></p>

<p>The FinAgentBench matrix sharpens that story: <strong>pay for a good Choice router</strong> (AnyJev / Lux beat JSON chat), then <strong>pair it with an efficient passage ranker</strong> (E5 or BM25 on this task) instead of assuming one heavy model should own both hops. Tiny routers remain attractive for high-frequency, short-context gates—just not as drop-in winners for long SEC-type triage in our sample.</p>

<p>The survey also notes patterns that map directly to product design: pre-computing embeddings for a stable tool registry (Action Cache–style), and order-robust Choice when menu order must not change the decision (exactly why AnyJev helped on Stage 1).</p>

<hr />

<h2 id="opportunity-safety-and-operability">Opportunity: safety and operability</h2>

<p>Safety here is not “the model never errs.” It is <strong>failing in ways you can see and contain</strong>.</p>

<ol>
  <li><strong>Explicit candidate sets.</strong> Choice and Score only decide among options you pass in. Unauthorized tools and document classes never appear unless your application puts them on the list.</li>
  <li><strong>Local System-1 first.</strong> Routing and tool choice can stay on-premises open weights. Cloud generation is an escalation, not the default gatekeeper—aligned with data-sovereignty and blast-radius control.</li>
  <li><strong>Fail closed.</strong> If synthesis cannot run, we do not fabricate an answer from passages. If a route yields no evidence pool, that is recorded as a routing / yield issue, not papered over—and in our run that empty-pool rate was high enough that ignoring it would mislead any “answer quality” dashboard.</li>
  <li><strong>Deterministic work stays deterministic.</strong> Ratios, NPV-style calculations, and similar numerics belong in <strong>MCP tools</strong>, not in free-form model arithmetic. Decision models should not replace hard authorization rules either; they route <em>to</em> policies, they do not invent them.</li>
  <li><strong>Auditability.</strong> Structured probabilities, ranked IDs, and MCP tool I/O fit span traces. That is closer to how SRE and compliance teams already reason about systems than a wall of chain-of-thought text.</li>
</ol>

<p>Our empty-pool accounting is the enterprise lesson in miniature: <strong>log “routed to X but nothing to fetch”</strong> the same way you would log “selected MCP tool Y but the server returned not found.” Calibrated confidence (when you invest in it) then enables <strong>coverage under an error budget</strong>: automate high-confidence routes, escalate ambiguous ones. The architectural foundations survey discusses that pattern; this harness first makes the two-stage retrieval decisions measurable.</p>

<hr />

<h2 id="role-of-mcpand-why-decision-models-fit-mcp-servers-better">Role of MCP—and why decision models fit MCP servers better</h2>

<p><a href="https://modelcontextprotocol.io/">Model Context Protocol (MCP)</a> is how this stack exposes tools: SEC filing helpers and a financial calculator are MCP servers. Clients talk to them only through an <strong>agent gateway</strong>. That keeps schemas, auth, and side effects in one place.</p>

<p>Decision models improve that integration in three practical ways:</p>

<p><strong>1. Tool choice becomes a typed Choice over the MCP catalog.</strong><br />
Instead of asking a chat model to “pick a tool” and hoping the JSON matches your MCP <code class="language-plaintext highlighter-rouge">tools/list</code>, you pass the registered tool names (and short descriptions) as Choice candidates. The decision engine returns a distribution over <em>those</em> names. Wiring to <code class="language-plaintext highlighter-rouge">tools/call</code> is then ordinary application code: selected name → gateway → MCP server. Fewer parse failures, fewer hallucinated tool names.</p>

<p><strong>2. Preconditions and gates become Score / yes–no checks before the call.</strong><br />
Before invoking an MCP server that hits EDGAR or runs a calculation, a local decision can ask: “Is the selected filing type appropriate?” or “Is this step ready to call the calculator?” Low confidence → skip the call, ask for clarification, or escalate. That reduces unsafe or wasteful MCP traffic—the same spirit as “local System-1 before cloud System-2.”</p>

<p><strong>3. MCP stays the system of record for effects; the LLM stays optional for language.</strong><br />
MCP servers own I/O and deterministic logic. Decision models own <em>which</em> server and <em>which</em> arguments path to take. A generative model, if used, consumes tool results and ranked evidence to write user-facing text. Clear boundaries make it easier to swap MCP servers, add new tools to the Choice list, and keep traces meaningful (“Choice selected <code class="language-plaintext highlighter-rouge">mcp-sec-edgar.get_filing</code> with p=…”).</p>

<p>In short: <strong>MCP standardizes tools; decision models standardize how agents select and gate those tools.</strong> Together they replace “prompt spaghetti + hope” with a small, inspectable control loop.</p>

<hr />

<h2 id="takeaways-for-implementers">Takeaways for implementers</h2>

<ol>
  <li><strong>Split the agent into decisions vs generation.</strong> Use open decision models for Choice/Score on the hot path; reserve large language models for synthesis and dialogue.</li>
  <li><strong>Benchmark the decisions that hurt you.</strong> On FinAgentBench, typed Choice already beat JSON chat for filing-type routing; passage quality still favored proven search (E5/BM25). Measure those hops separately before arguing about answer prose.</li>
  <li><strong>Watch yield, not only “accuracy.”</strong> Roughly 40% empty Stage-2 pools in our Lux Choice run would look like “bad answers” if you only scored the final paragraph.</li>
  <li><strong>Put tools behind MCP and select them with Choice.</strong> Keep numerics and side effects in servers; keep auth deterministic.</li>
  <li><strong>Fail closed and trace everything.</strong> Empty routes, skipped synthesis, and tool calls should be first-class outcomes in your observability story.</li>
  <li><strong>Self-host when the control plane is sensitive.</strong> Open decision weights plus local MCP is a coherent enterprise deployment story—performance <em>and</em> a smaller trust boundary.</li>
</ol>

<p>For depth on model families, latency trade-offs, and failure modes (order sensitivity, candidate filtering ceilings, when <em>not</em> to use decision models), see <a href="https://github.com/caldeirav/finagent-mesh-mcp/blob/main/Architectural%20Foundations%20for%20Open%20Decision%20Models.md">Architectural Foundations for Open Decision Models</a>. For numbers, matrix commands, and artifacts, see the <a href="https://github.com/caldeirav/finagent-mesh-mcp/blob/main/README.md">finagent-mesh-mcp README</a> (<code class="language-plaintext highlighter-rouge">paper-n200-full</code> analysis).</p>]]></content><author><name>Vincent Caldeira</name></author><category term="AAIF" /><category term="Engineering" /><summary type="html"><![CDATA[Open decision models as the agent control plane: typed Choice and Score beat JSON chat for routing, pair with proven search for passages, and gate MCP tools with structured probabilities.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://thinkinginsidethebox.ai/assets/images/og/decision-models-enterprise-agentic-architecture.png" /><media:content medium="image" url="https://thinkinginsidethebox.ai/assets/images/og/decision-models-enterprise-agentic-architecture.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Improve SLM tool calling without post-training</title><link href="https://thinkinginsidethebox.ai/aaif/engineering/2026/09/17/tool-retrieval-for-slm-agents.html" rel="alternate" type="text/html" title="Improve SLM tool calling without post-training" /><published>2026-09-17T09:00:00+08:00</published><updated>2026-09-17T09:00:00+08:00</updated><id>https://thinkinginsidethebox.ai/aaif/engineering/2026/09/17/tool-retrieval-for-slm-agents</id><content type="html" xml:base="https://thinkinginsidethebox.ai/aaif/engineering/2026/09/17/tool-retrieval-for-slm-agents.html"><![CDATA[<p>When a small language model (SLM) fumbles tool calling, the usual advice is to fine-tune it or replace it with a bigger one. There is a cheaper move that stays in your application. <strong>Keep the SLM. Change what it sees.</strong></p>

<p>MCP made it easy to plug tools into an agent. A GitHub server, a Jira server, a Slack server, an internal API — <code class="language-plaintext highlighter-rouge">tools/list</code> returns them all, and most frameworks bind the whole list into the next model call. That is a good integration story. It is a bad inference story for a 3B or 8B model.</p>

<p>A small model can call a tool. It cannot search a catalog. When you paste hundreds of function schemas into the prompt, you are asking it to do library search and function calling in the same pass. Those are different jobs. <strong>Calling is already in the weights. Search is yours</strong> — and you can do it without post-training.</p>

<h2 id="the-slm-is-not-the-bottleneck">The SLM is not the bottleneck</h2>

<p>The failure is not “small models cannot do tool calling.” It is “small models cannot do tool calling <strong>over an unbounded registry</strong>.” Give them a short, allowed list and they start looking like a much larger model on the same task.</p>

<p>We ran five locally served models against a <strong>shared catalog of 443 tools</strong> — every unique function in a BFCL Multiple split, bound on every query. Same 200 prompts, same catalog. The baseline gave the model the lot.</p>

<p>That baseline fails in three different ways, depending on model size:</p>

<ul>
  <li><strong>Parse collapse (3B–8B Llama).</strong> Llama 3.2 3B named the right tool on <strong>2.5%</strong> of queries; Llama 3.1 8B on <strong>6%</strong>. The usual error was not a wrong pick. It was no valid call at all. ~60k tokens of tool JSON is enough to make the model stop producing a structured tool call.</li>
  <li><strong>Wrong-tool saturation (7B).</strong> Qwen2.5 7B could emit calls (40% name accuracy) but spent most of the rest on the <em>wrong</em> function. It was searching 443 names, not filling one schema.</li>
  <li><strong>Usable but worse (32B–70B).</strong> Llama 3.3 70B reached <strong>79%</strong> on the full catalog. That is deployable. It is not the ceiling. Extra tools still cost tokens, latency, and a higher chance of picking a near-duplicate name.</li>
</ul>

<p>You do not need a new model family to see the pattern. The catalog is in the prompt. Attention is finite. Binding everything is an application choice — which means you can undo it without touching weights.</p>

<h2 id="how-this-was-measured">How this was measured</h2>

<p>The numbers come from a <a href="https://gorilla.cs.berkeley.edu/leaderboard.html">BFCL</a>-derived harness — the Berkeley Function-Calling Leaderboard, built to test whether a model can pick a function and fill its arguments.</p>

<p>A typical BFCL item is small: a user prompt, a short list of function definitions, and a gold tool call. The <strong>Multiple</strong> split is the interesting one for catalogs: several candidate functions sit next to the query, and the model must choose. Official scoring does not execute tools. It parses the model’s predicted call and checks it against <code class="language-plaintext highlighter-rouge">possible_answer</code> — a structured set of acceptable names and argument values (optional fields may be omitted).</p>

<p>Our setup is harder than that per-item list. We collected every function from the Multiple split into <strong>one shared catalog</strong> of 443 tools. When two entries used the same name with different schemas, we kept the first definition we saw. Every query then ran against that full catalog, not against a handful of candidates packed with the prompt.</p>

<p>The baseline attached all 443 tools to the model call. The other runs first retrieved about ten tools, then attached only those. Each trial was a single LangGraph turn: the model had to emit a native tool call, and we never executed the tool. We served five open-weight models locally through llama.cpp.</p>

<p>Two scores matter, and they answer different engineering questions.</p>

<p><strong>Name accuracy</strong> asks only whether the model invoked a ground-truth function name. It measures routing: did it pick the right tool?</p>

<p><strong>AST accuracy</strong> is BFCL’s abstract-syntax-tree check. The model’s output is parsed into a call — a function name plus a map of argument names to values. That structure is compared to <code class="language-plaintext highlighter-rouge">possible_answer</code>, the dataset’s gold call. The score counts only if three things hold: the name is a gold name; every required argument is present; each value is in the allowed list for that field. Optional arguments may be omitted; if they appear, they still have to be allowed. The tool is never executed. The checker grades the <strong>shape of the call</strong>, not a live API result.</p>

<p>That split is the point for AppDev. Name accuracy tells you whether the shortlist and the model agreed on <em>which</em> API to hit. AST accuracy tells you whether the call would have been a legal invocation — right tool, right fields, right values — without standing up Jira or GitHub. A name match with a failed AST is almost always <code class="language-plaintext highlighter-rouge">bad_args</code>.</p>

<table>
  <thead>
    <tr>
      <th>Score</th>
      <th>Passes when</th>
      <th>A fail means</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Name accuracy</strong></td>
      <td>The predicted function name is a ground-truth name.</td>
      <td>Selection / routing is broken.</td>
    </tr>
    <tr>
      <td><strong>AST accuracy</strong></td>
      <td>The parsed call matches <code class="language-plaintext highlighter-rouge">possible_answer</code> on name <strong>and</strong> arguments.</td>
      <td>The name may be right; the arguments are not.</td>
    </tr>
  </tbody>
</table>

<p>Retrieval can move name accuracy a lot. It barely teaches AST accuracy. If you only track “did the agent succeed,” you will mix those bugs and fine-tune the wrong layer.</p>

<p>These scores are internally consistent for this harness. They are not official Gorilla leaderboard numbers. Treat them as evidence for the architecture, not as a production SLA.</p>

<h2 id="what-retrieval-changes--and-what-it-does-not">What retrieval changes — and what it does not</h2>

<p>The table below is tool-name accuracy on the same 200 queries and the same 443-tool catalog. One condition attached every tool. The other retrieved ten, then attached only those.</p>

<table>
  <thead>
    <tr>
      <th>Model</th>
      <th style="text-align: right">Full catalog</th>
      <th style="text-align: right">k=10 retrieved</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Llama 3.2 3B</td>
      <td style="text-align: right">2.5%</td>
      <td style="text-align: right">84.5%</td>
    </tr>
    <tr>
      <td>Llama 3.1 8B</td>
      <td style="text-align: right">6.0%</td>
      <td style="text-align: right">92.0%</td>
    </tr>
    <tr>
      <td>Qwen2.5 7B</td>
      <td style="text-align: right">40.0%</td>
      <td style="text-align: right">87.0%</td>
    </tr>
    <tr>
      <td>Llama 3.3 70B</td>
      <td style="text-align: right">79.0%</td>
      <td style="text-align: right">91.0%</td>
    </tr>
  </tbody>
</table>

<p>With a shortlist, the 8B model beat the 70B model that still saw the full catalog. That is the result that matters for application developers: a cheaper model did the same routing job because the prompt got smaller, with no extra training run. Tool JSON in the prompt shrank by about <strong>98%</strong>, from roughly 60k tokens to 1.4k. Parse failures on the small Llamas dropped to zero, and latency fell with the prompt.</p>

<p>Two limits still apply, and both change how you should build.</p>

<p><strong>Lexical and dense looked similar here. Do not take that as a production default.</strong> BM25 and embedding retrieval landed within a few points on this catalog. BFCL is a good fit for <strong>lexical</strong> search: function names and descriptions often share wording with the prompt (<code class="language-plaintext highlighter-rouge">math.gcd</code> / “greatest common divisor”, <code class="language-plaintext highlighter-rouge">whole_foods.check_price</code> / “price of tomatoes at Whole Foods”). Enterprise catalogs are messier. Tools are named <code class="language-plaintext highlighter-rouge">sync_records</code> or <code class="language-plaintext highlighter-rouge">case_update_v3</code>. Users say “nudge the KYC file after the refresh.” Descriptions are thin, duplicated, or written for humans who already know the system. In that setting you want <strong>hybrid</strong> retrieval: sparse match for tokens that do exist, dense match for paraphrase and intent. The design that always matters is still <em>retrieve, then bind</em>. The ranker you ship should assume the query and the schema will not be as aligned as BFCL.</p>

<p><strong>Retrieval does not fill arguments.</strong> At k=10, name accuracy sat in the mid-80s to low-90s; AST accuracy sat around <strong>46–61%</strong>. Most leftover errors were <code class="language-plaintext highlighter-rouge">bad_args</code>: right function, wrong or missing parameters. If the agent picks the wrong GitHub tool, filter the list. If it picks <code class="language-plaintext highlighter-rouge">create_issue</code> and invents field names, fix the schema, the description, or the model. Do not fine-tune to solve a context problem, and do not expect an embedder to teach JSON.</p>

<h2 id="treat-tools-like-you-already-treat-documents">Treat tools like you already treat documents</h2>

<p>You do not dump the company wiki into every request. You retrieve a few passages, then generate.</p>

<p>Do the same for tools — but start from <strong>what this agent is allowed to use</strong>, not from the raw MCP union:</p>

<ol>
  <li>Resolve identity (user, agent, workload).</li>
  <li>Apply policy so the catalog is already the <em>should-use</em> set.</li>
  <li>On each model call, retrieve from that set using the current messages.</li>
  <li>Bind about <strong>ten</strong> tools. That was the default that held up in the matrix.</li>
  <li>Invoke through the same MCP path. Authorize the call again, not only the list.</li>
</ol>

<p>The model still speaks ordinary tool calling. You did not add a <code class="language-plaintext highlighter-rouge">search_tools</code> meta-tool. You did not rewrite JSON schemas. You changed what is in context, and you changed <strong>when</strong> that list is assembled.</p>

<h2 id="bind-late-from-what-the-agent-should-use">Bind late, from what the agent should use</h2>

<p>Binding is a lifecycle event, not a startup config. If you bind the world when the process boots, you have already lost the two filters that make a small model usable: <strong>permission</strong> and <strong>relevance</strong>.</p>

<p><strong>Identity and IBAC first.</strong> Identity-based access control (IBAC) answers “what may this caller use?” — this user, this agent identity, this tenant, this environment. That is not retrieval. Retrieval answers “what is this turn about?” A support agent on a production JWT should never see <code class="language-plaintext highlighter-rouge">dangerous_delete_prod</code>, even if the user typed “delete.” A retrieval index over the unfiltered MCP union cannot be the only gate.</p>

<p><strong>Then retrieve, then bind.</strong> After IBAC, the allowed dictionary may still be large (dozens of Jira tools, not one). Retrieval cuts that to a shortlist for <em>this</em> prompt. Bind that shortlist on the model call, not earlier. When the conversation shifts, bind again. Sticky reuse across similar turns is fine; a frozen toolset from turn one is not.</p>

<p><strong>Keep k small, then measure.</strong> k=5 missed more ground-truth tools. k=20 sometimes <em>hurt</em> small models by putting near-duplicate names back in the shortlist (<code class="language-plaintext highlighter-rouge">database.query</code> vs <code class="language-plaintext highlighter-rouge">db_fetch_records</code>). Ten is a starting point, not a superstition.</p>

<p><strong>Write descriptions for retrieval.</strong> Name plus description is what gets indexed. Vague copy collides. Distinct copy is catalog hygiene — the same discipline as API docs. Hybrid search does not save you from five tools named <code class="language-plaintext highlighter-rouge">update</code>.</p>

<p><strong>Refresh when the catalog or the identity changes.</strong> Hook <code class="language-plaintext highlighter-rouge">notifications/tools/list_changed</code>, or fingerprint the list. Recompute when the JWT, tenant, or role changes. A stale index is a silent miss or a silent over-grant.</p>

<p><strong>Log the shortlist.</strong> When a call is wrong, you want to know whether the right tool was bound. That splits failures into policy miss, retrieval miss, sibling confusion, and bad arguments.</p>

<p><strong>Do not post-train for selection if a shortlist already works.</strong> Fine-tuning is the right move for argument quality, domain language, or a model that cannot emit tool calls at all. It is the slow move for “there are 200 MCP tools and the 7B model gets lost.” If the cheaper model already routes once the list is small, spend the GPU budget on serving more of it — not on teaching it the catalog.</p>

<h3 id="gateway-or-client">Gateway or client?</h3>

<p><strong>Authorization belongs at the gateway — and again on <code class="language-plaintext highlighter-rouge">tools/call</code>.</strong> A client wrapper that hides tools is a convenience. It is not a control. The agent process still saw the list, or could have. <a href="https://github.com/agentgateway/agentgateway">agentgateway</a> already filters MCP <code class="language-plaintext highlighter-rouge">tools/list</code> from JWT claims and policy, and rejects unauthorized <code class="language-plaintext highlighter-rouge">tools/call</code>. That is the right place for IBAC: one policy point, every framework, tools the identity must not use never enter the client.</p>

<p><strong>Relevance retrieval needs the conversation.</strong> Standard <code class="language-plaintext highlighter-rouge">tools/list</code> has no user prompt. The messages live in the agent (or in a model proxy that is already rewriting the completion request). So the <em>semantic</em> shortlist usually runs next to <code class="language-plaintext highlighter-rouge">bind_tools</code>: in the orchestrator, in middleware, or in an inference gateway that sees the chat. Putting <em>only</em> BM25/embeddings at the MCP perimeter, with no query, cannot rank by intent.</p>

<p>The production shape is therefore two filters, not one:</p>

<ol>
  <li><strong>Gateway (must):</strong> identity + IBAC → authorized tool dictionary. Hide and reject.</li>
  <li><strong>Bind-time (should):</strong> hybrid retrieval over that dictionary → k tools in the prompt.</li>
</ol>

<p>Do the first without the second, and a permitted catalog of 80 tools still drowns an SLM. Do the second without the first, and retrieval becomes a soft ACL. ToolScope, FastMCP wrappers, and LangChain middleware sit naturally on step 2. They should consume the <strong>already-authorized</strong> list from step 1, not the raw union of every MCP server.</p>

<h2 id="where-toolscope-fits">Where ToolScope fits</h2>

<p><a href="https://github.com/ilya-kolchinsky/ToolScope">ToolScope</a> (<code class="language-plaintext highlighter-rouge">pip install toolscope</code>) is a Python library for that bind-time step. It normalizes OpenAI and MCP tool shapes, embeds name and description, and returns the <strong>original</strong> tool objects unchanged. It does not replace IBAC. It ranks whatever catalog you hand it.</p>

<p>It is a good fit when:</p>

<ul>
  <li>you want an SLM (or a cheaper mid-size model) to <em>route</em> tools well without a custom fine-tune</li>
  <li>the identity-scoped catalog is still large (more than roughly twenty tools, often from one or more MCP servers)</li>
  <li>you want to keep LangChain, LangGraph, or FastMCP as they are</li>
  <li>you do not want meta-tools</li>
</ul>

<p>It is the wrong first move when the allowed set is already tiny, or when the model already picks the name and fails on arguments. It is also the wrong <em>only</em> move when the problem is “this agent should not see that tool.” That is gateway policy.</p>

<p>On BFCL, BM25 alone recovered most of the selection gain — because the dataset is lexically friendly. In an enterprise catalog you still want dense retrieval in the mix, plus tags/allow-deny as a coarse prior. ToolScope is useful when you want that gate packaged: mixed schemas, optional sticky sessions, optional reranking, and a trace of what was shortlisted. Pair it with lexical search if your names and user wording diverge.</p>

<p>Integration is a wrapper, not a new agent framework. Against FastMCP — after the gateway has already scoped the list:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="n">toolscope.adapters.fastmcp</span> <span class="kn">import</span> <span class="p">(</span>
    <span class="n">ToolScopeFastMCPClient</span><span class="p">,</span>
    <span class="n">FastMCPWrapperConfig</span><span class="p">,</span>
<span class="p">)</span>
<span class="kn">import</span> <span class="n">toolscope</span>

<span class="n">wrapper</span> <span class="o">=</span> <span class="nc">ToolScopeFastMCPClient</span><span class="p">(</span>
    <span class="n">upstream_client</span><span class="p">,</span>
    <span class="n">config</span><span class="o">=</span><span class="nc">FastMCPWrapperConfig</span><span class="p">(</span>
        <span class="n">embedding</span><span class="o">=</span><span class="n">toolscope</span><span class="p">.</span><span class="nc">EmbeddingConfig</span><span class="p">(</span>
            <span class="n">provider</span><span class="o">=</span><span class="sh">"</span><span class="s">http</span><span class="sh">"</span><span class="p">,</span>
            <span class="n">endpoint</span><span class="o">=</span><span class="sh">"</span><span class="s">http://localhost:8000/embed</span><span class="sh">"</span><span class="p">,</span>
            <span class="n">model</span><span class="o">=</span><span class="sh">"</span><span class="s">my-embedding-model</span><span class="sh">"</span><span class="p">,</span>
        <span class="p">),</span>
    <span class="p">),</span>
<span class="p">)</span>

<span class="n">tools</span> <span class="o">=</span> <span class="k">await</span> <span class="n">wrapper</span><span class="p">.</span><span class="nf">list_tools</span><span class="p">(</span>
    <span class="n">messages</span><span class="p">,</span>
    <span class="n">k</span><span class="o">=</span><span class="mi">10</span><span class="p">,</span>
    <span class="n">deny_tags</span><span class="o">=</span><span class="p">[</span><span class="sh">"</span><span class="s">dangerous</span><span class="sh">"</span><span class="p">],</span>
<span class="p">)</span>
<span class="n">result</span> <span class="o">=</span> <span class="k">await</span> <span class="n">wrapper</span><span class="p">.</span><span class="nf">call_tool</span><span class="p">(</span><span class="n">name</span><span class="p">,</span> <span class="n">args</span><span class="p">)</span>
</code></pre></div></div>

<p>The model still sees normal MCP tools — fewer of them. <code class="language-plaintext highlighter-rouge">call_tool</code> is a passthrough; the gateway should still authorize the invocation. LangChain has a middleware that overrides the bound tools per model call the same way. <code class="language-plaintext highlighter-rouge">filter_with_trace</code> records candidate counts, timings, and allow/deny decisions so the shortlist is inspectable.</p>

<p>The eval harness in that repo is how we produced the numbers above. It grades <strong>selection</strong> (right name) separately from <strong>calling</strong> (AST / right arguments). Use that split in your own tests: a retrieval miss is an index problem; <code class="language-plaintext highlighter-rouge">bad_args</code> is not; a tool that should never have been listable is an IBAC problem.</p>

<h2 id="keep-the-small-model">Keep the small model</h2>

<p>You can improve SLM tool calling without post-training. The weights already know how to invoke a function. They do not know how to search a 400-tool MCP union. That search is an engineering step: identity and IBAC at the gateway, hybrid retrieval at bind time.</p>

<p>Keep MCP as the contract. Keep the cheaper model in the loop. Put <strong>identity and IBAC</strong> on the gateway so the dictionary is already what the agent should use. Put <strong>hybrid retrieval</strong> in front of <code class="language-plaintext highlighter-rouge">bind_tools</code> so the prompt is what this turn needs. Train later, if argument quality still needs it. Selection is already solvable in the application.</p>]]></content><author><name>Vincent Caldeira</name></author><category term="AAIF" /><category term="Engineering" /><summary type="html"><![CDATA[An 8B model with a short tool list can beat a 70B model on the full catalog. Keep the cheaper model; filter what it sees — no fine-tune required.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://thinkinginsidethebox.ai/assets/images/og/tool-retrieval-for-slm-agents.png" /><media:content medium="image" url="https://thinkinginsidethebox.ai/assets/images/og/tool-retrieval-for-slm-agents.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Stop Trusting the Black Box: Evaluation-Driven Development for Enterprise Financial Agents</title><link href="https://thinkinginsidethebox.ai/aaif/engineering/2026/08/17/evaluation-driven-development-financial-agents.html" rel="alternate" type="text/html" title="Stop Trusting the Black Box: Evaluation-Driven Development for Enterprise Financial Agents" /><published>2026-08-17T09:00:00+08:00</published><updated>2026-08-17T09:00:00+08:00</updated><id>https://thinkinginsidethebox.ai/aaif/engineering/2026/08/17/evaluation-driven-development-financial-agents</id><content type="html" xml:base="https://thinkinginsidethebox.ai/aaif/engineering/2026/08/17/evaluation-driven-development-financial-agents.html"><![CDATA[<p>Deploying autonomous AI agents into enterprise workflows — financial analysis, market research, automated portfolio briefing — demands a fundamental transition from subjective “vibe checks” to <strong>Evaluation-Driven Development (EDD)</strong>. Non-deterministic LLM outputs, multi-step planning, and distributed tool invocations present severe operational risks around data precision, tool routing errors, and ungrounded synthesis. EDD addresses these failure modes by embedding quantitative testing, distributed tracing, and domain-calibrated evaluation directly into the software development lifecycle.</p>

<hr />

<h2 id="the-case-for-evaluation-driven-development-in-agentic-systems">The Case for Evaluation-Driven Development in Agentic Systems</h2>

<p>Traditional software testing relies on deterministic assertions, while classic machine learning relies on static evaluation datasets. Neither approach is sufficient for agentic architectures, where an LLM dynamically plans execution steps, invokes external tools via standards like the <a href="https://modelcontextprotocol.io">Model Context Protocol (MCP)</a>, and iteratively refines its reasoning based on tool outputs.</p>

<p>EDD structures agent quality assurance around two continuous evaluation loops:</p>

<ul>
  <li><strong>Inner Loop (CI/CD Pre-Deployment):</strong> Developers evaluate prompt templates, tool selection, and answer groundedness against a versioned golden dataset before merging code. By running automated evaluation suites during pull requests, engineering teams catch regressions in tool routing or reasoning logic before deployment.</li>
  <li><strong>Outer Loop (Production Observability):</strong> Live production monitoring continuously traces real-world user interactions with financial agents. Outer-loop evaluation measures hallucination rates on live market queries, tracks tool-call failure rates, and detects semantic drift over time.</li>
</ul>

<hr />

<h2 id="pillar-1-persistent-tracing-and-protocol-level-observability">Pillar 1: Persistent Tracing and Protocol-Level Observability</h2>

<p>Agentic architectures are distributed state machines. When a financial agent processes a request like <em>“Provide a financial analysis of AAPL — cover recent price context, relevant news, key financial-statement signals, and risks,”</em> the system generates a chain of intermediate thoughts, tool calls, and payload responses. Without granular, persistent tracing, debugging why an agent selected an incorrect tool or produced an erroneous financial figure becomes impossible.</p>

<p>Persistent state management relies on tracking every decision point, LLM call, and tool execution inside a persistent backend like SQLite via MLflow Tracing. Storing full execution graphs enables developers to replay agent state, isolate intermediate failures, and audit the exact lineage of every generated financial metric.</p>

<h3 id="overcoming-the-mcp-observability-challenge-with-mlflow-tracing">Overcoming the MCP Observability Challenge with MLflow Tracing</h3>

<p>When agents interact with external tools over MCP, tool execution is decoupled across independent server processes. Standard agent logs fail at the MCP boundary, creating a critical blind spot where engineers cannot distinguish between an LLM tool-selection error, a JSON payload serialization failure, or a downstream data timeout.</p>

<p>Resolving this means capturing OpenTelemetry-style spans with <code class="language-plaintext highlighter-rouge">mlflow.langchain.autolog()</code>. A FastMCP server (<code class="language-plaintext highlighter-rouge">mcp_server.py</code>) exposes live Yahoo Finance tools over stdio. LangGraph binds those tools through <code class="language-plaintext highlighter-rouge">langchain-mcp-adapters</code>. Autolog then records typed <code class="language-plaintext highlighter-rouge">AGENT</code> / <code class="language-plaintext highlighter-rouge">TOOL</code> / <code class="language-plaintext highlighter-rouge">LLM</code> spans — including arguments and results — into <code class="language-plaintext highlighter-rouge">sqlite:///mlflow.db</code> for inspection in the MLflow UI.</p>

<p>The MCP server itself is deliberately small: three market-data tools, no mock tool list.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="n">mcp.server.fastmcp</span> <span class="kn">import</span> <span class="n">FastMCP</span>

<span class="n">mcp</span> <span class="o">=</span> <span class="nc">FastMCP</span><span class="p">(</span><span class="sh">"</span><span class="s">yahoo-finance</span><span class="sh">"</span><span class="p">)</span>

<span class="nd">@mcp.tool</span><span class="p">()</span>
<span class="k">def</span> <span class="nf">get_stock_price</span><span class="p">(</span><span class="n">ticker</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
    <span class="sh">"""</span><span class="s">Return the latest stock price and short context for a ticker.</span><span class="sh">"""</span>
    <span class="n">t</span> <span class="o">=</span> <span class="n">yf</span><span class="p">.</span><span class="nc">Ticker</span><span class="p">(</span><span class="n">ticker</span><span class="p">.</span><span class="nf">strip</span><span class="p">().</span><span class="nf">upper</span><span class="p">())</span>
    <span class="n">info</span> <span class="o">=</span> <span class="n">t</span><span class="p">.</span><span class="n">fast_info</span><span class="p">.</span><span class="n">__dict__</span> <span class="k">if</span> <span class="nf">hasattr</span><span class="p">(</span><span class="n">t</span><span class="p">,</span> <span class="sh">"</span><span class="s">fast_info</span><span class="sh">"</span><span class="p">)</span> <span class="k">else</span> <span class="p">{}</span>
    <span class="n">price</span> <span class="o">=</span> <span class="n">info</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">last_price</span><span class="sh">"</span><span class="p">)</span> <span class="ow">or</span> <span class="n">info</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">lastPrice</span><span class="sh">"</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">json</span><span class="p">.</span><span class="nf">dumps</span><span class="p">({</span>
        <span class="sh">"</span><span class="s">ticker</span><span class="sh">"</span><span class="p">:</span> <span class="n">ticker</span><span class="p">.</span><span class="nf">upper</span><span class="p">(),</span>
        <span class="sh">"</span><span class="s">price</span><span class="sh">"</span><span class="p">:</span> <span class="nf">float</span><span class="p">(</span><span class="n">price</span><span class="p">)</span> <span class="k">if</span> <span class="n">price</span> <span class="ow">is</span> <span class="ow">not</span> <span class="bp">None</span> <span class="k">else</span> <span class="bp">None</span><span class="p">,</span>
        <span class="sh">"</span><span class="s">currency</span><span class="sh">"</span><span class="p">:</span> <span class="n">info</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">currency</span><span class="sh">"</span><span class="p">),</span>
    <span class="p">},</span> <span class="n">default</span><span class="o">=</span><span class="nb">str</span><span class="p">)</span>

<span class="nd">@mcp.tool</span><span class="p">()</span>
<span class="k">def</span> <span class="nf">get_financial_news</span><span class="p">(</span><span class="n">ticker</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">limit</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">5</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
    <span class="sh">"""</span><span class="s">Return recent financial news headlines for a ticker.</span><span class="sh">"""</span>
    <span class="bp">...</span>

<span class="nd">@mcp.tool</span><span class="p">()</span>
<span class="k">def</span> <span class="nf">get_financial_statements</span><span class="p">(</span>
    <span class="n">ticker</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span>
    <span class="n">statement_type</span><span class="p">:</span> <span class="n">Literal</span><span class="p">[</span><span class="sh">"</span><span class="s">income</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">balance</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">cashflow</span><span class="sh">"</span><span class="p">]</span> <span class="o">=</span> <span class="sh">"</span><span class="s">income</span><span class="sh">"</span><span class="p">,</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
    <span class="sh">"""</span><span class="s">Return key financial statement rows for a ticker (truncated).</span><span class="sh">"""</span>
    <span class="bp">...</span>
</code></pre></div></div>

<p>On the agent side, tracing is enabled before the ReAct loop starts. The MCP client launches the FastMCP process, loads the tool schemas, and every subsequent tool invocation lands as a durable <code class="language-plaintext highlighter-rouge">TOOL</code> span:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">mlflow</span><span class="p">.</span><span class="n">langchain</span><span class="p">.</span><span class="nf">autolog</span><span class="p">()</span>
<span class="n">llm</span> <span class="o">=</span> <span class="nf">build_llm</span><span class="p">()</span>
<span class="n">client</span> <span class="o">=</span> <span class="nc">MultiServerMCPClient</span><span class="p">(</span><span class="nf">_mcp_server_command</span><span class="p">())</span>
<span class="n">tools</span> <span class="o">=</span> <span class="k">await</span> <span class="n">client</span><span class="p">.</span><span class="nf">get_tools</span><span class="p">()</span>
<span class="n">agent</span> <span class="o">=</span> <span class="nf">create_react_agent</span><span class="p">(</span><span class="n">llm</span><span class="p">,</span> <span class="n">tools</span><span class="p">,</span> <span class="n">prompt</span><span class="o">=</span><span class="n">SYSTEM_PROMPT</span><span class="p">)</span>
<span class="n">result_state</span> <span class="o">=</span> <span class="k">await</span> <span class="n">agent</span><span class="p">.</span><span class="nf">ainvoke</span><span class="p">(</span>
    <span class="p">{</span><span class="sh">"</span><span class="s">messages</span><span class="sh">"</span><span class="p">:</span> <span class="p">[{</span><span class="sh">"</span><span class="s">role</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">user</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">content</span><span class="sh">"</span><span class="p">:</span> <span class="n">question</span><span class="p">}]},</span>
<span class="p">)</span>
</code></pre></div></div>

<p>That is the audit trail. Open the experiment <strong>edd-financial-assistant</strong> in the MLflow UI, inspect a trace, and you can see which MCP tool was called, with which ticker, and what observation the model used when it wrote the Markdown report.</p>

<hr />

<h2 id="pillar-2-automated-evaluation-pipelines-and-metric-standardization">Pillar 2: Automated Evaluation Pipelines and Metric Standardization</h2>

<p>Scalable agent engineering requires replacing manual inspection with programmatic, reproducible evaluation pipelines. Automated EDD pipelines utilize LLM-as-a-Judge evaluators combined with deterministic heuristic metrics to assess agent outputs across multiple performance dimensions.</p>

<p>Evaluation pipelines leverage <code class="language-plaintext highlighter-rouge">mlflow.genai.evaluate()</code> to execute standardized evaluation suites against agent runs. Rather than scoring overall output text in isolation, the evaluation pipeline breaks down agent execution into distinct quantitative metrics:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Metric</th>
      <th style="text-align: left">Type</th>
      <th style="text-align: left">Target Evaluation Focus</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><strong>RequiredMarkdownSections</strong></td>
      <td style="text-align: left">Code scorer</td>
      <td style="text-align: left">Required <code class="language-plaintext highlighter-rouge">##</code> headings present (Price context, News, Financial statements, Risks/limitations).</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>RequiredToolsUsed</strong></td>
      <td style="text-align: left">Code scorer</td>
      <td style="text-align: left">Required market tools actually called (<code class="language-plaintext highlighter-rouge">get_stock_price</code>, <code class="language-plaintext highlighter-rouge">get_financial_news</code>, <code class="language-plaintext highlighter-rouge">get_financial_statements</code>).</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>ToolCallCorrectness</strong></td>
      <td style="text-align: left">Gemini judge</td>
      <td style="text-align: left">Right tools for the request — selection accuracy against expectations.</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>ToolCallEfficiency</strong></td>
      <td style="text-align: left">Gemini judge</td>
      <td style="text-align: left">Lean tool use — needed calls without redundant thrash.</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Groundedness</strong></td>
      <td style="text-align: left">Gemini judge</td>
      <td style="text-align: left">Numeric and factual claims supported by live tool observations, not invented.</td>
    </tr>
  </tbody>
</table>

<p>Together, the tool scorers target <strong>calling accuracy</strong>; Groundedness targets whether tool <strong>execution results</strong> were used faithfully in the report. Judges score inputs, outputs, and expectations (not raw traces). Tool evidence is compacted into outputs so qualitative judges see the same facts as the UI.</p>

<p>A baseline pass over the versioned golden dataset looks like this:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">scorers</span> <span class="o">=</span> <span class="p">[</span>
    <span class="n">RequiredMarkdownSections</span><span class="p">,</span>
    <span class="n">RequiredToolsUsed</span><span class="p">,</span>
    <span class="o">*</span><span class="nf">build_uncalibrated_judges</span><span class="p">(),</span>
<span class="p">]</span>
<span class="k">with</span> <span class="n">mlflow</span><span class="p">.</span><span class="nf">start_run</span><span class="p">(</span><span class="n">run_name</span><span class="o">=</span><span class="sa">f</span><span class="sh">"</span><span class="s">baseline-eval-</span><span class="si">{</span><span class="n">dataset_version</span><span class="si">}</span><span class="sh">"</span><span class="p">):</span>
    <span class="n">mlflow</span><span class="p">.</span><span class="nf">set_tags</span><span class="p">(</span><span class="n">tags</span><span class="p">)</span>
    <span class="n">results</span> <span class="o">=</span> <span class="n">mlflow</span><span class="p">.</span><span class="n">genai</span><span class="p">.</span><span class="nf">evaluate</span><span class="p">(</span>
        <span class="n">data</span><span class="o">=</span><span class="n">data</span><span class="p">,</span>
        <span class="n">predict_fn</span><span class="o">=</span><span class="n">_predict_fn</span><span class="p">,</span>
        <span class="n">scorers</span><span class="o">=</span><span class="n">scorers</span><span class="p">,</span>
    <span class="p">)</span>
</code></pre></div></div>

<p>The Groundedness judge is explicit about live tool payloads — not frozen snapshots — as the source of truth:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">groundedness</span> <span class="o">=</span> <span class="nf">make_judge</span><span class="p">(</span>
    <span class="n">name</span><span class="o">=</span><span class="sh">"</span><span class="s">Groundedness</span><span class="sh">"</span><span class="p">,</span>
    <span class="n">instructions</span><span class="o">=</span><span class="p">(</span>
        <span class="sh">"</span><span class="s">You are evaluating a financial-analysis agent.</span><span class="se">\n</span><span class="sh">"</span>
        <span class="sh">"</span><span class="s">Inputs: </span><span class="se">\n</span><span class="sh">"</span>
        <span class="sh">"</span><span class="s">Outputs include the Markdown report and tool_observations </span><span class="sh">"</span>
        <span class="sh">"</span><span class="s">(live tool results from this run — not frozen snapshots): </span><span class="se">\n</span><span class="sh">"</span>
        <span class="sh">"</span><span class="s">Return </span><span class="sh">'</span><span class="s">grounded</span><span class="sh">'</span><span class="s"> if numeric/factual claims in the report are supported </span><span class="sh">"</span>
        <span class="sh">"</span><span class="s">by tool_observations; </span><span class="sh">'</span><span class="s">ungrounded</span><span class="sh">'</span><span class="s"> if the answer invents or contradicts </span><span class="sh">"</span>
        <span class="sh">"</span><span class="s">tool data.</span><span class="sh">"</span>
    <span class="p">),</span>
    <span class="n">model</span><span class="o">=</span><span class="sh">"</span><span class="s">gemini:/gemini-2.5-pro</span><span class="sh">"</span><span class="p">,</span>
    <span class="n">feedback_value_type</span><span class="o">=</span><span class="n">Literal</span><span class="p">[</span><span class="sh">"</span><span class="s">grounded</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">ungrounded</span><span class="sh">"</span><span class="p">],</span>
<span class="p">)</span>
</code></pre></div></div>

<p>Automating these evaluations within CI/CD pipelines ensures that changes to system prompts, underlying foundation models, or tool schemas do not silently degrade agent performance in production.</p>

<hr />

<h2 id="pillar-3-expert-judge-alignment-and-continuous-learning-via-human-feedback-clhf">Pillar 3: Expert Judge Alignment and Continuous Learning via Human Feedback (CLHF)</h2>

<p>Automated LLM-as-a-Judge evaluators provide rapid feedback, but uncalibrated LLM judges often suffer from self-preference bias, verbosity bias, and an inability to detect subtle domain-specific errors. In financial analysis, an automated judge might rate an answer as correct because it sounds coherent, while completely missing an invented price, a contradicted income-statement row, or a news claim that never appeared in the tool payload.</p>

<p>The third pillar of EDD focuses on calibrating automated evaluators against human Subject Matter Experts (SMEs), such as senior financial analysts and auditors. By establishing a blind double-scoring protocol — where both the LLM judge and human experts evaluate identical agent runs — teams can measure agreement and systematically refine scoring rubrics until the automated judge closely mirrors expert human judgment.</p>

<h3 id="solving-mcp-tool-selection-accuracy-with-clhf">Solving MCP Tool Selection Accuracy with CLHF</h3>

<p>In enterprise agent deployments, incorrect tool selection is one of the most common failure modes. Agents operating over MCP tool registries frequently pick sub-optimal tools, hallucinate non-existent parameter names, or enter redundant tool-execution loops when presented with ambiguous user prompts.</p>

<p>Continuous Learning via Human Feedback (CLHF) resolves this by connecting production observability directly to expert calibration through a closed-loop engineering workflow. That loop can be implemented on MLflow traces with MemAlign:</p>

<ol>
  <li><strong>Trace Capture &amp; Span Filtering:</strong> Agent runs captured via autolog and stored in the SQLite/MLflow backend are reviewed in the UI. Engineers screen traces for anomalies — low evaluator confidence, failed tool calls, ungrounded figures, or empty reports.</li>
  <li><strong>SME Annotation:</strong> Flagged traces receive a <strong>HUMAN</strong> assessment using the <strong>same name</strong> as the judge being calibrated (<code class="language-plaintext highlighter-rouge">ToolCallCorrectness</code>, <code class="language-plaintext highlighter-rouge">ToolCallEfficiency</code>, or <code class="language-plaintext highlighter-rouge">Groundedness</code>), plus a short rationale. MemAlign uses both the label and the rationale.</li>
  <li><strong>Adaptive Feedback Loop:</strong> <code class="language-plaintext highlighter-rouge">MemAlignOptimizer</code> embeds those expert examples into an episodic memory attached to the judge. Alignment does not fine-tune Gemini weights. At eval time, the aligned judge retrieves nearby human-labeled cases as few-shot context. A second eval run is tagged <code class="language-plaintext highlighter-rouge">eval_phase=aligned</code>; the uncalibrated baseline is retained for comparison.</li>
</ol>

<p>This continuous feedback loop turns tool-selection and grounding failures into high-value training and evaluation data, systematically driving calling accuracy and faithfulness toward production thresholds.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">optimizer</span> <span class="o">=</span> <span class="nc">MemAlignOptimizer</span><span class="p">(</span>
    <span class="n">reflection_lm</span><span class="o">=</span><span class="sh">"</span><span class="s">gemini:/gemini-2.5-flash</span><span class="sh">"</span><span class="p">,</span>
    <span class="n">embedding_model</span><span class="o">=</span><span class="sh">"</span><span class="s">gemini:/gemini-embedding-001</span><span class="sh">"</span><span class="p">,</span>
<span class="p">)</span>
<span class="n">feedback_traces</span> <span class="o">=</span> <span class="nf">_traces_with_human_feedback</span><span class="p">(</span><span class="sh">"</span><span class="s">Groundedness</span><span class="sh">"</span><span class="p">)</span>
<span class="n">aligned_judge</span> <span class="o">=</span> <span class="n">base_judge</span><span class="p">.</span><span class="nf">align</span><span class="p">(</span><span class="n">traces</span><span class="o">=</span><span class="n">feedback_traces</span><span class="p">,</span> <span class="n">optimizer</span><span class="o">=</span><span class="n">optimizer</span><span class="p">)</span>
<span class="n">aligned_judge</span><span class="p">.</span><span class="nf">register</span><span class="p">(</span><span class="n">experiment_id</span><span class="o">=</span><span class="n">experiment_id</span><span class="p">)</span>
</code></pre></div></div>

<hr />

<h2 id="operationalizing-edd-from-demo-to-production">Operationalizing EDD: From Demo to Production</h2>

<p>Transitioning agentic AI from an experimental prototype to an enterprise-grade asset requires abandoning the belief that foundation models can be trusted out of the box. The <a href="https://github.com/caldeirav/mlflow-edd-financial-agent">caldeirav/mlflow-edd-financial-agent</a> repository is an example implementation of these three EDD pillars: persistent MLflow tracing, an OpenTelemetry-style view of MCP tool calls, automated evaluation suites, and expert-aligned CLHF. It shows how that combination becomes a working software engineering harness for a financial research assistant.</p>

<p>The operational loop is always the same: <strong>instrumented run → evaluate tool use and groundedness → CLHF in the UI → align → compare</strong>. Which judge you annotate depends on whether you are tuning <strong>tool selection</strong> or <strong>answer quality from tool results</strong> (or both). Further rounds add more HUMAN feedback; baselines remain for regression comparison.</p>

<p>By building on these three pillars of EDD, engineering organizations gain complete visibility into agent decision-making, ensure strict tool-calling accuracy across distributed protocols, and deliver trustworthy financial AI systems ready for production deployment.</p>

<hr />

<p><em>Demo repository: <a href="https://github.com/caldeirav/mlflow-edd-financial-agent">github.com/caldeirav/mlflow-edd-financial-agent</a>.</em></p>]]></content><author><name>Vincent Caldeira</name></author><category term="AAIF" /><category term="Engineering" /><summary type="html"><![CDATA[From vibe checks to Evaluation-Driven Development: MLflow tracing, MCP tool-call evaluation, and MemAlign CLHF for enterprise financial agents.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://thinkinginsidethebox.ai/assets/images/og/evaluation-driven-development-financial-agents.png" /><media:content medium="image" url="https://thinkinginsidethebox.ai/assets/images/og/evaluation-driven-development-financial-agents.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Spec-Driven Development: Managing Autonomous Agents at Scale</title><link href="https://thinkinginsidethebox.ai/aaif/engineering/2026/08/16/spec-driven-development-agents-md-at-scale.html" rel="alternate" type="text/html" title="Spec-Driven Development: Managing Autonomous Agents at Scale" /><published>2026-08-16T09:00:00+08:00</published><updated>2026-08-16T09:00:00+08:00</updated><id>https://thinkinginsidethebox.ai/aaif/engineering/2026/08/16/spec-driven-development-agents-md-at-scale</id><content type="html" xml:base="https://thinkinginsidethebox.ai/aaif/engineering/2026/08/16/spec-driven-development-agents-md-at-scale.html"><![CDATA[<p>The role of the software engineer is undergoing a profound transformation. As AI coding agents become increasingly capable of generating complex boilerplate, refactoring legacy systems, and even architecting new modules, the primary bottleneck in software development has shifted. It is no longer about writing code; it is about writing specifications. Enter <strong>spec-driven development</strong>—a paradigm where engineers orchestrate AI behavior through rigorous, structured documentation rather than ad-hoc, ephemeral prompting.</p>

<p>Recent empirical research, notably detailed in the paper <a href="https://arxiv.org/abs/2608.11095"><em>Why Does CLAUDE.md Keep Growing? Catastrophic Remembering in Agentic Coding</em></a> (Chakrabarti, 2026), highlights a critical operational challenge in this new era: managing the persistent system context and instructions provided to autonomous agents without falling victim to performance-degrading prompt bloat.</p>

<p>Earlier in this series we treated <a href="https://agents.md/"><strong>AGENTS.md</strong></a> as a lean front door — first as a <a href="/aaif/engineering/2026/07/10/spec-driven-development-enterprise-template.html">registry in a local enterprise template</a>, then as a <a href="/aaif/engineering/2026/07/11/spec-driven-development-lola-persona-modules.html">distributable index</a> for packaged personas and skills. Those articles assumed the file stays small. This one is about what happens when it does not: when persistent agent context becomes a dumping ground, and <strong>governance fails through accumulation rather than absence</strong>.</p>

<hr />

<h2 id="the-central-role-of-agentsmd-in-the-artifact-ecosystem">The Central Role of <code class="language-plaintext highlighter-rouge">AGENTS.md</code> in the Artifact Ecosystem</h2>

<p>In modern codebases, engineering teams increasingly rely on persistent context files, commonly centralized in an <code class="language-plaintext highlighter-rouge">AGENTS.md</code> file (or vendor-specific variants like <code class="language-plaintext highlighter-rouge">CLAUDE.md</code> and <code class="language-plaintext highlighter-rouge">copilot-instructions.md</code>). This file acts as the system prompt and central governing engine for AI assistants, dictating global coding conventions, architectural boundaries, security constraints, and operational guidelines.</p>

<p>However, <code class="language-plaintext highlighter-rouge">AGENTS.md</code> should never operate as a monolithic dumping ground for every detail of a project. In a mature spec-driven development workflow — the same spec → plan → tasks loop used by <a href="https://github.com/github/spec-kit">GitHub Spec-Kit</a> — it serves as an orchestrator that defines how agents interact with dedicated, specialized artifacts. Knowledge must be strictly compartmentalized across four foundational spec layers:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Artifact File</th>
      <th style="text-align: left">Core Responsibility</th>
      <th style="text-align: left">Primary Owner</th>
      <th style="text-align: left">Key Contents</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><strong><code class="language-plaintext highlighter-rouge">AGENTS.md</code></strong></td>
      <td style="text-align: left"><strong>Governance &amp; Routing</strong></td>
      <td style="text-align: left">Lead Architect / Human</td>
      <td style="text-align: left">Persona definitions, global constraints, directory layout rules, pointers to sub-specs.</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong><code class="language-plaintext highlighter-rouge">requirements.md</code></strong></td>
      <td style="text-align: left"><strong>The Objective (What)</strong></td>
      <td style="text-align: left">Product / Human</td>
      <td style="text-align: left">High-level business goals, user stories, functional boundaries, acceptance criteria.</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong><code class="language-plaintext highlighter-rouge">plan.md</code></strong></td>
      <td style="text-align: left"><strong>Architecture (How)</strong></td>
      <td style="text-align: left">Agent (Drafted) / Human (Approved)</td>
      <td style="text-align: left">System design, technical approach, dependency graph, risk mitigations.</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong><code class="language-plaintext highlighter-rouge">tasks.md</code></strong></td>
      <td style="text-align: left"><strong>Execution Tracking</strong></td>
      <td style="text-align: left">Agent (Iterative)</td>
      <td style="text-align: left">Stateful checklist of atomic, testable, and executable units of work.</td>
    </tr>
  </tbody>
</table>

<h3 id="the-operational-flow-of-spec-driven-execution">The Operational Flow of Spec-Driven Execution</h3>

<p>Rather than interacting with an AI through endless conversational turns, a spec-driven workflow follows a structured lifecycle. When a developer assigns a goal to an agent, the agent begins by parsing <code class="language-plaintext highlighter-rouge">AGENTS.md</code> to ingest its core governance constraints and routing directives. Guided by these rules, the agent loads <code class="language-plaintext highlighter-rouge">requirements.md</code> to understand the functional objective and business criteria.</p>

<p>Before generating any application code, the agent drafts a proposed implementation design inside <code class="language-plaintext highlighter-rouge">plan.md</code>. The human developer reviews, adjusts, and approves this architectural proposal. Once approved, the agent breaks the plan down into discrete work items inside <code class="language-plaintext highlighter-rouge">tasks.md</code>, executing each task sequentially, running automated verification tests, and updating state checkboxes until completion.</p>

<hr />

<h2 id="managing-multi-agent-architectures-in-agentsmd">Managing Multi-Agent Architectures in <code class="language-plaintext highlighter-rouge">AGENTS.md</code></h2>

<p>As AI integration matures, single generalist agents are rapidly being replaced by multi-agent architectures—specialized sub-agents collaborating within a single codebase (such as a <em>Planner Agent</em>, a <em>Coder Agent</em>, a <em>Reviewer Agent</em>, and a <em>Security Auditor Agent</em>). Managing these distinct personae efficiently requires <code class="language-plaintext highlighter-rouge">AGENTS.md</code> to act as a master orchestration framework.</p>

<h3 id="1-the-modular-persona-directory-agents-pattern">1. The Modular Persona Directory (<code class="language-plaintext highlighter-rouge">.agents/</code> Pattern)</h3>
<p>To prevent <code class="language-plaintext highlighter-rouge">AGENTS.md</code> from expanding into an unmaintainable document, teams should keep <code class="language-plaintext highlighter-rouge">AGENTS.md</code> focused on global rules while offloading persona-specific system prompts into a modular <code class="language-plaintext highlighter-rouge">.agents/</code> directory. Under this pattern:</p>
<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">AGENTS.md</code></strong> serves as the root entry point, global policy guide, and hand-off manager.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">.agents/planner.md</code></strong> defines specialized constraints for system design and dependency mapping.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">.agents/coder.md</code></strong> contains strict syntax, framing, and unit-testing guidelines for implementation.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">.agents/reviewer.md</code></strong> provides rules for static analysis, style enforcement, and security inspection.</li>
</ul>

<h3 id="2-explicit-hand-off-and-state-passing-protocols">2. Explicit Hand-off and State-Passing Protocols</h3>
<p>Multi-agent coordination requires strict contracts governing transitions between personae. <code class="language-plaintext highlighter-rouge">AGENTS.md</code> must explicitly define these hand-off protocols in plain text. For instance, the specification should mandate that when transitioning from the <em>Planner</em> persona to the <em>Coder</em> persona, the Planner must write its finalized architectural decisions into <code class="language-plaintext highlighter-rouge">plan.md</code>. The Coder persona is explicitly restricted from modifying application source files until <code class="language-plaintext highlighter-rouge">plan.md</code> contains a verified human approval marker.</p>

<h3 id="3-file-access-and-boundary-enforcement">3. File Access and Boundary Enforcement</h3>
<p>A primary cause of failure in multi-agent environments occurs when sub-agents modify overlapping files out of order or outside their designated domain. <code class="language-plaintext highlighter-rouge">AGENTS.md</code> must enforce file-level access permissions per persona:</p>
<ul>
  <li><strong>Architect Agent:</strong> Full read access across the codebase; write access restricted strictly to <code class="language-plaintext highlighter-rouge">plan.md</code> and <code class="language-plaintext highlighter-rouge">architecture.md</code>.</li>
  <li><strong>Coder Agent:</strong> Full read access; write access restricted to <code class="language-plaintext highlighter-rouge">src/</code> and <code class="language-plaintext highlighter-rouge">tests/</code>. Explicitly forbidden from altering root governance specs or product requirements.</li>
  <li><strong>Auditor Agent:</strong> Full read access; write access restricted to <code class="language-plaintext highlighter-rouge">reports/audit.md</code>.</li>
</ul>

<hr />

<h2 id="empirical-findings-the-threat-of-catastrophic-remembering">Empirical Findings: The Threat of “Catastrophic Remembering”</h2>

<p>While persistent context files are essential for guiding autonomous agents, recent research in <a href="https://arxiv.org/abs/2608.11095"><em>Why Does CLAUDE.md Keep Growing? Catastrophic Remembering in Agentic Coding</em></a> reveals a severe anti-pattern: <strong>unbounded context growth</strong>.</p>

<p>In continual machine learning, <em>catastrophic forgetting</em> describes a model overwriting prior critical knowledge. In agentic software engineering, developers face the inverse phenomenon: <strong>catastrophic remembering</strong>. Developers continuously append instructions to fix transient agent failures but rarely delete old rules, causing directives to persist long after their underlying justification has expired.</p>

<h3 id="the-context-bloat-lifecycle">The Context Bloat Lifecycle</h3>

<p>The cycle typically begins when an agent fails a specific task—for instance, failing to mock a legacy database connection during automated testing. To prevent repeated failures, a developer appends a quick patch rule to <code class="language-plaintext highlighter-rouge">AGENTS.md</code>: <em>“Always use the custom legacy mock adapter for unit tests.”</em></p>

<p>Months later, the engineering team upgrades the testing framework, rendering the legacy mock adapter obsolete. However, because the original developer did not record the reason behind the rule, subsequent maintainers cannot determine whether removing the directive will break hidden agent behaviors. Out of caution, the instruction remains in <code class="language-plaintext highlighter-rouge">AGENTS.md</code> indefinitely.</p>

<h3 id="key-research-insights-chakrabarti-2026">Key Research Insights (Chakrabarti, 2026):</h3>
<ul>
  <li><strong>Unbounded File Growth:</strong> Persistent agent context files expand by an average of +226% over their operational lifetime (+4.9 net instructions per commit) as teams iteratively patch failures.</li>
  <li><strong>Attention Dilution:</strong> As instruction counts rise, large language models suffer from degraded instruction-following precision, increased token costs, and higher latency due to prompt bloat.</li>
  <li><strong>Loss of Latent Reasoning:</strong> The root cause of prompt bloat is the absence of documented intent. Without explicit reasoning attached to a prompt rule, developers cannot assess its continued relevance.</li>
</ul>

<p>That cost is not only empirical in Chakrabarti’s measurements. On this blog, Christopher Nuland described the same structural trap in <a href="/engineering/2026/08/06/over-harnessing-context-file-cost.html">Over-harnessing: what your context files actually cost</a>: the harness is additive, attention is a budget, and nothing in the workflow tells you to stop adding.</p>

<hr />

<h2 id="best-practices-for-agentic-prompt-governance">Best Practices for Agentic Prompt Governance</h2>

<p>To maximize agent performance while preventing catastrophic remembering, engineering teams should adopt six core practices for managing <code class="language-plaintext highlighter-rouge">AGENTS.md</code> and associated spec files:</p>

<h3 id="1-enforce-hypothesis-driven-prompt-comments">1. Enforce Hypothesis-Driven Prompt Comments</h3>
<p>The primary mitigation identified by Chakrabarti (2026) is <strong>Latent Reasoning Documentation</strong>—treating system instructions with the same discipline as production code by commenting on their origin, hypothesis, and expiration conditions. In empirical evaluations, encoding latent reasoning reduced excess instruction accumulation by 99.3%.</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;!--
Hypothesis: Agent fails to resolve ES Module imports in Jest test suites.
Added: 2026-04-12 | Task Ref: #402 | Expiration Condition: Framework upgrade to Jest v30+
--&gt;
<span class="p">-</span> Pass --experimental-vm-modules when running test execution scripts.
</code></pre></div></div>

<h3 id="2-embrace-progressive-disclosure">2. Embrace Progressive Disclosure</h3>
<p>Keep the main <code class="language-plaintext highlighter-rouge">AGENTS.md</code> file concise—ideally under 100 lines. Structure the document so that detailed, domain-specific instructions are loaded dynamically via links (e.g., instructing the agent to read <code class="language-plaintext highlighter-rouge">docs/database-guidelines.md</code> only when editing files within the database directory). This is the same discipline as the lean registry in the <a href="/aaif/engineering/2026/07/11/spec-driven-development-lola-persona-modules.html">Lola distribution article</a>: <code class="language-plaintext highlighter-rouge">AGENTS.md</code> points; it does not contain.</p>

<h3 id="3-shift-from-chat-prompts-to-executable-spec-artifacts">3. Shift from Chat Prompts to Executable Spec Artifacts</h3>
<p>Eliminate long, interactive chat prompts. Use <code class="language-plaintext highlighter-rouge">AGENTS.md</code> to instruct agents to read task specifications from <code class="language-plaintext highlighter-rouge">tasks.md</code>, implement changes in small commits, execute automated test suites, and mark task status checkboxes autonomously.</p>

<h3 id="4-implement-agentic-linting-and-pruning-cycles">4. Implement Agentic “Linting” and Pruning Cycles</h3>
<p>Treat prompt bloat as technical debt. Establish regular pruning schedules where maintainers review <code class="language-plaintext highlighter-rouge">AGENTS.md</code> directives against the current state of the repository. Specialized “Refactoring Agents” can also be deployed to cross-reference <code class="language-plaintext highlighter-rouge">AGENTS.md</code> instructions against the active codebase to flag obsolete or redundant rules.</p>

<h3 id="5-utilize-negative-examples-anti-patterns-sparingly">5. Utilize Negative Examples (Anti-Patterns) Sparingly</h3>
<p>Language models learn effectively from concrete counter-examples. Combine abstract guidance with concise “DO / DO NOT” pairings to anchor expected behavior without lengthy explanations:</p>
<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">-</span> DO NOT use inline CSS or raw <span class="sb">`style`</span> attributes.
<span class="p">-</span> DO use Tailwind utility classes for element styling.
</code></pre></div></div>

<h3 id="6-establish-a-single-source-of-truth-for-multi-agent-consensus">6. Establish a Single Source of Truth for Multi-Agent Consensus</h3>
<p>In multi-agent systems, use <code class="language-plaintext highlighter-rouge">tasks.md</code> as the shared state register. Require all sub-agents to log their operational state (<code class="language-plaintext highlighter-rouge">[PENDING]</code>, <code class="language-plaintext highlighter-rouge">[IN_PROGRESS]</code>, <code class="language-plaintext highlighter-rouge">[BLOCKED]</code>, <code class="language-plaintext highlighter-rouge">[VERIFIED]</code>) directly in <code class="language-plaintext highlighter-rouge">tasks.md</code> to prevent race conditions and execution loops across personae.</p>

<hr />

<h2 id="conclusion">Conclusion</h2>

<p>The future of software development relies on curation, specification, and governance. By structuring <code class="language-plaintext highlighter-rouge">AGENTS.md</code> as a lean orchestrator, modularizing specialized agent personae, maintaining clear artifact boundaries, and documenting latent reasoning to prevent catastrophic remembering, software organizations can build scalable, high-throughput agent workflows that remain clean and efficient over time.</p>

<hr />

<h2 id="references--further-reading">References &amp; Further Reading</h2>

<ul>
  <li>Chakrabarti, K. (2026). <em>Why Does CLAUDE.md Keep Growing? Catastrophic Remembering in Agentic Coding</em>. arXiv:2608.11095.
    <ul>
      <li><strong>Abstract:</strong> <a href="https://arxiv.org/abs/2608.11095">https://arxiv.org/abs/2608.11095</a></li>
      <li><strong>PDF:</strong> <a href="https://arxiv.org/pdf/2608.11095">https://arxiv.org/pdf/2608.11095</a></li>
    </ul>
  </li>
</ul>

<hr />

<p><em>This is the third article in the spec-driven development series — from a local template, to enterprise distribution, to governing the context files that orchestrate autonomous agents at scale.</em></p>]]></content><author><name>Vincent Caldeira</name></author><category term="AAIF" /><category term="Engineering" /><summary type="html"><![CDATA[AGENTS.md should orchestrate specs, not hoard them. How spec-driven teams govern multi-agent context — and stop catastrophic remembering from bloating the harness.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://thinkinginsidethebox.ai/assets/images/og/spec-driven-development-agents-md-at-scale.png" /><media:content medium="image" url="https://thinkinginsidethebox.ai/assets/images/og/spec-driven-development-agents-md-at-scale.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Over-harnessing: what your context files actually cost</title><link href="https://thinkinginsidethebox.ai/engineering/2026/08/06/over-harnessing-context-file-cost.html" rel="alternate" type="text/html" title="Over-harnessing: what your context files actually cost" /><published>2026-08-06T09:00:00+08:00</published><updated>2026-08-06T09:00:00+08:00</updated><id>https://thinkinginsidethebox.ai/engineering/2026/08/06/over-harnessing-context-file-cost</id><content type="html" xml:base="https://thinkinginsidethebox.ai/engineering/2026/08/06/over-harnessing-context-file-cost.html"><![CDATA[<p>I once trained an AI agent to play Double Dragon, and it learned that the most efficient way to score points was to die and replay the level. Repeatedly, deliberately, right before the boss fight. I told the story on the Compiler podcast in an episode called <a href="https://www.redhat.com/en/compiler-podcast/agentic-ai">“Agentic AI: Working As Instructed”</a>, and the line I keep coming back to is the one I said then: an agent will do exactly what you tell it to do, which is frequently not what you meant.</p>

<p>That agent became good at the game only after I stopped tuning the reward and changed the objective, from maximizing points to exploring new experiences. Same game, same model, different framing; it soon started pulling off techniques speedrunners use.</p>

<p>I’m watching engineers make the identical mistake, except now the instructions are written in English, and there are four thousand tokens of them. The agent does something wrong, somebody adds a rule, the rule works on that case, and the system prompt grows by another paragraph. Nobody checks what the paragraph costs.</p>

<h2 id="the-cost-nobody-considers"><strong>The cost nobody considers</strong></h2>

<p>Rules are countable. Capability is not. So the rule count goes up forever, because adding one is a visible act of diligence and removing one looks like negligence.</p>

<p>The cost does not land where you are looking. It lands on the long sessions, the ambiguous inputs, and the cases that are not in your golden-path test suite, which is exactly the set of cases you built the agent for in the first place. You added the rule for the failure you saw, and you paid for it in the failures you never see.</p>

<p>Ralph Bean wrote this best in <a href="https://www.redhat.com/en/blog/what-even-harness-ai">“What even is the harness in AI?”</a>: “The sandbox and the harness have opposite design philosophies… The sandbox is subtractive… The harness is additive.” The sandbox takes capability away, on purpose, and there is a natural stopping condition, which is the point where the agent can no longer do its job. The harness (your AGENTS.md, your skills.md files, your tools, your system prompt, your tests) only ever adds, and additive systems have no stopping condition at all.</p>

<p>That’s the whole problem in one structural fact. Nothing in your workflow ever tells you to stop adding.</p>

<h2 id="attention-divides-the-harness-compresses"><strong>Attention divides, the harness compresses</strong></h2>

<p>Long context doesn’t make a model dumber by adding weight. It makes it dumber by dividing attention. Attention is a budget, a fixed pool of focus split across everything you show the model, so every token you add takes a share from the tokens that mattered.</p>

<p>Researchers built a <a href="https://arxiv.org/abs/2502.05167">hard version of the needle in a haystack test</a> where the literal word match is stripped out, so finding the answer takes a real leap of understanding instead of a string match. One of the strongest reasoning models available scored 99.9% on the short version, and 31.1% once the input reached 32,000 tokens. Same model, same question, nowhere near its advertised limit.</p>

<p>That result isn’t only about memory. Long input makes the right passage harder to find, and it degrades the reasoning that rides on top of that search; every model I’ve seen tested degrades as the input grows, well before the percentage reaches the max context size.</p>

<p>Then the harness starts throwing things away. When a session runs long enough to fill the window, the tooling summarizes and compresses the conversation and drops the rest, with a risk of not knowing which discarded detail you’ll need twenty minutes later.</p>

<p>Two clocks run at once. Attention keeps dividing, the harness keeps compressing, and every paragraph you add speeds up both.</p>

<h2 id="two-costs-you-are-already-paying"><strong>Two costs you are already paying</strong></h2>

<p>The most immediate and obvious penalty is a drop in accuracy, a vulnerability that is likely embedded in your system prompt right now. A <a href="https://arxiv.org/abs/2601.02023">study</a> evaluated standard anti-hallucination techniques, such as instructing the model to avoid fabrication and rely strictly on the provided text. When operating with a context window filled mostly with provided context (<a href="http://skills.md">skills.md</a>, RAG data, etc), the model’s proficiency in extracting precise answers directly from the source material plummeted from 90.3% down to 72.0%.</p>

<p>The authors have a name for what happens next: Faithfulness Masking. You told it not to be creative, it heard don’t risk it, and the safest output is no output, similar to my early Double Dragon results.</p>

<p>The second cost is the one with a dollar sign on it. Your context window isn’t written once, it’s re-read on every single request, and those tokens occupy memory for the whole conversation, which caps how many conversations one GPU can hold at once. Fewer conversations per GPU is a bigger bill, a slower service, or both. There are ways to reuse some of that work, and <a href="https://developers.redhat.com/articles/2025/10/07/master-kv-cache-aware-routing-llm-d-efficient-ai-inference">I’ve written about one of them</a>, but reuse doesn’t give the memory back completely and does nothing at all for quality.</p>

<p>Somebody finally measured the paragraphs. Researchers handed coding agents <a href="https://arxiv.org/abs/2602.11988">repository context files</a>, and the agents plainly used them, reaching for exactly the tools the files named. Obedience was never the question. Cost per task rose about 20%, and on whether any of it helped, nobody could measure a difference. You’re often paying a measured premium for an unmeasured benefit.</p>

<h2 id="where-heavy-constraint-is-correct"><strong>Where heavy constraint is correct</strong></h2>

<p>Locking down what an agent may touch is correct. Which files, which commands, which endpoints; that’s the sandbox, it’s subtractive, and it costs you nothing in capability because it takes away reach rather than reasoning. This is all done in the runtime environment of the agent, not defined in the context of your session.</p>

<p>While safety-critical and regulated operations often require this high-cost approach, those teams deliberately accept a documented trade-off for a specific advantage. The real issue is not constraint itself, but rather unpriced constraint, the administrative overhead that teams continuously deal with without evaluating the actual value.</p>

<h2 id="short-and-sweet-beats-a-chapter-book"><strong>Short and sweet beats a chapter book</strong></h2>

<p>Anthropic’s own <a href="https://code.claude.com/docs/en/costs">documentation</a> tells you to move instructions out of the file that loads at session start and into skills, because those instructions sit in context even when you’re doing unrelated work, and then it gives you a number: <strong><em>keep that file under 200 lines, essentials only</em></strong>. Two hundred lines, from the company that built the skills harness.</p>

<p>Skill defines a name and a description, one or two lines the agent carries all the time. Activation happens only once a task actually matches, and that’s when it reads the full <code class="language-plaintext highlighter-rouge">SKILL.md</code>. Everything else in the directory, the scripts, the reference files, the threshold table you nearly pasted into your prompt, sits on disk until something asks for it.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>---
name: incident-escalation
description: Escalation policy and severity thresholds for production incidents. Use when an alert must be classified or routed to on-call.
---

# Escalation policy

Classify severity, then route. Full threshold table in references/severity.md.
Do not page on-call for SEV3 or below during business hours.
</code></pre></div></div>

<p>Two lines are loaded into context, everything else loads when it’s relevant. The description is the load-bearing part, and it doesn’t scale by piling on more of them. <a href="https://arxiv.org/abs/2605.24050">One early study</a> stacked a library up to a couple hundred skills, and the agents got worse at picking the right one; that selection failure cost far more than the extra descriptions ever did. Fewer and sharper beats more.</p>

<p>At a certain point, a bloated harness simply transforms into a costly, GPU-based deterministic automation script. Therefore, try cutting your system prompt in half and rerunning your evaluations to observe what actually changes. What you see might surprise you.</p>]]></content><author><name>Christopher Nuland</name></author><category term="Engineering" /><summary type="html"><![CDATA[An agent will do exactly what you tell it to do — and every paragraph you add to the harness costs attention, accuracy, and GPU memory.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://thinkinginsidethebox.ai/assets/images/og/over-harnessing-context-file-cost.png" /><media:content medium="image" url="https://thinkinginsidethebox.ai/assets/images/og/over-harnessing-context-file-cost.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Non-Blocking Human-in-the-Loop Agents: Re-Engineering Agentic Runloops and State Machines with MRTR</title><link href="https://thinkinginsidethebox.ai/aaif/engineering/2026/08/02/non-blocking-hitl-agents-mrtr.html" rel="alternate" type="text/html" title="Non-Blocking Human-in-the-Loop Agents: Re-Engineering Agentic Runloops and State Machines with MRTR" /><published>2026-08-02T09:00:00+08:00</published><updated>2026-08-02T09:00:00+08:00</updated><id>https://thinkinginsidethebox.ai/aaif/engineering/2026/08/02/non-blocking-hitl-agents-mrtr</id><content type="html" xml:base="https://thinkinginsidethebox.ai/aaif/engineering/2026/08/02/non-blocking-hitl-agents-mrtr.html"><![CDATA[<p>If you have built an AI agent that can call tools — query a database, open a ticket, run a migration — you have already met the hard part of enterprise adoption: <strong>when the agent must stop and ask a human before it acts</strong>. Approving a destructive change is not optional in regulated environments. The awkward question is how you pause mid-tool without freezing your infrastructure around that pause.</p>

<p>The <a href="https://modelcontextprotocol.io">Model Context Protocol (MCP)</a> is the open standard many agent stacks now use to talk to tools. Think of it as a shared contract between an agent host and the services that expose capabilities (search, tickets, databases, and so on). Until recently, many production HITL stacks still leaned on a desktop-era <em>shape</em>: open a session, keep a long-lived connection, and push follow-up questions over that pipe. Streamable HTTP made server-to-client streams available; parking a paused tool frame in process memory was a common implementation choice on top of that — not a protocol mandate.</p>

<p>The <a href="https://blog.modelcontextprotocol.io/posts/2026-07-28/">2026-07-28 MCP specification</a> makes a cleaner alternative first-class. The protocol core becomes <strong>stateless</strong>: each request stands alone. For human-in-the-loop flows, the important new piece is <a href="https://blog.modelcontextprotocol.io/posts/2026-07-28/#multi-round-trip-requests-mrtr"><strong>Multi Round-Trip Requests (MRTR)</strong></a>. Instead of holding a network connection open while an operator thinks, the server finishes the HTTP response with “I need input,” hands the client a signed continuation token, and walks away. When the human answers, the client retries. Any healthy server replica can pick up where the first one left off.</p>

<p>This article is the first in a series on building production agents against MCP 2026-07-28. We will unpack why the connection-held pause failed at scale, how MRTR redesigns both the tool-side and agent-side state machines, and what a working DevOps demo actually proves — without turning this into a clone of the repository README. The reference code lives at <a href="https://github.com/caldeirav/mcp-mrtr-devops-demo"><strong>mcp-mrtr-devops-demo</strong></a>.</p>

<hr />

<h2 id="why-just-ask-the-human-used-to-break-the-network">Why “just ask the human” used to break the network</h2>

<p>Picture an autonomous DevOps agent asked to run an emergency migration on production cluster <code class="language-plaintext highlighter-rouge">prod-db-01</code>. The script is <code class="language-plaintext highlighter-rouge">V004__drop_legacy_users.sql</code>. Before any <code class="language-plaintext highlighter-rouge">DROP</code> executes, a human must confirm.</p>

<p>In many earlier HITL implementations on MCP Streamable HTTP, that confirmation was treated as a <strong>transport</strong> problem. The client opened a protocol session, kept a long-lived server-to-client stream (often Server-Sent Events, or SSE — a one-way HTTP channel the server can push messages on), and invoked the tool. Mid-call, the server pushed an elicitation — a structured “please fill this form” request — over the open stream. A common choice was to leave the tool’s execution frame paused in that process’s memory — so when the operator answered, the reply had to reach <strong>the same</strong> server instance for that frame to resume.</p>

<p>That design is fine on a laptop. It is fragile behind an enterprise load balancer. The gateway must “stick” the client to one pod (session affinity). Idle proxies time out quiet streams. Rolling deploys and autoscaling kill the pod that was holding the pause. Meanwhile the open connection sits in memory for as long as a human takes to read the dialog — seconds or minutes that your connection pools cannot usefully spend waiting.</p>

<p>In enterprise system architecture, human approval is a control-plane concern — a deliberate gate in the execution path — not something the network layer should absorb by holding sockets open. Coupling that gate to a long-lived connection turns a governance checkpoint into a load-balancer and capacity failure mode.</p>

<hr />

<h2 id="what-the-new-specification-changes">What the new specification changes</h2>

<p>MCP 2026-07-28 retires protocol-level sessions. There is no mandatory <code class="language-plaintext highlighter-rouge">initialize</code> handshake and no <code class="language-plaintext highlighter-rouge">Mcp-Session-Id</code> that pins you to one backend. Protocol version and client capabilities travel with each request in a <code class="language-plaintext highlighter-rouge">_meta</code> object — small structured metadata attached to the JSON-RPC body — and clients should also identify themselves with <code class="language-plaintext highlighter-rouge">clientInfo</code>. Authentication and caller identity still come from the authorization layer. If you want to learn a server’s capabilities up front, there is an optional <code class="language-plaintext highlighter-rouge">server/discover</code> call; you do not need it just to invoke a tool.</p>

<p>On the wire, Streamable HTTP also requires a few HTTP headers that name the method and tool (<code class="language-plaintext highlighter-rouge">Mcp-Method</code>, <code class="language-plaintext highlighter-rouge">Mcp-Name</code>, plus the protocol version). That sounds pedantic until you run traffic through a gateway such as <a href="https://github.com/agentgateway/agentgateway"><strong>agentgateway</strong></a>: once those headers are checked against the request body, the perimeter can route and apply coarse policy without digging through every JSON body. Fine-grained authorization still needs the authenticated caller, the operation, and the resource. That fits the perimeter layer we described in <a href="/aaif/security/2026/06/29/governed-run-loops-for-mcp.html">Governed Run Loops</a>.</p>

<p>MRTR is how interactivity works once sessions are gone. When a tool needs mid-call input, the server does not push over a held stream. It <strong>completes</strong> the current HTTP response with an interim result. Three fields matter:</p>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">resultType</code></strong> — tells the client whether this is a finished answer (<code class="language-plaintext highlighter-rouge">complete</code>) or a yield (<code class="language-plaintext highlighter-rouge">input_required</code>).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">inputRequests</code></strong> — describes what the client should collect, typically a form elicitation with a JSON Schema the UI can render.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">requestState</code></strong> — an opaque continuation handle minted by the server: a sealed snapshot of “where we were,” not a session cookie.</li>
</ul>

<p>The client gathers answers in its own UI or terminal, then <strong>re-issues the same tool call</strong> with <code class="language-plaintext highlighter-rouge">inputResponses</code> and the echoed <code class="language-plaintext highlighter-rouge">requestState</code>. A new JSON-RPC request id is used (normal JSON-RPC practice); continuity lives in those params, not in transport affinity. When the work is done, the server returns <code class="language-plaintext highlighter-rouge">resultType: "complete"</code>.</p>

<p>The protocol also constrains <em>when</em> a server may ask. A server may only solicit input while it is actively handling a client-initiated request. That chain of custody keeps agents from spontaneously popping dialogs at the user.</p>

<p>The mental model is the whole story:</p>

<blockquote>
  <p><strong>Common before:</strong> pause the thread, keep the socket.<br />
<strong>With MRTR:</strong> serialize the state, close the socket, resume on any instance.</p>
</blockquote>

<hr />

<h2 id="two-state-machines-deliberately-separated">Two state machines, deliberately separated</h2>

<p>Those earlier stacks often fused “agent waiting for a human” with “server holding a network connection.” MRTR makes the split explicit: continuation in <code class="language-plaintext highlighter-rouge">requestState</code>, human think-time off the wire.</p>

<h3 id="on-the-server-continue-do-not-suspend">On the server: continue, do not suspend</h3>

<p>A destructive tool no longer blocks on network I/O. It inspects the script, decides confirmation is required, mints a continuation, returns <code class="language-plaintext highlighter-rouge">input_required</code>, and ends the response. Later, on a fresh request, another process — possibly on another machine — verifies the continuation, checks the human’s answers, and either applies or denies.</p>

<p>How you store that continuation is an engineering choice. You can pack the step and bindings into a signed blob and send it entirely in <code class="language-plaintext highlighter-rouge">requestState</code> (no shared database; excellent for horizontal scale). Or you can park rich context in Redis or similar and put only a handle on the wire (smaller payloads; you now operate a cache with TTLs).</p>

<p>Either way, on the retry path you must treat <code class="language-plaintext highlighter-rouge">requestState</code> as <strong>attacker-controlled input</strong>. Sign it so clients cannot flip the cluster name or step flags. Bind the tool arguments (and ideally the authenticated user) into what you sign, and reject mismatches. Expire the token so approvals cannot be replayed forever. Fail closed: an invalid or stale continuation denies the action — it never “best efforts” into production.</p>

<h3 id="on-the-agent-a-non-blocking-harness">On the agent: a non-blocking harness</h3>

<p>The agent runloop — the loop that plans, calls tools, and feeds results back to the model — should branch on <code class="language-plaintext highlighter-rouge">resultType</code>, not on whether a streaming socket is still open. With LangGraph, a natural shape is: call the model, call the tool, and if the tool yields, interrupt for human input, then retry the tool with the answers and the continuation token before finishing.</p>

<p>The harness is responsible for turning an elicitation schema into a UI (or terminal prompts), packaging answers under the keys the server expects, echoing <code class="language-plaintext highlighter-rouge">requestState</code> exactly, and capping how many round trips a single tool call may take so a buggy server cannot loop the operator forever. Agent-side checkpointing (so your graph can resume after a page refresh) is useful — but it is not a substitute for server-side verification of <code class="language-plaintext highlighter-rouge">requestState</code>. One is UX continuity; the other is trust.</p>

<p>That separation is the difference between a demo that works once and a fleet that survives a pod recycle while someone is still reading the confirmation dialog.</p>

<hr />

<h2 id="what-the-demo-is-designed-to-show">What the demo is designed to show</h2>

<p>The open-source demo <a href="https://github.com/caldeirav/mcp-mrtr-devops-demo"><strong>caldeirav/mcp-mrtr-devops-demo</strong></a> is a deliberately small production <em>shape</em>, not a full platform. A LangGraph agent asks an MCP tool <code class="language-plaintext highlighter-rouge">apply_db_migration</code> to run a migration. Tool traffic always goes through <strong>agentgateway</strong> in <code class="language-plaintext highlighter-rouge">statefulMode: stateless</code> — meaning the proxy is not allowed to invent sticky MCP sessions. The MCP server is a FastAPI endpoint speaking the 2026-07-28 request shape. Continuations are HMAC-SHA256 signed with a five-minute lifetime. The gateway also exports OpenTelemetry traces (OTLP) so each short HTTP round trip can show up as inspectable spans in Jaeger — optional for the core HITL story, useful when you want the pause/resume pattern visible outside the terminal.</p>

<p>The story the demo walks is the enterprise one: the agent proposes a destructive script; the server yields; the operator confirms; the agent retries; the migration completes (simulated) or is denied — and at no point does anyone hold an SSE stream open waiting for the human.</p>

<h3 id="server-pattern-yield-then-verify-on-resume">Server pattern: yield, then verify on resume</h3>

<p>On the first call, if the script looks destructive and there is no continuation yet, the server builds an elicitation and mints <code class="language-plaintext highlighter-rouge">requestState</code>, then returns immediately:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">_build_input_required</span><span class="p">(</span><span class="n">cluster_id</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">script_name</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="n">Any</span><span class="p">]:</span>
    <span class="n">token</span> <span class="o">=</span> <span class="nf">mint_request_state</span><span class="p">(</span>
        <span class="n">secret</span><span class="o">=</span><span class="nf">_hmac_secret</span><span class="p">(),</span>
        <span class="n">cluster_id</span><span class="o">=</span><span class="n">cluster_id</span><span class="p">,</span>
        <span class="n">script_name</span><span class="o">=</span><span class="n">script_name</span><span class="p">,</span>
    <span class="p">)</span>
    <span class="n">elicit</span> <span class="o">=</span> <span class="nc">ElicitRequest</span><span class="p">(</span>
        <span class="n">params</span><span class="o">=</span><span class="nc">ElicitFormParams</span><span class="p">(</span>
            <span class="n">message</span><span class="o">=</span><span class="p">(</span>
                <span class="sa">f</span><span class="sh">"</span><span class="s">Confirm destructive migration on </span><span class="si">{</span><span class="n">cluster_id</span><span class="si">}</span><span class="s"> (</span><span class="si">{</span><span class="n">script_name</span><span class="si">}</span><span class="s">). </span><span class="sh">"</span>
                <span class="sa">f</span><span class="sh">"</span><span class="s">environment_tag must be one of: </span><span class="si">{</span><span class="sh">'</span><span class="s">, </span><span class="sh">'</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="n">ENVIRONMENT_TAGS</span><span class="p">)</span><span class="si">}</span><span class="sh">"</span>
            <span class="p">),</span>
            <span class="n">requestedSchema</span><span class="o">=</span><span class="nf">build_confirm_drop_schema</span><span class="p">(),</span>
        <span class="p">)</span>
    <span class="p">)</span>
    <span class="k">return</span> <span class="nc">InputRequiredResult</span><span class="p">(</span>
        <span class="n">inputRequests</span><span class="o">=</span><span class="p">{</span><span class="n">ELICITATION_KEY</span><span class="p">:</span> <span class="n">elicit</span><span class="p">},</span>
        <span class="n">requestState</span><span class="o">=</span><span class="n">token</span><span class="p">,</span>
    <span class="p">).</span><span class="nf">model_dump</span><span class="p">()</span>
</code></pre></div></div>

<p>The important design choice is what goes into the token. The demo signs <code class="language-plaintext highlighter-rouge">cluster_id</code>, <code class="language-plaintext highlighter-rouge">script_name</code>, and an issuance timestamp. On resume it verifies the HMAC, checks the TTL, and rejects tokens that do not match the arguments on the new request. Tampering, expiry, or a forged signature all fail closed before any “apply” path runs.</p>

<p>The yield the client sees looks like this — a finished HTTP 200 JSON-RPC result, not a parked connection:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"jsonrpc"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2.0"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
  </span><span class="nl">"result"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"resultType"</span><span class="p">:</span><span class="w"> </span><span class="s2">"input_required"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"inputRequests"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"confirm_drop_form"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"method"</span><span class="p">:</span><span class="w"> </span><span class="s2">"elicitation/create"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"params"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
          </span><span class="nl">"mode"</span><span class="p">:</span><span class="w"> </span><span class="s2">"form"</span><span class="p">,</span><span class="w">
          </span><span class="nl">"message"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Confirm destructive migration on prod-db-01 (V004__drop_legacy_users.sql). environment_tag must be one of: dev, staging, prod"</span><span class="p">,</span><span class="w">
          </span><span class="nl">"requestedSchema"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"object"</span><span class="p">,</span><span class="w">
            </span><span class="nl">"properties"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
              </span><span class="nl">"confirm_drop"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"boolean"</span><span class="p">,</span><span class="w">
                </span><span class="nl">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Confirm DROP/destructive ops"</span><span class="w">
              </span><span class="p">},</span><span class="w">
              </span><span class="nl">"environment_tag"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"string"</span><span class="p">,</span><span class="w">
                </span><span class="nl">"enum"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"dev"</span><span class="p">,</span><span class="w"> </span><span class="s2">"staging"</span><span class="p">,</span><span class="w"> </span><span class="s2">"prod"</span><span class="p">],</span><span class="w">
                </span><span class="nl">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Target environment tag"</span><span class="w">
              </span><span class="p">}</span><span class="w">
            </span><span class="p">},</span><span class="w">
            </span><span class="nl">"required"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"confirm_drop"</span><span class="p">,</span><span class="w"> </span><span class="s2">"environment_tag"</span><span class="p">]</span><span class="w">
          </span><span class="p">}</span><span class="w">
        </span><span class="p">}</span><span class="w">
      </span><span class="p">}</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"requestState"</span><span class="p">:</span><span class="w"> </span><span class="s2">"&lt;payload_b64&gt;.&lt;hmac_hex&gt;"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>The same yield through agentgateway looks like this in the MCP Tool Playground — <code class="language-plaintext highlighter-rouge">apply_db_migration</code> on <code class="language-plaintext highlighter-rouge">prod-db-01</code> with <code class="language-plaintext highlighter-rouge">V004__drop_legacy_users.sql</code>, returning HTTP 200 with <code class="language-plaintext highlighter-rouge">resultType: "input_required"</code>, the elicitation schema, and a <code class="language-plaintext highlighter-rouge">requestState</code> handle:</p>

<p><img src="/assets/images/og/mttr-agentgateway-mcp-playground.png" alt="agentgateway MCP Tool Playground showing apply_db_migration returning input_required with requestState" /></p>

<h3 id="client-pattern-branch-on-resulttype-interrupt-retry">Client pattern: branch on resultType, interrupt, retry</h3>

<p>The LangGraph graph is intentionally boring — and that is the point. After <code class="language-plaintext highlighter-rouge">call_tool</code>, routing looks only at the protocol discriminator:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">route_after_tool</span><span class="p">(</span><span class="n">state</span><span class="p">:</span> <span class="n">AgentState</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Literal</span><span class="p">[</span><span class="sh">"</span><span class="s">human_input</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">done</span><span class="sh">"</span><span class="p">]:</span>
    <span class="k">if</span> <span class="n">state</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">error</span><span class="sh">"</span><span class="p">):</span>
        <span class="k">return</span> <span class="sh">"</span><span class="s">done</span><span class="sh">"</span>
    <span class="n">result</span> <span class="o">=</span> <span class="n">state</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">last_result</span><span class="sh">"</span><span class="p">)</span> <span class="ow">or</span> <span class="p">{}</span>
    <span class="k">if</span> <span class="n">result</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">resultType</span><span class="sh">"</span><span class="p">)</span> <span class="o">==</span> <span class="sh">"</span><span class="s">input_required</span><span class="sh">"</span><span class="p">:</span>
        <span class="k">return</span> <span class="sh">"</span><span class="s">human_input</span><span class="sh">"</span>
    <span class="k">return</span> <span class="sh">"</span><span class="s">done</span><span class="sh">"</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">human_input</code> node uses LangGraph’s <code class="language-plaintext highlighter-rouge">interrupt()</code> so the graph parks without blocking the MCP server. When the operator answers, <code class="language-plaintext highlighter-rouge">retry_tool</code> issues a <strong>new</strong> HTTP POST through the gateway with the same tool arguments, plus <code class="language-plaintext highlighter-rouge">inputResponses</code> and the echoed <code class="language-plaintext highlighter-rouge">requestState</code>. Nothing on the server needs the original thread — only the shared signing secret and the arguments on the wire.</p>

<p>The demo runs a single MCP server, so what you see live is those two independent requests through a stateless gateway rather than a retry that lands on a different replica. Cross-replica resume is a property of the design: <code class="language-plaintext highlighter-rouge">requestState</code> is a self-contained HMAC-signed snapshot of the pause (not a pointer into one process’s memory), the gateway stays in <code class="language-plaintext highlighter-rouge">statefulMode: stateless</code> and never invents an <code class="language-plaintext highlighter-rouge">Mcp-Session-Id</code>, and any instance that shares the signing secret can verify the token, re-bind the tool arguments, and continue. Put several replicas behind the same round-robin edge and the same retry path works without a sticky conversation glued to one pod.</p>

<p>Because each retry is a fresh HTTP POST, distributed tracing fits the architecture naturally. With agentgateway’s OTLP export pointed at a local Jaeger all-in-one, tool round trips appear as ordinary spans — here a <code class="language-plaintext highlighter-rouge">POST</code> into the gateway nesting a <code class="language-plaintext highlighter-rouge">tools/call</code> on the <code class="language-plaintext highlighter-rouge">devops-migration</code> target — instead of one long-lived stream that spans the entire human think-time:</p>

<p><img src="/assets/images/og/mttr-jaeger-flamegraph.png" alt="Jaeger flamegraph for an agentgateway tools/call span on the devops-migration MCP target" /></p>

<p>For setup and run instructions, use the <a href="https://github.com/caldeirav/mcp-mrtr-devops-demo">repository README</a>. The harness’s banded console output (<code class="language-plaintext highlighter-rouge">TRACE</code>, <code class="language-plaintext highlighter-rouge">AGENT</code>, <code class="language-plaintext highlighter-rouge">HITL</code>, <code class="language-plaintext highlighter-rouge">SEP</code>) remains useful for watching protocol fields on a single happy path; Jaeger is the complementary view when you want the same round trips as spans.</p>

<hr />

<h2 id="design-takeaways-for-production">Design takeaways for production</h2>

<p>A few rules of thumb follow directly from this pattern:</p>

<ul>
  <li><strong>Keep HITL state out of the transport.</strong> The client or harness must preserve and echo requestState on retry so that a replica with access to the required verification material or shared state can resume. The model and human operator do not need direct access to the value.</li>
  <li><strong>Keep the gateway honest.</strong> If your proxy still pins MCP sessions, you have reintroduced sticky routing under a new name. The demo’s project rules forbid <code class="language-plaintext highlighter-rouge">Mcp-Session-Id</code> for that reason.</li>
  <li><strong>Treat the continuation like a capability.</strong> Protect integrity (HMAC or AEAD), bound time, bind arguments (and ideally the authenticated principal), and fail closed on every resume.</li>
  <li><strong>Separate agent checkpoints from MCP continuation.</strong> Graph memory helps the UX; server verification of <code class="language-plaintext highlighter-rouge">requestState</code> is what makes the action safe.</li>
  <li><strong>Cap client round trips.</strong> A misbehaving tool must not turn “ask once” into an infinite confirmation loop.</li>
  <li><strong>Route on headers at the edge.</strong> Prefer gateways that steer on validated <code class="language-plaintext highlighter-rouge">Mcp-Method</code> and <code class="language-plaintext highlighter-rouge">Mcp-Name</code> for routing and coarse policy, and reserve deep body inspection for audit enrichment rather than basic steering.</li>
</ul>

<p>MRTR does not replace long-running async work (that is moving into the Tasks extension), and it does not replace your governance stack. It replaces the <em>fragile pause</em> — the part that used to couple human think-time to socket lifetime.</p>

<hr />

<h2 id="series-roadmap">Series roadmap</h2>

<p>This post focused on non-blocking human-in-the-loop design: how agents ask for confirmation without sticky streams.</p>

<p>Coming articles in this MCP 2026-07-28 series will stay practical and architecture-led for developers shipping enterprise agents. We will look at how session-less MCP and mandatory routing headers reshape Layer-7 topologies and the MCP gateway pattern; take a wider pass at network design for the agentic era — what changes, what does not, and where the real opportunities sit; dig into richer JSON Schema patterns that make tool definitions fail less often at runtime; and explore dynamic discovery in enterprise tool marketplaces, including ontology-driven definitions and collaboration with communities such as Data in AI.</p>

<p>With MRTR, that mid-call pause becomes portable: signed, time-bounded, and free of open sockets — instead of being welded to a pod that might not exist when the human finally clicks <strong>Confirm</strong>.</p>

<hr />

<p><em>Demo repository: <a href="https://github.com/caldeirav/mcp-mrtr-devops-demo">github.com/caldeirav/mcp-mrtr-devops-demo</a>. Spec announcement: <a href="https://blog.modelcontextprotocol.io/posts/2026-07-28/">The 2026-07-28 Specification</a>.</em></p>]]></content><author><name>Vincent Caldeira</name></author><category term="AAIF" /><category term="Engineering" /><summary type="html"><![CDATA[How MCP 2026-07-28 Multi Round-Trip Requests let enterprise agents pause for human approval without long-lived server connections — with a LangGraph and agentgateway design walkthrough.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://thinkinginsidethebox.ai/assets/images/og/non-blocking-hitl-agents-mrtr.png" /><media:content medium="image" url="https://thinkinginsidethebox.ai/assets/images/og/non-blocking-hitl-agents-mrtr.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Spec-Driven Development: Scaling Agentic Governance with Lola</title><link href="https://thinkinginsidethebox.ai/aaif/engineering/2026/07/11/spec-driven-development-lola-persona-modules.html" rel="alternate" type="text/html" title="Spec-Driven Development: Scaling Agentic Governance with Lola" /><published>2026-07-11T09:00:00+08:00</published><updated>2026-07-11T09:00:00+08:00</updated><id>https://thinkinginsidethebox.ai/aaif/engineering/2026/07/11/spec-driven-development-lola-persona-modules</id><content type="html" xml:base="https://thinkinginsidethebox.ai/aaif/engineering/2026/07/11/spec-driven-development-lola-persona-modules.html"><![CDATA[<p>Your platform team ships the template. A developer clones it, runs the bootstrap commands, and for the first time an AI agent writes LangGraph nodes inside a constitution, under a CodeGuard skill, through a phased Spec-Kit loop. Governance works — on that machine, in that repository, on that afternoon.</p>

<p>That was the story of our <a href="/2026/07/10/spec-driven-development-enterprise-template.html">first article</a>: proving the <strong>Secure Spec-Driven Framework (SSDF)</strong> locally — <a href="https://agents.md/"><strong>AGENTS.md</strong></a> as universal context, Spec-Kit as the process engine, CodeGuard as the proactive shield, Foundry as the reactive evaluator. Four pillars, one repository, one happy path.</p>

<p>Now imagine the next Monday. Security publishes an updated OWASP LLM rule pack. Architecture mandates a new dependency policy in the constitution. And your VP asks you to roll the same governed experience to five hundred engineers — half of whom use Cursor, a third live in Claude Code, and the remainder experiment with Copilot and Gemini CLI.</p>

<p>Establishing the template was day one. <strong>Day two is distribution, maintenance, and enterprise governance.</strong> And day two is where most agentic programs quietly fail.</p>

<p>On this blog, we still call that discipline <strong>thinking inside the box</strong>. The box must simply be shippable — all four walls of it.</p>

<hr />

<h2 id="when-the-template-stops-traveling">When the Template Stops Traveling</h2>

<p>The failure mode is rarely the framework itself. It is what happens when a working scaffold leaves the hands of its authors.</p>

<p><strong>Tool fragmentation</strong> fractures the rollout first. Your @Architect persona lives in <code class="language-plaintext highlighter-rouge">.agents/personas/architect.md</code>, CodeGuard rules sit in <code class="language-plaintext highlighter-rouge">.agents/skills/codeguard/</code>, Spec-Kit templates in <code class="language-plaintext highlighter-rouge">.specify/templates/</code> — but Cursor, Claude Code, and Copilot each expect different directory layouts. A platform engineer who manually copied context into one IDE format has not solved the problem; they have created three divergent forks before lunch.</p>

<p><strong>Context drift</strong> follows. Security updates the CodeGuard rule packs; three teams patch their repos; two forget. Architecture revs the enterprise constitution; only the platform team’s golden template reflects the change. Within a quarter, “SSDF-compliant” means different things in different business units. Auditors ask which version of the guardrails produced a given pull request. Nobody can answer confidently.</p>

<p><strong>The monolithic prompt trap</strong> is the cruelest failure — and it begins by misunderstanding what AGENTS.md is for. Panicked by drift, organizations paste <em>everything</em> into the repository root file — every persona body, every security rule, every constitutional clause — until AGENTS.md becomes a small novel. The agent’s context window fills before it reads your actual task. Developers conclude that “governed agents are slow” and quietly return to vibe coding.</p>

<p>Article 1 established the right pattern across all four pillars. Day two is about <strong>packaging each pillar independently</strong> while keeping AGENTS.md lean as the shared front door.</p>

<p>This is where <a href="https://github.com/LobsterTrap/lola"><strong>Lola</strong></a> enters the story — not as an AGENTS.md accessory, but as the <strong>distribution layer for the entire SSDF stack</strong>.</p>

<hr />

<h2 id="the-package-manager-for-the-whole-framework">The Package Manager for the Whole Framework</h2>

<p>If an agent’s skills, prompts, and configurations are an RPM package, <a href="https://lobstertrap.org/lola/"><strong>Lola</strong></a> is the DNF for them — an AI context package manager from Red Hat Product Security (<a href="https://developers.redhat.com/articles/2026/04/08/manage-ai-context-lola-package-manager">overview</a>).</p>

<p>Platform teams write each SSDF pillar once, version it, and declare it in a single <code class="language-plaintext highlighter-rouge">.lola-req</code> manifest — the same mental model as <code class="language-plaintext highlighter-rouge">requirements.txt</code> for Python (<a href="https://lobstertrap.org/lola/guides/declarative/">declarative guide</a>). Running <code class="language-plaintext highlighter-rouge">lola sync</code> installs every module to the assistants detected on a developer’s machine. Running <code class="language-plaintext highlighter-rouge">lola sync</code> in CI ensures the pipeline evaluates against <strong>the same context the developer had locally</strong>.</p>

<p>Install the CLI once with uv:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>uv tool <span class="nb">install </span>lola-ai
</code></pre></div></div>

<p>From there, the four pillars Article 1 assembled by hand become four packages in one catalog.</p>

<hr />

<h2 id="four-pillars-four-packages-one-manifest">Four Pillars, Four Packages, One Manifest</h2>

<p>Article 1 built the SSDF locally with curl, mkdir, and hand-authored markdown. Lola redistributes each pillar without collapsing them into a single blob:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>┌─────────────────────────────────────────────────────────────┐
│  repo/AGENTS.md  (Lean index — AAIF standard, always first) │
│  Stack · uv commands · boundaries · .lola-req pointer       │
└──────────────────────────────┬──────────────────────────────┘
                               │  lola sync
                               ▼
┌─────────────────────────────────────────────────────────────┐
│  .lola-req  — declarative manifest (version-pinned modules) │
└───┬──────────────┬─────────────────┬──────────────────┬─────┘
    ▼              ▼                 ▼                  ▼
 Personas      CodeGuard         Spec-Kit           Foundry
 (agents/)      (skills/)         (templates/skills) (CI agents/)
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th style="text-align: left">SSDF pillar (Article 1)</th>
      <th style="text-align: left">What Lola packages</th>
      <th style="text-align: left">What stays in the repo</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><strong>AGENTS.md registry</strong></td>
      <td style="text-align: left">Lean repo file + module persona registries</td>
      <td style="text-align: left">Project stack, commands, boundaries</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>CodeGuard (proactive shield)</strong></td>
      <td style="text-align: left">Security skills and OWASP rule packs</td>
      <td style="text-align: left">Nothing — replace curl with a module</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Spec-Kit (process engine)</strong></td>
      <td style="text-align: left">Enterprise constitution baselines and spec templates</td>
      <td style="text-align: left">Project-specific constitution <em>extensions</em>, local <code class="language-plaintext highlighter-rouge">.specify/</code> state</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Foundry (reactive evaluator)</strong></td>
      <td style="text-align: left">@Reviewer agent + Detector context for CI</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">.github/workflows/foundry-eval.yml</code> workflow wiring</td>
    </tr>
  </tbody>
</table>

<p>Repository <strong>AGENTS.md</strong> remains the AAIF entry point — the first file every assistant reads. It does not grow to contain all four pillars. It points at what <code class="language-plaintext highlighter-rouge">.lola-req</code> installs.</p>

<hr />

<h3 id="1-personas-and-the-agentsmd-chain">1. Personas and the AGENTS.md chain</h3>

<p>Personas are the most visible Lola use case — and the one that clarifies the two-layer AGENTS.md pattern.</p>

<ul>
  <li><strong>Repository AGENTS.md</strong> — stack, uv commands, boundaries, and a pointer to installed modules. Never paste @Architect/@Engineer/@Reviewer bodies here.</li>
  <li><strong>Module AGENTS.md</strong> (inside <code class="language-plaintext highlighter-rouge">enterprise-personas</code>) — phase registry mapping Spec-Kit commands to agent files in <code class="language-plaintext highlighter-rouge">agents/</code>.</li>
</ul>

<p>When an assistant initializes, it reads repo AGENTS.md, follows the Lola pointer to module AGENTS.md, and loads <code class="language-plaintext highlighter-rouge">@Architect</code> during <code class="language-plaintext highlighter-rouge">/speckit.plan</code>, <code class="language-plaintext highlighter-rouge">@Engineer</code> during <code class="language-plaintext highlighter-rouge">/speckit.implement</code>, <code class="language-plaintext highlighter-rouge">@Reviewer</code> at pull request time. Session-scoped, phase-appropriate, lean.</p>

<hr />

<h3 id="2-distributing-the-proactive-shield-cosai-codeguard">2. Distributing the proactive shield (CoSAI CodeGuard)</h3>

<p>Article 1 pulled CodeGuard YAML rules with raw curl into <code class="language-plaintext highlighter-rouge">.agents/skills/codeguard/</code>. That works once; it does not survive a security patch cycle.</p>

<p>CodeGuard is already structured as agent skills upstream (<code class="language-plaintext highlighter-rouge">skills/codeguard/</code> in the <a href="https://github.com/cosai-oasis/project-codeguard">CoSAI Project CodeGuard</a> repository). Lola can consume it directly — no manual curl, no forked rule files drifting out of date.</p>

<p>Declare it in <code class="language-plaintext highlighter-rouge">.lola-req</code>:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Upstream skill pack (subdirectory fragment)
https://github.com/cosai-oasis/project-codeguard.git#subdirectory=skills/codeguard

# Or a version-pinned enterprise mirror in your marketplace
codeguard-rules&gt;=2.0.1
</code></pre></div></div>

<p>When security publishes an updated OWASP LLM Top 10 pack, the platform team bumps the module version. Every developer — and every CI job — picks up the same rules on the next <code class="language-plaintext highlighter-rouge">lola sync</code>. The @Engineer persona from pillar 1 activates the CodeGuard skill during <code class="language-plaintext highlighter-rouge">/speckit.implement</code>; the rules themselves arrive as a managed package, not a forgotten curl from onboarding week.</p>

<hr />

<h3 id="3-standardizing-spec-driven-workflows-github-spec-kit">3. Standardizing spec-driven workflows (GitHub Spec-Kit)</h3>

<p>Spec-Kit owns the phase commands — <code class="language-plaintext highlighter-rouge">/speckit.specify</code>, <code class="language-plaintext highlighter-rouge">/speckit.clarify</code>, <code class="language-plaintext highlighter-rouge">/speckit.plan</code>, <code class="language-plaintext highlighter-rouge">/speckit.implement</code>. Lola does not replace those. It <strong>distributes the enterprise templates</strong> that make every Spec-Kit invocation consistent.</p>

<p>Article 1 scaffolded <code class="language-plaintext highlighter-rouge">.specify/constitution.md</code> and empty <code class="language-plaintext highlighter-rouge">templates/</code> by hand. At scale, the <strong>enterprise baseline constitution</strong> — LangGraph mandates, uv-only dependency policy, OGX model routing — belongs in a Lola module as reference templates and compliance skills. Project teams <strong>extend</strong> that baseline with product-specific clauses in their local <code class="language-plaintext highlighter-rouge">.specify/constitution.md</code>; they do not copy-paste a stale corporate markdown file from a wiki.</p>

<p>A platform module might ship:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssdf-spec-kit/module/
├── skills/
│   └── constitution-compliance/
│       ├── SKILL.md
│       └── reference/
│           ├── constitution-baseline.md
│           └── templates/
│               ├── spec-template.md
│               ├── plan-template.md
│               └── tasks-template.md
└── commands/
    └── align-constitution.md   # Helper — NOT a replacement for /speckit.*
</code></pre></div></div>

<p>When architecture changes — say, a new HITL requirement for destructive LangGraph nodes — the platform team updates the module. Developers run <code class="language-plaintext highlighter-rouge">lola update</code> instead of manually diffing markdown across hundreds of repositories. Spec-Kit still drives the phase loop; Lola keeps the <strong>templates and constitutional baselines</strong> current.</p>

<hr />

<h3 id="4-cicd-environment-parity-cisco-foundry">4. CI/CD environment parity (Cisco Foundry)</h3>

<p>Foundry’s reactive evaluation only matters if the CI agent sees the <strong>same context</strong> the developer had when writing code. If the @Reviewer persona and CodeGuard rules exist locally but not in the pipeline, you are evaluating a different agent than the one that implemented the feature.</p>

<p>The fix is straightforward: provision the CI environment with the same manifest as the developer laptop.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># .github/workflows/foundry-eval.yml (excerpt)</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Install Lola</span>
  <span class="na">run</span><span class="pi">:</span> <span class="s">uv tool install lola-ai</span>

<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Replicate agentic context</span>
  <span class="na">run</span><span class="pi">:</span> <span class="s">lola sync</span>

<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Run Foundry evaluation</span>
  <span class="na">run</span><span class="pi">:</span> <span class="c1"># Foundry Detector + Exploratory against PR diff</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">lola sync</code> reads the repository’s <code class="language-plaintext highlighter-rouge">.lola-req</code> and installs enterprise-personas, codeguard-rules, and ssdf-spec-kit into the CI runner’s assistant context. The Foundry <strong>Detector</strong> sweeps against the same CodeGuard rules the Engineer applied during implementation. The <strong>Exploratory</strong> agent hunts from the same @Reviewer persona definition — not a generic security prompt someone wrote for CI in a hurry.</p>

<p>Development context and evaluation context converge. The box in CI is the same box on the desk.</p>

<hr />

<h2 id="from-fifteen-lines-of-curl-to-one-manifest">From Fifteen Lines of Curl to One Manifest</h2>

<p>Compare Article 1’s bootstrap with a Lola-managed enterprise template:</p>

<p><strong>The old way</strong> — every pillar hand-wired:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir </span>python-ssdf-agent-template <span class="o">&amp;&amp;</span> <span class="nb">cd </span>python-ssdf-agent-template
git init <span class="o">&amp;&amp;</span> uv init <span class="nt">--app</span>
uv add langchain langgraph ogx-ai pydantic python-dotenv
<span class="nb">mkdir</span> <span class="nt">-p</span> .specify/templates .specify/scripts <span class="o">&amp;&amp;</span> <span class="nb">touch</span> .specify/constitution.md

<span class="nb">mkdir</span> <span class="nt">-p</span> .agents/skills/codeguard
curl <span class="nt">-sL</span> https://raw.githubusercontent.com/cosai-oasis/project-codeguard/main/rules/owasp-llm-top-10.yml <span class="se">\</span>
  <span class="nt">-o</span> .agents/skills/codeguard/owasp-llm-top-10.yml
curl <span class="nt">-sL</span> https://raw.githubusercontent.com/cosai-oasis/project-codeguard/main/rules/python-secure-defaults.yml <span class="se">\</span>
  <span class="nt">-o</span> .agents/skills/codeguard/python-secure-defaults.yml

<span class="nb">mkdir</span> <span class="nt">-p</span> .github/workflows
curl <span class="nt">-sL</span> https://raw.githubusercontent.com/CiscoDevNet/foundry-security-spec/main/examples/github-actions/foundry-eval-reference.yml <span class="se">\</span>
  <span class="nt">-o</span> .github/workflows/foundry-eval.yml

<span class="nb">touch </span>AGENTS.md
<span class="nb">mkdir</span> <span class="nt">-p</span> .agents/personas
<span class="nb">touch</span> .agents/personas/architect.md .agents/personas/engineer.md .agents/personas/reviewer.md
</code></pre></div></div>

<p><strong>The Lola way</strong> — Python and Spec-Kit scaffold stay; governed context becomes declarative:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir </span>python-ssdf-agent-template <span class="o">&amp;&amp;</span> <span class="nb">cd </span>python-ssdf-agent-template
git init <span class="o">&amp;&amp;</span> uv init <span class="nt">--app</span>
uv add langchain langgraph ogx-ai pydantic python-dotenv
<span class="nb">mkdir</span> <span class="nt">-p</span> .specify <span class="o">&amp;&amp;</span> <span class="nb">touch</span> .specify/constitution.md

<span class="nb">cat</span> <span class="o">&lt;&lt;</span><span class="sh">'</span><span class="no">EOF</span><span class="sh">' &gt; .lola-req
# SSDF context — all four pillars, version-pinned
enterprise-personas&gt;=1.2.0
codeguard-rules&gt;=2.0.1
ssdf-spec-kit&gt;=1.0.0
https://github.com/cosai-oasis/project-codeguard.git#subdirectory=skills/codeguard
</span><span class="no">EOF

</span>lola market add enterprise-core https://internal.corp/ai/enterprise-market.yml
lola <span class="nb">sync</span>
</code></pre></div></div>

<p>Foundry workflow YAML still lives in <code class="language-plaintext highlighter-rouge">.github/workflows/</code> — that is CI infrastructure. Everything the agents <em>know</em> arrives through the manifest.</p>

<p>After sync, repository AGENTS.md stays lean:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gh"># Enterprise AI Agent Instructions</span>

<span class="gu">## Stack &amp; Commands</span>
<span class="p">-</span> Install packages: <span class="sb">`uv add &lt;package&gt;`</span> (NEVER use <span class="sb">`pip`</span>)
<span class="p">-</span> Agent framework: LangChain, LangGraph, OGX

<span class="gu">## Boundaries</span>
<span class="p">-</span> Extend <span class="sb">`.specify/constitution.md`</span> from the ssdf-spec-kit baseline — do not replace it.
<span class="p">-</span> Load personas per Spec-Kit phase via installed Lola modules.

<span class="gu">## Context Modules</span>
Managed by <span class="sb">`.lola-req`</span> — run <span class="sb">`lola sync`</span> after clone or pull.
</code></pre></div></div>

<hr />

<h2 id="managing-enterprise-templating">Managing Enterprise Templating</h2>

<p>Rolling the SSDF to hundreds of teams requires a clear ownership model — not just modules, but <strong>who may change what, and how changes propagate</strong>.</p>

<h3 id="the-golden-template-repository">The golden template repository</h3>

<p>Maintain one canonical <code class="language-plaintext highlighter-rouge">python-ssdf-agent-template</code> your internal developer portal scaffolds from. It checks in:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">pyproject.toml</code> with the approved Python stack</li>
  <li>A thin <code class="language-plaintext highlighter-rouge">.specify/constitution.md</code> that <strong>extends</strong> the enterprise baseline (product-specific clauses only)</li>
  <li><code class="language-plaintext highlighter-rouge">.lola-req</code> pinning all four pillar modules at tested versions</li>
  <li>Lean <code class="language-plaintext highlighter-rouge">AGENTS.md</code> pointing at Lola-managed context</li>
  <li><code class="language-plaintext highlighter-rouge">.github/workflows/foundry-eval.yml</code> running <code class="language-plaintext highlighter-rouge">lola sync</code> before evaluation</li>
</ul>

<p>New projects inherit governance by cloning the golden template — not by copying a Confluence page.</p>

<h3 id="the-marketplace-as-internal-supply-chain">The marketplace as internal supply chain</h3>

<p>Host <code class="language-plaintext highlighter-rouge">enterprise-market.yml</code> on GitHub Enterprise. Platform engineering owns module versions; security owns <code class="language-plaintext highlighter-rouge">codeguard-rules</code> bumps; architecture owns <code class="language-plaintext highlighter-rouge">ssdf-spec-kit</code> constitution baselines; developer experience owns <code class="language-plaintext highlighter-rouge">enterprise-personas</code>.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">name</span><span class="pi">:</span> <span class="s">Enterprise SSDF Marketplace</span>
<span class="na">version</span><span class="pi">:</span> <span class="s">1.0.0</span>
<span class="na">modules</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">enterprise-personas</span>
    <span class="na">version</span><span class="pi">:</span> <span class="s">1.2.0</span>
    <span class="na">repository</span><span class="pi">:</span> <span class="s">https://github.com/your-org/lola-ssdf-personas.git</span>
    <span class="na">tags</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">personas</span><span class="pi">,</span> <span class="nv">agents-md</span><span class="pi">]</span>

  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">codeguard-rules</span>
    <span class="na">version</span><span class="pi">:</span> <span class="s">2.0.1</span>
    <span class="na">repository</span><span class="pi">:</span> <span class="s">https://github.com/your-org/lola-codeguard.git</span>
    <span class="na">tags</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">security</span><span class="pi">,</span> <span class="nv">codeguard</span><span class="pi">]</span>

  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">ssdf-spec-kit</span>
    <span class="na">version</span><span class="pi">:</span> <span class="s">1.0.0</span>
    <span class="na">repository</span><span class="pi">:</span> <span class="s">https://github.com/your-org/lola-ssdf-spec-kit.git</span>
    <span class="na">tags</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">spec-kit</span><span class="pi">,</span> <span class="nv">constitution</span><span class="pi">,</span> <span class="nv">templates</span><span class="pi">]</span>
</code></pre></div></div>

<p>Teams do not edit marketplace modules in place. They propose changes through the owning team’s repository; the marketplace version increments; <code class="language-plaintext highlighter-rouge">.lola-req</code> constraints pull the update on next sync.</p>

<h3 id="baseline-versus-override">Baseline versus override</h3>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Asset</th>
      <th style="text-align: left">Enterprise module (Lola)</th>
      <th style="text-align: left">Project repository</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left">Constitution</td>
      <td style="text-align: left">Baseline mandates (LangGraph, uv, OGX)</td>
      <td style="text-align: left">Product-specific extensions</td>
    </tr>
    <tr>
      <td style="text-align: left">Spec/plan templates</td>
      <td style="text-align: left">Corporate format and required sections</td>
      <td style="text-align: left">Project examples in <code class="language-plaintext highlighter-rouge">.specify/templates/</code></td>
    </tr>
    <tr>
      <td style="text-align: left">Personas</td>
      <td style="text-align: left">@Architect, @Engineer, @Reviewer</td>
      <td style="text-align: left">None — always from module</td>
    </tr>
    <tr>
      <td style="text-align: left">CodeGuard rules</td>
      <td style="text-align: left">Current OWASP LLM packs</td>
      <td style="text-align: left">None — always from module</td>
    </tr>
    <tr>
      <td style="text-align: left">Foundry reviewer context</td>
      <td style="text-align: left">@Reviewer agent definition</td>
      <td style="text-align: left">Workflow YAML only</td>
    </tr>
  </tbody>
</table>

<p>This split prevents “fork the constitution because our team is special” while still allowing product teams to specify domain constraints in local <code class="language-plaintext highlighter-rouge">.specify/</code>.</p>

<h3 id="scopes-and-the-hallway-test">Scopes and the hallway test</h3>

<p><strong>Project scope</strong> (default) installs modules into the repository — personas and templates travel with the clone. <strong>User scope</strong> (<code class="language-plaintext highlighter-rouge">--scope user</code>) installs globally — useful for baseline security skills that must follow a developer across every project.</p>

<p>Two developers, Cursor and Claude Code, run <code class="language-plaintext highlighter-rouge">lola sync</code> against the same <code class="language-plaintext highlighter-rouge">.lola-req</code>. Both receive all four pillars. Neither reformats anything. That is <strong>mandating an outcome</strong>, not an IDE.</p>

<hr />

<h2 id="a-day-in-the-life--all-four-pillars-one-sync">A Day in the Life — All Four Pillars, One Sync</h2>

<p>Return to the HTML metadata extractor from Article 1. The narrative is unchanged. The plumbing spans every pillar.</p>

<p>A developer clones the golden template and runs <code class="language-plaintext highlighter-rouge">lola sync</code>. <strong>Repository AGENTS.md</strong> loads on init — stack, boundaries, pointer to installed modules. <strong>Module AGENTS.md</strong> routes to <code class="language-plaintext highlighter-rouge">@Architect</code> for <code class="language-plaintext highlighter-rouge">/speckit.specify</code> and <code class="language-plaintext highlighter-rouge">/speckit.plan</code>. The <strong>ssdf-spec-kit</strong> skill ensures the plan follows the enterprise constitution baseline. The Architect documents <code class="language-plaintext highlighter-rouge">beautifulsoup4</code> in a Dependencies table — plan before add.</p>

<p>On <code class="language-plaintext highlighter-rouge">/speckit.implement</code>, <code class="language-plaintext highlighter-rouge">@Engineer</code> loads and the <strong>CodeGuard</strong> skill activates — the same OWASP rules security patched last Tuesday, not a stale curl from onboarding. The Engineer runs <code class="language-plaintext highlighter-rouge">uv add beautifulsoup4</code>, implements the node, loops on <code class="language-plaintext highlighter-rouge">uv run pytest -v</code>.</p>

<p>The pull request triggers Foundry CI. The workflow runs <code class="language-plaintext highlighter-rouge">lola sync</code> first — <strong>@Reviewer</strong> context matches the developer’s environment. Detector sweeps against the same CodeGuard rules. Exploratory hunts from the same persona definition.</p>

<p>Nothing about <strong>spec → plan → implement → evaluate</strong> changed. Everything about <strong>consistency across pillars, machines, and the pipeline</strong> did.</p>

<hr />

<h2 id="removing-the-friction-that-kills-adoption">Removing the Friction That Kills Adoption</h2>

<table>
  <thead>
    <tr>
      <th style="text-align: left"> </th>
      <th style="text-align: left">Without Lola</th>
      <th style="text-align: left">With Lola</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><strong>IDE choice</strong></td>
      <td style="text-align: left">Force one tool for consistency</td>
      <td style="text-align: left">Engineers choose; all four pillars translate</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Security patches</strong></td>
      <td style="text-align: left">Stale CodeGuard rules on local machines</td>
      <td style="text-align: left">Central bump; <code class="language-plaintext highlighter-rouge">lola sync</code> everywhere including CI</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Constitution updates</strong></td>
      <td style="text-align: left">Manual copy into each repo</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">ssdf-spec-kit</code> module bump; <code class="language-plaintext highlighter-rouge">lola update</code></td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>CI parity</strong></td>
      <td style="text-align: left">Generic CI prompts ≠ dev context</td>
      <td style="text-align: left">Same <code class="language-plaintext highlighter-rouge">.lola-req</code> in pipeline and on laptop</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Context size</strong></td>
      <td style="text-align: left">Monolithic AGENTS.md</td>
      <td style="text-align: left">Lean registry; four modules on demand</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="conclusion">Conclusion</h2>

<p>Article 1 proved the SSDF could work locally — four pillars, one template, one governed loop. This article proves it can <strong>survive the enterprise</strong> — when each pillar is a versioned package, declared in one manifest, synced on every clone and every CI run.</p>

<p>Lola is not an AGENTS.md tool. It is the distribution fabric for the whole framework: personas through module registries, CodeGuard through skill packs, Spec-Kit consistency through template modules, Foundry through CI context parity. AGENTS.md stays the AAIF front door — thin, permanent, readable. Everything else ships behind <code class="language-plaintext highlighter-rouge">.lola-req</code>.</p>

<p>That is how governed agentic development scales from a template that worked on day one into infrastructure that still works on day five hundred — same box, same rules, every desk, every pipeline.</p>

<hr />

<p><em>This is the second article in the spec-driven development series. Having established local workflow and enterprise distribution, the series next examines how phase gates keep agents from skipping specification checkpoints — and how automated evaluation closes the loop before merge.</em></p>]]></content><author><name>Vincent Caldeira</name></author><category term="AAIF" /><category term="Engineering" /><summary type="html"><![CDATA[Day two of enterprise agentic engineering — distributing all four SSDF pillars (AGENTS.md personas, CodeGuard, Spec-Kit templates, Foundry CI context) through Lola modules and a single .lola-req manifest.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://thinkinginsidethebox.ai/assets/images/og/spec-driven-development-lola-persona-modules.png" /><media:content medium="image" url="https://thinkinginsidethebox.ai/assets/images/og/spec-driven-development-lola-persona-modules.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Spec-Driven Development: An Enterprise Base Template for Agentic AI Engineering</title><link href="https://thinkinginsidethebox.ai/aaif/engineering/2026/07/10/spec-driven-development-enterprise-template.html" rel="alternate" type="text/html" title="Spec-Driven Development: An Enterprise Base Template for Agentic AI Engineering" /><published>2026-07-10T09:00:00+08:00</published><updated>2026-07-10T09:00:00+08:00</updated><id>https://thinkinginsidethebox.ai/aaif/engineering/2026/07/10/spec-driven-development-enterprise-template</id><content type="html" xml:base="https://thinkinginsidethebox.ai/aaif/engineering/2026/07/10/spec-driven-development-enterprise-template.html"><![CDATA[<p>As AI coding agents transition from unstructured generation — often called “vibe coding” — to structured software engineering, organizations need tighter control over context, security, and architecture. Prompting harder is not enough. Enterprises need a repeatable scaffold that grounds agents in the right stack, forces explicit specification checkpoints, and evaluates output before it merges.</p>

<p>This article outlines a <strong>Secure Spec-Driven Framework (SSDF)</strong> for building a unified Python base template tailored to agentic development. The stack combines LangChain, LangGraph, <a href="https://github.com/ogx-ai/ogx"><strong>OGX</strong></a>, and uv — governed by four open-source pillars that together create a predictable, auditable environment for AI-assisted engineering.</p>

<p>On this blog, that discipline is <strong>thinking inside the box</strong>: agents read explicit context, follow phased specs, and execute inside boundaries that are inspectable from day one.</p>

<hr />

<h2 id="the-four-pillars-of-the-architecture">The Four Pillars of the Architecture</h2>

<p>To understand how this template works, it helps to look at the four foundational components that drive it — and why each is necessary in an enterprise environment.</p>

<h3 id="1-the-universal-context-agentsmd">1. The Universal Context: AGENTS.md</h3>

<p>Think of <a href="https://agents.md/"><strong>AGENTS.md</strong></a> as the “README for agents.” Maintained by the Agentic AI Foundation, it is a standardized file that AI assistants — Claude Code, GitHub Copilot, Cursor — read immediately upon initialization.</p>

<p>Without this file, assistants are forced to guess project standards from scattered context, which often leads to hallucinations or outdated practices — like defaulting to <code class="language-plaintext highlighter-rouge">pip</code> instead of <code class="language-plaintext highlighter-rouge">uv</code>. By explicitly defining the environment, tech stack, and acting as a registry for specialized agent personas, AGENTS.md grounds the LLM in the enterprise’s reality and prevents unwanted behaviors right from the start.</p>

<h3 id="2-the-process-engine-github-spec-kit">2. The Process Engine: GitHub Spec-Kit</h3>

<p><a href="https://github.com/github/spec-kit"><strong>GitHub Spec-Kit</strong></a> shifts the traditional development model. Instead of humans writing code guided by specs, humans write the specs — and the AI agent generates the implementation.</p>

<p>The toolkit enforces a phased approach, moving from specification to planning, and finally to implementation. By separating stable business logic from flexible implementation details, Spec-Kit forces the AI through explicit checkpoints. Developers shift from merely writing code to steering and verifying architecture — preventing the agent from generating technical debt and ensuring that rules — like mandating LangGraph for state management — are baked in from day one.</p>

<h3 id="3-the-proactive-shield-cosai-project-codeguard">3. The Proactive Shield: CoSAI Project CodeGuard</h3>

<p>Coding agents are optimized for speed, which means they can frequently introduce vulnerabilities — insecure output handling, prompt injection flaws — if left unchecked.</p>

<p><a href="https://github.com/cosai-oasis/project-codeguard"><strong>CoSAI Project CodeGuard</strong></a> acts as a proactive security shield. It is an AI model-agnostic security framework that provides a corpus of secure-by-default rules designed specifically for AI workflows. By embedding these best practices directly into the generation phase as active skills, CodeGuard can proactively flag issues. For example, if an AI attempts to write a LangChain Tool that blindly passes unsanitized LLM output into a database query, CodeGuard will catch and prevent it during the drafting phase.</p>

<h3 id="4-the-reactive-evaluator-cisco-foundry-security-spec">4. The Reactive Evaluator: Cisco Foundry Security Spec</h3>

<p>While static security rules are essential, they are not always enough to catch novel LLM attack vectors.</p>

<p>The <a href="https://github.com/CiscoDevNet/foundry-security-spec"><strong>Cisco Foundry Security Spec</strong></a> provides a safety net by defining an open specification for building agentic AI security evaluation pipelines post-implementation. It uses a <strong>Detector</strong> role to sweep code with static rules like CodeGuard, alongside an <strong>Exploratory</strong> role that creatively hunts for hidden logic flaws. If the Exploratory agent finds a vulnerability that static rules missed, Foundry records that gap — which is then transformed into a new CodeGuard rule, creating a continuous feedback loop that catches the vulnerability statically on all future sweeps.</p>

<hr />

<h2 id="the-methodology-secure-spec-driven-framework-ssdf">The Methodology: Secure Spec-Driven Framework (SSDF)</h2>

<p>The SSDF methodology creates a smooth, synergistic workflow: <strong>context → spec → code → evaluation.</strong></p>

<ol>
  <li><strong>The Context</strong> — The agent reads AGENTS.md, recognizes its registered personas (Architect, Engineer, Security Reviewer), and confirms that uv, LangChain, and OGX are the required stack.</li>
  <li><strong>The Engine</strong> — The developer uses Spec-Kit commands. The agent consults <code class="language-plaintext highlighter-rouge">.specify/constitution.md</code> to guarantee the plan uses LangGraph for state management and OGX for model connections.</li>
  <li><strong>The Guardrails</strong> — During implementation, CodeGuard skills constrain the generated code, preventing common LLM tool vulnerabilities before the code is even committed.</li>
  <li><strong>The Evaluation</strong> — Once the developer opens a Pull Request, the Cisco Foundry evaluation pipeline automatically executes. An isolated CI job provisions a Foundry <strong>Detector Agent</strong> to scan the PR diff against CodeGuard rules, followed by a Foundry <strong>Exploratory Agent</strong> that simulates attacks against the new code — for example, attempting prompt injection against newly created LangChain tools.</li>
</ol>

<hr />

<h2 id="building-the-template-from-scratch">Building the Template from Scratch</h2>

<p>To build the <code class="language-plaintext highlighter-rouge">python-ssdf-agent-template</code> from scratch, a platform engineer should execute the following terminal commands. This uses the recommended quickstart initialization patterns for uv, Spec-Kit, CodeGuard, and Foundry.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 1. Initialize the Base Python Environment with uv</span>
<span class="nb">mkdir </span>python-ssdf-agent-template
<span class="nb">cd </span>python-ssdf-agent-template
git init
uv init <span class="nt">--app</span>
uv add langchain langgraph ogx-ai pydantic python-dotenv

<span class="c"># 2. Initialize GitHub Spec-Kit</span>
<span class="c"># Scaffold the required directory structure for spec-driven development</span>
<span class="nb">mkdir</span> <span class="nt">-p</span> .specify/templates .specify/scripts
<span class="nb">touch</span> .specify/constitution.md

<span class="c"># 3. Initialize CoSAI Project CodeGuard</span>
<span class="c"># Create the local skills directory and pull down the standard LLM rule packs</span>
<span class="nb">mkdir</span> <span class="nt">-p</span> .agents/skills/codeguard
curl <span class="nt">-sL</span> https://raw.githubusercontent.com/cosai-oasis/project-codeguard/main/rules/owasp-llm-top-10.yml <span class="se">\</span>
  <span class="nt">-o</span> .agents/skills/codeguard/owasp-llm-top-10.yml
curl <span class="nt">-sL</span> https://raw.githubusercontent.com/cosai-oasis/project-codeguard/main/rules/python-secure-defaults.yml <span class="se">\</span>
  <span class="nt">-o</span> .agents/skills/codeguard/python-secure-defaults.yml

<span class="c"># 4. Initialize Cisco Foundry Security Spec</span>
<span class="c"># Create the GitHub Actions directory and deploy the reference Foundry evaluation workflow</span>
<span class="nb">mkdir</span> <span class="nt">-p</span> .github/workflows
curl <span class="nt">-sL</span> https://raw.githubusercontent.com/CiscoDevNet/foundry-security-spec/main/examples/github-actions/foundry-eval-reference.yml <span class="se">\</span>
  <span class="nt">-o</span> .github/workflows/foundry-eval.yml

<span class="c"># 5. Create the Universal Context and Persona Files</span>
<span class="c"># Note: Populate these files with the configurations in the sections below.</span>
<span class="nb">touch </span>AGENTS.md
<span class="nb">mkdir</span> <span class="nt">-p</span> .agents/personas
<span class="nb">touch</span> .agents/personas/architect.md .agents/personas/engineer.md .agents/personas/reviewer.md
</code></pre></div></div>

<hr />

<h2 id="repository-structure">Repository Structure</h2>

<p>After running the initialization commands above, your resulting template structure will look like this:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python-ssdf-agent-template/
├── AGENTS.md                   # Universal context registry (Stack, Commands, Boundaries)
├── pyproject.toml              # Project metadata and uv dependency definitions
├── .specify/                   # GitHub Spec-Kit configuration
│   ├── constitution.md         # Enterprise architectural/security non-negotiables
│   ├── templates/              # Custom enterprise templates for specs, plans, tasks
│   └── scripts/                # Spec-Kit helper scripts
├── .agents/
│   ├── personas/               # Session-scoped persona definitions (loaded on demand)
│   └── skills/                 # Local extensions (CodeGuard configs, custom LangChain tools)
├── .github/workflows/
│   └── foundry-eval.yml        # CI pipeline running the Cisco Foundry spec evaluation
├── src/                        # Main Python source (LangGraph nodes, edges, state)
└── README.md
</code></pre></div></div>

<hr />

<h2 id="file-configurations">File Configurations</h2>

<h3 id="defining-the-context-agentsmd">Defining the Context (AGENTS.md)</h3>

<p>Industry best practices dictate that AGENTS.md should not be bloated with every persona definition. Instead, it must serve as a highly structured registry that defines the exact commands, the tech stack, a three-tier boundary system, and a directory of available, session-scoped personas.</p>

<p>Populate <code class="language-plaintext highlighter-rouge">AGENTS.md</code> with:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gh"># Enterprise AI Agent Instructions</span>

You are an enterprise AI engineering agent. Your goal is to write secure, robust Python AI agents using Spec-Driven Development (SDD).

<span class="gu">## The HOW: Executable Commands (Run Exactly as Written)</span>
<span class="p">-</span> <span class="gs">**Install packages**</span>: <span class="sb">`uv add &lt;package&gt;`</span> (NEVER use <span class="sb">`pip`</span> or <span class="sb">`poetry`</span>)
<span class="p">-</span> <span class="gs">**Run a script**</span>: <span class="sb">`uv run python &lt;script.py&gt;`</span>
<span class="p">-</span> <span class="gs">**Run tests**</span>: <span class="sb">`uv run pytest -v`</span>
<span class="p">-</span> <span class="gs">**Format code**</span>: <span class="sb">`uv run ruff format .`</span>

<span class="gu">## The WHAT: Stack &amp; Architecture</span>
<span class="p">-</span> <span class="gs">**Language**</span>: Python 3.12+
<span class="p">-</span> <span class="gs">**Agent Framework**</span>: LangChain and LangGraph (StateGraphs for all workflows).
<span class="p">-</span> <span class="gs">**Model Connections**</span>: <span class="p">[</span><span class="nv">OGX</span><span class="p">](</span><span class="sx">https://github.com/ogx-ai/ogx</span><span class="p">)</span> <span class="p">(</span><span class="sx">`ogx-ai`</span><span class="p">)</span>. Never hardcode direct provider SDKs (e.g., <span class="sb">`anthropic`</span>).

<span class="gu">## The Boundaries</span>
<span class="p">-</span> <span class="gs">**✅ Always do**</span>: Use <span class="sb">`pydantic.BaseModel`</span> for LangChain Tool schemas. Cross-reference <span class="sb">`.specify/constitution.md`</span> before writing code.
<span class="p">-</span> <span class="gs">**⚠️ Ask first**</span>: Before adding a new external API dependency.
<span class="p">-</span> <span class="gs">**🚫 Never do**</span>: Use standalone <span class="sb">`while True`</span> loops for agent execution. Hardcode API keys or secrets.

<span class="gu">## Persona Registry</span>
Do not adopt all personas at once. Load the specific persona from <span class="sb">`.agents/personas/`</span> based on the current command or phase:

| Persona | Invoke Via | Definition File | Role Focus |
| :--- | :--- | :--- | :--- |
| <span class="gs">**@Architect**</span> | <span class="sb">`/speckit.plan`</span> | <span class="sb">`.agents/personas/architect.md`</span> | System design, LangGraph state schemas, and Spec-Kit planning. |
| <span class="gs">**@Engineer**</span> | <span class="sb">`/speckit.implement`</span> | <span class="sb">`.agents/personas/engineer.md`</span> | Execution only. Strict adherence to the spec/plan, writing tests, applying CodeGuard rules. Never re-architect. |
| <span class="gs">**@Reviewer**</span> | GitHub PR | <span class="sb">`.agents/personas/reviewer.md`</span> | Post-implementation Foundry evaluation, hunting for CodeGuard violations. |
</code></pre></div></div>

<h3 id="establishing-the-constitution-specifyconstitutionmd">Establishing the Constitution (<code class="language-plaintext highlighter-rouge">.specify/constitution.md</code>)</h3>

<p>The Spec-Kit constitution serves as the immutable architectural law. It defines strict rules to prevent the LLM from hallucinating older or simpler frameworks.</p>

<p>Populate <code class="language-plaintext highlighter-rouge">.specify/constitution.md</code> with:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gh"># Enterprise AI Development Constitution</span>

<span class="gu">## 1. Architectural Standards (Python &amp; AI)</span>
<span class="p">-</span> <span class="gs">**State Management**</span>: All multi-step agent logic MUST be implemented as a <span class="sb">`StateGraph`</span> using <span class="sb">`langgraph`</span>.
<span class="p">-</span> <span class="gs">**Model Agnosticism**</span>: All language model clients MUST be instantiated via <span class="p">[</span><span class="nv">OGX</span><span class="p">](</span><span class="sx">https://github.com/ogx-ai/ogx</span><span class="p">)</span> pointing to our internal OpenAI-compatible gateway.
<span class="p">-</span> <span class="gs">**Typing**</span>: Strict Python type hinting is required.

<span class="gu">## 2. Security (Aligned with CodeGuard for LLMs)</span>
<span class="p">-</span> <span class="gs">**Least Privilege Tools**</span>: AI tools (functions) must only have permissions to execute their specific narrow task.
<span class="p">-</span> <span class="gs">**Human-in-the-Loop (HITL)**</span>: Any LangGraph node that performs a destructive action (e.g., DROP TABLE) must include an interrupt for human approval before execution.

<span class="gu">## 3. Dependency Management</span>
<span class="p">-</span> <span class="gs">**uv ONLY**</span>: The <span class="sb">`pyproject.toml`</span> managed by <span class="sb">`uv`</span> is the single source of truth. Do not generate <span class="sb">`requirements.txt`</span>.
<span class="p">-</span> <span class="gs">**Plan before add**</span>: New packages must appear in the <span class="sb">`plan.md`</span> Dependencies section before <span class="sb">`uv add`</span> is run during <span class="sb">`/speckit.implement`</span>.

<span class="gu">## 4. Testing</span>
<span class="p">-</span> Agent nodes must be tested in isolation by mocking the LLM response.
<span class="p">-</span> Tests must pass before the <span class="sb">`/speckit.implement`</span> phase completes.
</code></pre></div></div>

<h3 id="session-scoped-personas-agentspersonas">Session-Scoped Personas (<code class="language-plaintext highlighter-rouge">.agents/personas/</code>)</h3>

<p>These files define the exact boundaries for the agent depending on which phase of the SDD loop it is executing. Later in the series, the same content is repackaged for portable, cross-repo distribution at enterprise scale.</p>

<p><strong><code class="language-plaintext highlighter-rouge">.agents/personas/architect.md</code></strong></p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gh"># @Architect Persona</span>

<span class="gs">**Primary Directive**</span>: You are the System Architect. Your job is to design the system, not build it. You must NOT write executable Python implementation code in <span class="sb">`src/`</span>.

<span class="gs">**Focus Areas**</span>:
<span class="p">1.</span> Run <span class="sb">`/speckit.clarify`</span> to resolve ambiguities about requirements.
<span class="p">2.</span> Design LangGraph <span class="sb">`StateGraph`</span> schemas ensuring robust state transitions.
<span class="p">3.</span> Define strict <span class="sb">`pydantic.BaseModel`</span> interfaces for all proposed LangChain Tools.
<span class="p">4.</span> Output your design strictly to <span class="sb">`plan.md`</span> and <span class="sb">`tasks.md`</span> in the Spec-Kit format.
<span class="p">5.</span> Document any new third-party dependencies in a <span class="gs">**Dependencies**</span> section of <span class="sb">`plan.md`</span> — with package name, rationale, and the phase where they will be added. Do not run <span class="sb">`uv add`</span> yourself.

<span class="gs">**Constraints**</span>: Ensure all designs strictly adhere to <span class="sb">`.specify/constitution.md`</span>.
</code></pre></div></div>

<p><strong><code class="language-plaintext highlighter-rouge">.agents/personas/engineer.md</code></strong></p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gh"># @Engineer Persona</span>

<span class="gs">**Primary Directive**</span>: You are the Execution Engineer. Your job is to purely execute the <span class="sb">`plan.md`</span> created by the Architect. Do NOT attempt to redesign the architecture or add unauthorized features.

<span class="gs">**Focus Areas**</span>:
<span class="p">1.</span> Read the <span class="gs">**Dependencies**</span> section of <span class="sb">`plan.md`</span> and run <span class="sb">`uv add &lt;package&gt;`</span> for each authorized package before writing code that imports it.
<span class="p">2.</span> Write the Python code in the <span class="sb">`src/`</span> directory.
<span class="p">3.</span> Write robust <span class="sb">`pytest`</span> unit tests for every node and tool you build.
<span class="p">4.</span> Run <span class="sb">`uv run pytest`</span> autonomously and fix failing tests before concluding implementation.

<span class="gs">**Security Guardrails**</span>: You must enforce all rules found in <span class="sb">`.agents/skills/codeguard/`</span>. Ensure all LLM inputs/outputs to tools are heavily sanitized and strongly typed.
</code></pre></div></div>

<p><strong><code class="language-plaintext highlighter-rouge">.agents/personas/reviewer.md</code></strong></p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gh"># @Reviewer Persona (Cisco Foundry)</span>

<span class="gs">**Primary Directive**</span>: You are the CI/CD Security Evaluator acting under the Cisco Foundry Security Spec framework. Your job is to break the code, not write it.

<span class="gs">**Focus Areas**</span>:
<span class="p">1.</span> <span class="gs">**Detector Role**</span>: Scan the Pull Request diff against the static rules defined in <span class="sb">`.agents/skills/codeguard/`</span>. Flag any violations (e.g., direct instantiation of non-OGX models, missing HITL on destructive actions).
<span class="p">2.</span> <span class="gs">**Exploratory Role**</span>: Creatively hunt for LLM-specific logic flaws that static rules miss. Can the LangGraph state be manipulated? Are the Pydantic schemas vulnerable to Prompt Injection bypasses?
<span class="p">3.</span> <span class="gs">**Output**</span>: Generate a structured Markdown security report as a GitHub PR comment. Cite the exact file paths and lines of code that pose a risk.
</code></pre></div></div>

<hr />

<h2 id="the-developer-workflow">The Developer Workflow</h2>

<p>A day in the life of a developer using this template — including how a new dependency enters the project only when the spec demands it.</p>

<h3 id="1-onboarding">1. Onboarding</h3>

<p>A developer clones <code class="language-plaintext highlighter-rouge">python-ssdf-agent-template</code>. The IDE agent reads AGENTS.md, registers that uv is required, and learns about the persona registry and CodeGuard boundaries.</p>

<h3 id="2-specification">2. Specification</h3>

<p>The developer types:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/speckit.specify Build an agent node that extracts structured metadata from public HTML pages.
</code></pre></div></div>

<p>The base template already ships with LangChain, LangGraph, OGX, and Pydantic — enough for orchestration and typed tool schemas. It does <strong>not</strong> include an HTML parser. That gap only becomes visible once the feature is specified.</p>

<h3 id="3-clarification">3. Clarification</h3>

<p>The agent adopts the @Architect persona (loading <code class="language-plaintext highlighter-rouge">.agents/personas/architect.md</code>) and runs <code class="language-plaintext highlighter-rouge">/speckit.clarify</code>. Because the spec mentions HTML, the Architect asks targeted questions before any code or dependencies are touched:</p>

<ul>
  <li><em>Should the node accept raw HTML strings or fetch URLs itself?</em></li>
  <li><em>What fields belong in the output Pydantic schema — title, author, published date?</em></li>
  <li><em>Are there rate limits or allowlisted domains for outbound fetches?</em></li>
</ul>

<p>The answers resolve ambiguity without prematurely adding packages to <code class="language-plaintext highlighter-rouge">pyproject.toml</code>.</p>

<h3 id="4-planning">4. Planning</h3>

<p>The Architect generates <code class="language-plaintext highlighter-rouge">plan.md</code> and <code class="language-plaintext highlighter-rouge">tasks.md</code> in Spec-Kit format, strictly following <code class="language-plaintext highlighter-rouge">.specify/constitution.md</code>. LangGraph handles the workflow; OGX handles model calls. For HTML parsing, the Architect adds an explicit <strong>Dependencies</strong> section to the plan — not a shell command yet, but a documented requirement with rationale:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gu">## Dependencies (new — not in base template)</span>

| Package | Reason | Added in phase |
| :--- | :--- | :--- |
| <span class="sb">`beautifulsoup4`</span> | Parse messy real-world HTML into structured fields for the metadata tool | <span class="sb">`/speckit.implement`</span> |
</code></pre></div></div>

<p>This is the critical SDD checkpoint: the <strong>why</strong> is captured in the plan before the <strong>how</strong> runs on the terminal. The Architect does not run <code class="language-plaintext highlighter-rouge">uv add</code> — that belongs to the Engineer persona during implementation, keeping dependency changes auditable and tied to an approved spec.</p>

<h3 id="5-implementation">5. Implementation</h3>

<p>The developer runs <code class="language-plaintext highlighter-rouge">/speckit.implement</code>. The agent transitions to the @Engineer persona (loading <code class="language-plaintext highlighter-rouge">.agents/personas/engineer.md</code>) and executes the plan in order:</p>

<ol>
  <li><strong>Add declared dependencies first</strong> — the Engineer reads the Dependencies table and installs only what the plan authorizes:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>uv add beautifulsoup4
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">uv</code> updates <code class="language-plaintext highlighter-rouge">pyproject.toml</code> and the lockfile in one step. AGENTS.md forbids <code class="language-plaintext highlighter-rouge">pip</code> or hand-edited <code class="language-plaintext highlighter-rouge">requirements.txt</code>, so this is the only supported path.</p>

<ol>
  <li><strong>Implement the node</strong> — write the LangGraph node and Pydantic tool schemas in <code class="language-plaintext highlighter-rouge">src/</code>.</li>
  <li><strong>Apply CodeGuard guardrails</strong> — sanitize LLM inputs and outputs at tool boundaries.</li>
  <li><strong>Verify</strong> — write <code class="language-plaintext highlighter-rouge">pytest</code> coverage for the node (mocking LLM responses per the constitution) and run:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>uv run pytest <span class="nt">-v</span>
</code></pre></div></div>

<p>The Engineer fixes failures before marking implementation complete. If implementation reveals a <em>different</em> library is needed than the one in the plan, the correct action is to stop and return to <code class="language-plaintext highlighter-rouge">/speckit.clarify</code> or <code class="language-plaintext highlighter-rouge">/speckit.plan</code> — not to silently <code class="language-plaintext highlighter-rouge">uv add</code> an unauthorized package.</p>

<h3 id="6-evaluation">6. Evaluation</h3>

<p>The developer raises a PR. The <code class="language-plaintext highlighter-rouge">.github/workflows/foundry-eval.yml</code> CI pipeline triggers the Foundry Security Spec. The CI-driven agentic system invokes the @Reviewer persona to review the implementation, hunt for prompt injection vectors, and leave a structured security report directly on the GitHub PR.</p>

<hr />

<h2 id="conclusion">Conclusion</h2>

<p>By integrating robust Python tooling directly into the AGENTS.md context and the Spec-Kit constitution, enterprises can scale AI development rapidly. CodeGuard and Cisco Foundry provide the necessary friction — both proactively during drafting and reactively during CI — to ensure that this speed does not compromise security.</p>

<p>The result is not a faster autocomplete. It is a disciplined AI engineering partner that reasons, plans, and executes inside a box you can inspect, audit, and evolve.</p>

<p>The template above uses hand-maintained files under <code class="language-plaintext highlighter-rouge">.agents/personas/</code> and <code class="language-plaintext highlighter-rouge">.agents/skills/</code> — the right starting point for a single repository. From here, the series follows a natural maturity curve: first proving the workflow locally, then asking how that governed context <strong>packages and distributes</strong> across hundreds of engineers and multiple IDEs without drift, then how <strong>phase gates</strong> keep agents from skipping specification checkpoints, and finally how <strong>automated evaluation</strong> closes the loop before code merges.</p>

<hr />

<p><em>This is the opening article in the spec-driven development series — from local template to enterprise-scale agentic engineering.</em></p>]]></content><author><name>Vincent Caldeira</name></author><category term="AAIF" /><category term="Engineering" /><summary type="html"><![CDATA[A practical base template strategy for Python agent development — combining AGENTS.md, GitHub Spec-Kit, and OGX into a governed spec-driven workflow.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://thinkinginsidethebox.ai/assets/images/og/spec-driven-development-enterprise-template.png" /><media:content medium="image" url="https://thinkinginsidethebox.ai/assets/images/og/spec-driven-development-enterprise-template.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Governed Run Loops: An End-to-End Multi-Layered Policy Architecture for Enterprise AI</title><link href="https://thinkinginsidethebox.ai/aaif/security/2026/06/29/governed-run-loops-for-mcp.html" rel="alternate" type="text/html" title="Governed Run Loops: An End-to-End Multi-Layered Policy Architecture for Enterprise AI" /><published>2026-06-29T09:00:00+08:00</published><updated>2026-06-29T09:00:00+08:00</updated><id>https://thinkinginsidethebox.ai/aaif/security/2026/06/29/governed-run-loops-for-mcp</id><content type="html" xml:base="https://thinkinginsidethebox.ai/aaif/security/2026/06/29/governed-run-loops-for-mcp.html"><![CDATA[<p>The race to deploy autonomous AI agents within highly regulated enterprises — finance, healthcare, law — is no longer focused solely on raw capability. It is strictly a matter of <strong>control</strong>.</p>

<p>Unlike creative writing tools, where hallucinations are a harmless quirk, a hallucination or policy breach within an enterprise asset acting as a fiduciary or compliance advisor is a severe liability. Bridging the gap between an inspiring laboratory demo and a production-grade enterprise asset cannot be solved by prompting harder. It requires a foundational architectural evolution that wraps the entire lifecycle of an agent’s execution into a strictly <strong>governed run loop</strong>.</p>

<p>On this blog, we call that discipline <strong>thinking inside the box</strong>: agents must reason, plan, and execute inside strict, deterministic, inspectable boundaries — not because creativity is undesirable, but because unconstrained autonomy is an operational and security risk in regulated environments.</p>

<p>True enterprise readiness requires shifting away from treating safety as a singular perimeter fence. Instead, policy management must be treated as an <strong>end-to-end framework</strong> implemented across multiple distinct layers — ensuring that an agent’s internal thinking, tool orchestration, and global boundaries are systematically assessed, controlled, and made fully auditable.</p>

<p>This article outlines a conceptual overview of that multi-layered policy management architecture. Without diving prematurely into code, we explore how foundational open projects like <a href="https://github.com/agentgateway/agentgateway"><strong>agentgateway</strong></a> and the <a href="https://modelcontextprotocol.io"><strong>Model Context Protocol (MCP)</strong></a> fit into an integrated, progressive governance strategy for enterprise deployments.</p>

<hr />

<h2 id="beyond-the-perimeter-why-centralized-gateways-alone-fail">Beyond the Perimeter: Why Centralized Gateways Alone Fail</h2>

<p>Traditional enterprise security relies heavily on perimeter control — intercepting inputs and filtering outputs at the network border. While an external API gateway (such as the <strong>agentgateway</strong> project) remains a vital operational component for global monitoring, relying <em>exclusively</em> on it creates a massive blind spot.</p>

<p>As language models evolve from passive assistants into autonomous, tool-using collectives, systemic risks completely change shape. High-risk threats — strategic deception, mid-loop coordination failures, suppressed information, misaligned tool usage — occur <strong>inside</strong> the execution lifecycle. If an agent decides mid-loop to execute an unauthorized database query or propagates a deceptive citation to a downstream component, an external perimeter fence cannot easily identify or mitigate the logical drift before the action occurs.</p>

<p>Governance cannot merely be a backend implementation detail tacked onto the edge of a network. It must be integrated into a <strong>hybrid architecture</strong> that blends centralized global coordination with localized autonomy directly inside the agent’s actual thinking process. The box is not just around the network — it runs through the run loop itself.</p>

<hr />

<h2 id="the-four-pillars-of-multi-layered-policy-management">The Four Pillars of Multi-Layered Policy Management</h2>

<p>To construct a resilient and auditable environment, enterprise policy implementation must operate across four foundational pillars.</p>

<h3 id="1-the-perimeter-layer-the-gateway">1. The Perimeter Layer (The Gateway)</h3>

<p>The outermost tier of the architecture is handled by a robust centralized proxy, represented by projects like <strong>agentgateway</strong>. This layer manages coarse-grained, system-level controls:</p>

<ul>
  <li><strong>Ingress &amp; egress validation</strong> — User inputs comply with corporate safety guidelines; outgoing streams do not leak intellectual property, sensitive system configurations, or personally identifiable information (PII).</li>
  <li><strong>System-level metrics &amp; throughput</strong> — The gateway acts as the operational traffic controller, managing global request routing across base models, enforcing rate limits, and securing system throughput under heavy enterprise loads.</li>
  <li><strong>Unified audit tracing</strong> — Basic transaction-level audit logs track user identity, session metadata, and agent selection for initial operational compliance.</li>
</ul>

<p>This is the outer wall of the box: the first line of defense, but not the only one.</p>

<h3 id="2-the-runtime-integration-layer-mcp-middleware">2. The Runtime Integration Layer (MCP Middleware)</h3>

<p>Once a transaction passes the perimeter, it enters the execution stack. Here, runtime middleware — the agent orchestration layer common to frameworks such as LangGraph and Semantic Kernel — operates directly over the agent’s interaction loop.</p>

<ul>
  <li><strong>Decoupled tool isolation via MCP</strong> — By leveraging the Model Context Protocol, the architecture cleanly decouples corporate databases, local financial tools, and enterprise repositories from core model weights and the immediate context window.</li>
  <li><strong>Declarative policy files</strong> — The runtime middleware evaluates actions against structured, configurable policy files (such as YAML configurations) that define strict boundaries for restricted topics — separating general educational content from heavily regulated transactional paths.</li>
  <li><strong>Decision-aware routing</strong> — Based on real-time policy scores, the middleware dynamically determines execution paths. Low-risk tracks respond immediately; medium-risk requests route strictly through trusted tools (e.g., verified calculation engines); high-risk domains trigger automated disclaimers or complete escalation paths.</li>
</ul>

<p>MCP gives enterprises a standard, inspectable interface for what the agent can touch — keeping tool traffic inside policy-defined lanes.</p>

<h3 id="3-the-internal-cognitive-layer-metacognitive-ai">3. The Internal Cognitive Layer (Metacognitive AI)</h3>

<p>A governed run loop must ensure that an agent can evaluate its own choices internally before executing them. This layer implements <strong>System 2</strong> thinking — an architectural superego that forces an agent to pause, reflect, and verify its own logic.</p>

<ul>
  <li><strong>Reflexive review nodes</strong> — The run loop abandons the traditional, linear <code class="language-plaintext highlighter-rouge">Input → Output</code> pipeline in favor of a reflexive <code class="language-plaintext highlighter-rouge">Input → Evaluate → Act</code> loop. Built as a specific node within the orchestration framework, the agent uses its immediate context window to review a tentative plan against runtime constraints before hitting any tool or external endpoint.</li>
  <li><strong>Structured self-models</strong> — To prevent an agent from “hallucinating expertise,” the architecture maintains a persistent, structured representation of the agent’s specific identity, capabilities, and regulatory limits. If an asset designed for portfolio tracking is prompted for detailed legal advice, it checks its self-model, identifies the boundary violation, and cleanly declines the task.</li>
</ul>

<p>This is metacognition as architecture: the agent learns to police itself <em>inside</em> the box before the box is breached.</p>

<h3 id="4-the-systemic-oversight-layer-hybrid-coordination--alignment">4. The Systemic Oversight Layer (Hybrid Coordination &amp; Alignment)</h3>

<p>Because purely localized self-assessment carries an inherent bias, complex enterprise tasks require objective, multi-agent oversight mechanisms to ensure total human alignment.</p>

<ul>
  <li><strong>Coordinator–judge topologies</strong> — Complex execution paths use an LLM-as-a-Judge pattern: a central Coordinator Agent plans a multi-step workflow, delegates sub-tasks to localized Expert Agents, and acts as a strict governance judge over their collective outputs.</li>
  <li><strong>Human-in-the-loop (HITL) triggers</strong> — When internal evaluation scores reveal low decision confidence, high-risk policy intersections, or anomalous tracing metrics, the system halts autonomous execution. The session is flagged and routed to a human analyst interface for manual validation.</li>
  <li><strong>Cognitive memory management</strong> — This layer handles the stability–plasticity balance: evaluating highly volatile real-time data inputs (plasticity) without forgetting long-term enterprise context or client risk boundaries (stability). By using MCP to decouple memory from underlying model weights, long-term context shapes tool selection and safeguard adherence without risking catastrophic forgetting.</li>
  <li><strong>Deep tracing &amp; reasoning logs</strong> — Every step of the internal decision trace, confidence computation, and metacognitive reasoning history is captured and saved to a unified compliance ledger, providing empirical assurance for formal safety cases.</li>
</ul>

<hr />

<h2 id="the-road-ahead-a-progressive-enterprise-rollout">The Road Ahead: A Progressive Enterprise Rollout</h2>

<p>Achieving complete, end-to-end governance over enterprise AI agents does not require building every component simultaneously — that path invites architectural complexity and unnecessary execution latency. Instead, organizations should roll out these foundational layers progressively:</p>

<ol>
  <li><strong>Phase 1 — The Boundary:</strong> Establish the perimeter layer via basic API proxying (<strong>agentgateway</strong>) alongside declarative runtime policy files to handle basic request routing and input/output filtering.</li>
  <li><strong>Phase 2 — The Pause:</strong> Introduce reflexive runtime nodes into orchestration tracks, training the agent to evaluate its short-term plans against user constraints before executing commands.</li>
  <li><strong>Phase 3 — The Self-Model:</strong> Implement explicit, structured self-models to give agents a reliable, auditable definition of their own functional identities and boundary restrictions.</li>
  <li><strong>Phase 4 — The Systemic Collective:</strong> Scale into hybrid multi-agent coordinator frameworks featuring persistent enterprise memory management and real-time human-in-the-loop escalation capabilities.</li>
</ol>

<p>By engineering a governance framework that manages both the external network boundary and the internal cognitive run loop, enterprises can move beyond the unconstrained risks of early generative AI into the auditable stability of governed enterprise assets.</p>

<hr />

<p><em>This is the opening article in a series on thinking inside the box. In subsequent posts, we will break down each of these four foundational pillars progressively — moving from this high-level conceptual architecture into explicit technical patterns, concrete policy configurations, and practical coding examples.</em></p>]]></content><author><name>Vincent Caldeira</name></author><category term="AAIF" /><category term="Security" /><summary type="html"><![CDATA[A conceptual overview of four foundational policy layers — perimeter, runtime, metacognitive, and systemic — for governing enterprise agent run loops with agentgateway and MCP.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://thinkinginsidethebox.ai/assets/images/og/governed-run-loops-for-mcp.png" /><media:content medium="image" url="https://thinkinginsidethebox.ai/assets/images/og/governed-run-loops-for-mcp.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>