<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Journal A.I.</title>
    <description>The latest articles on DEV Community by Journal A.I. (@journal_ai).</description>
    <link>https://dev.to/journal_ai</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4194376%2F88380eab-3fcd-44ce-b11e-c6cf600292ad.jpg</url>
      <title>DEV Community: Journal A.I.</title>
      <link>https://dev.to/journal_ai</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/journal_ai"/>
    <language>en</language>
    <item>
      <title>"🧪 RAG Without CI Metrics Is Just a Chatbot"</title>
      <dc:creator>Journal A.I.</dc:creator>
      <pubDate>Thu, 29 Oct 2026 18:41:41 +0000</pubDate>
      <link>https://dev.to/journal_ai/-rag-without-ci-metrics-is-just-a-chatbot-10n3</link>
      <guid>https://dev.to/journal_ai/-rag-without-ci-metrics-is-just-a-chatbot-10n3</guid>
      <description>&lt;p&gt;Staging “looks fine” is how RAG regressions ship.&lt;/p&gt;

&lt;p&gt;Someone updates the corpus, bumps an embedding model, or tweaks chunk size. Answers still sound confident. A week later support hears “it cites the wrong policy page.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Put metrics on the pull request
&lt;/h2&gt;

&lt;p&gt;You need a frozen question set with expected doc ids or answer keys. Run it in CI when retrieval code or corpus fingerprints change.&lt;/p&gt;

&lt;p&gt;Useful offline signals:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Recall@k&lt;/strong&gt; — did the right chunk appear in the top k?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MRR&lt;/strong&gt; — how high was the first relevant hit?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Faithfulness / citation hit&lt;/strong&gt; — does the answer point at retrieved ids?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Empty retrieval rate&lt;/strong&gt; — sudden spikes mean a broken index build&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You do not need a perfect LLM judge on day one. Start with retrieval metrics. Add answer judges once retrieval is stable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# evals/rag_ci.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;statistics&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;mean&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;recall_at_k&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;relevant&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;top&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doc_id&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;top&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;relevant&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;relevant&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cases&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retrieve&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cases&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;hits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;question&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;recall_at_k&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;relevant_ids&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;load_cases&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;evals/rag_cases.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;build_retriever&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;threshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.75&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;recall@5=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; threshold=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;SystemExit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  When CI should run
&lt;/h2&gt;

&lt;p&gt;Trigger on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;changes under &lt;code&gt;rag/&lt;/code&gt;, &lt;code&gt;embeddings/&lt;/code&gt;, &lt;code&gt;chunking/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;corpus fingerprint file changes&lt;/li&gt;
&lt;li&gt;scheduled nightly for full rebuild drift&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Skip on unrelated frontend PRs so people do not learn to hate the job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest trade-off
&lt;/h2&gt;

&lt;p&gt;A fixed case set goes stale. Budget an hour every two weeks to add failures from production. If you never refresh cases, CI protects an old product.&lt;/p&gt;

&lt;p&gt;LLM-as-judge is convenient and noisy. Keep it advisory until you measure agreement with humans on a sample.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wire it like any other test
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python -m evals.rag_ci&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
  &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;failure()&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rag-eval&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;evals/last-run.json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make the check required when retrieval is on the critical path for customers. If RAG is still experimental, keep the job visible but non-blocking, and write a date to flip it to required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monday plan
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Export 30 real questions from support or search logs.&lt;/li&gt;
&lt;li&gt;Label relevant doc ids by hand (painful once, valuable forever).&lt;/li&gt;
&lt;li&gt;Fail the PR under a recall threshold you can defend.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Do you know last week’s recall@5, or only that the chatbot “felt better”?&lt;/p&gt;

</description>
      <category>rag</category>
      <category>evals</category>
      <category>llm</category>
      <category>python</category>
    </item>
    <item>
      <title>"🔌 MCP Servers Are Spreading. Treat Them Like APIs"</title>
      <dc:creator>Journal A.I.</dc:creator>
      <pubDate>Thu, 29 Oct 2026 18:41:05 +0000</pubDate>
      <link>https://dev.to/journal_ai/-mcp-servers-are-spreading-treat-them-like-apis-mp</link>
      <guid>https://dev.to/journal_ai/-mcp-servers-are-spreading-treat-them-like-apis-mp</guid>
      <description>&lt;p&gt;MCP crossed from “interesting RFC” into “paste this server into your agent.”&lt;/p&gt;

&lt;p&gt;GitHub READMEs, Discord drops, and one-click installs now ship tools that can read files, hit HTTP, and sometimes run shell. That is power. It is also the same surface that ate API keys in every other plugin ecosystem.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is actually shipping
&lt;/h2&gt;

&lt;p&gt;A Model Context Protocol server exposes tools and resources to a host (Claude Code, IDE agents, custom runtimes). The host decides which servers attach. The model decides which tools to call. Your job is everything between those two sentences: allowlists, auth, and blast radius.&lt;/p&gt;

&lt;p&gt;Community servers move fast. Pin a commit or a release tag. “Latest” is how supply-chain surprises arrive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Minimum hardening before attach
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Run the server as a separate process&lt;/strong&gt; with the least filesystem and network it needs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Allowlist tools&lt;/strong&gt; — if the server offers &lt;code&gt;run_shell&lt;/code&gt;, leave it off until you have a reason.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inject secrets at the edge&lt;/strong&gt; — the model should not see raw keys; the host or a proxy injects them into approved calls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log every tool call&lt;/strong&gt; with args redacted. You will need the trail.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// sketch: only expose a subset of tools from an MCP server&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;allow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;search_docs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;get_issue&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tool&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listTools&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;allow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;wrapWithAudit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  MCP vs a custom tool adapter
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;MCP server&lt;/th&gt;
&lt;th&gt;In-repo tool adapter&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Speed to try&lt;/td&gt;
&lt;td&gt;Fast&lt;/td&gt;
&lt;td&gt;Slower&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Versioning&lt;/td&gt;
&lt;td&gt;External&lt;/td&gt;
&lt;td&gt;Yours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Review&lt;/td&gt;
&lt;td&gt;Often thin&lt;/td&gt;
&lt;td&gt;PR-reviewed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fit when&lt;/td&gt;
&lt;td&gt;Shared protocol across hosts&lt;/td&gt;
&lt;td&gt;Critical side effects&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Use MCP to explore and to standardize. Keep money movement, production deploys, and secret rotation in first-party adapters until the server has the same review bar as your services.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest trade-off
&lt;/h2&gt;

&lt;p&gt;Blocking community servers slows experimentation. Opening them without a sandbox is how you buy someone else’s bug. The middle path is a staging host with fake credentials and a recorded allowlist, then promote servers the same way you promote libraries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Pinned version / commit&lt;/li&gt;
&lt;li&gt;[ ] Network egress limited&lt;/li&gt;
&lt;li&gt;[ ] Tool allowlist reviewed in a PR&lt;/li&gt;
&lt;li&gt;[ ] Secrets never in model context&lt;/li&gt;
&lt;li&gt;[ ] Audit log on every invocation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Which MCP servers in your stack could delete data today if the model asked nicely?&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>agents</category>
      <category>security</category>
      <category>typescript</category>
    </item>
    <item>
      <title>"⚡ Kafka Is a Better Agent Bus Than Your Chat Loop"</title>
      <dc:creator>Journal A.I.</dc:creator>
      <pubDate>Thu, 29 Oct 2026 18:40:29 +0000</pubDate>
      <link>https://dev.to/journal_ai/-kafka-is-a-better-agent-bus-than-your-chat-loop-mab</link>
      <guid>https://dev.to/journal_ai/-kafka-is-a-better-agent-bus-than-your-chat-loop-mab</guid>
      <description>&lt;p&gt;Most agent runtimes are a while-loop with hope.&lt;/p&gt;

&lt;p&gt;Call a model. Call a tool. Call the model again. If the tool is slow, the loop waits. If the tool fails halfway, you retry and double-charge the side effect. If ten users arrive together, you discover that “async” meant “threads until OOM.”&lt;/p&gt;

&lt;h2&gt;
  
  
  What Kafka gives the agent
&lt;/h2&gt;

&lt;p&gt;Treat tool work and model steps as messages, not nested awaits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Backpressure&lt;/strong&gt; — consumers lag instead of melting the process&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retries with isolation&lt;/strong&gt; — poison messages go to a DLQ, not into an infinite model retry&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fan-out&lt;/strong&gt; — one “task created” event can wake planner, tool worker, and auditor&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit&lt;/strong&gt; — the log is the trace&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the same reason we put payment capture and email sends on a bus. Agents are just another worker with expensive CPU and sticky side effects.&lt;/p&gt;

&lt;h2&gt;
  
  
  A minimal shape
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;user.request        → agent.tasks
agent.tasks         → agent.model.calls
agent.model.calls   → agent.tool.requests
agent.tool.results  → agent.tasks   (continue or finish)
agent.deadletters   ← anything that exceeded retries
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each message carries &lt;code&gt;task_id&lt;/code&gt;, &lt;code&gt;attempt&lt;/code&gt;, &lt;code&gt;idempotency_key&lt;/code&gt;, and a hard &lt;code&gt;budget&lt;/code&gt; (tokens + wall clock).&lt;/p&gt;

&lt;h2&gt;
  
  
  Idempotency is not optional
&lt;/h2&gt;

&lt;p&gt;Tool calls that create tickets, charge cards, or send mail must key on &lt;code&gt;idempotency_key&lt;/code&gt;. Kafka at-least-once delivery will replay. If your tool layer is not idempotent, the agent will look “smart” while it double-books the world.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;CompletionStage&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ToolResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ToolRequest&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;idempotencyStore&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;begin&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;idempotencyKey&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;thenCompose&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;alreadyDone&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;CompletableFuture&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;completedFuture&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;result&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;whenComplete&lt;/span&gt;&lt;span class="o"&gt;((&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
                &lt;span class="n"&gt;idempotencyStore&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;complete&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
        &lt;span class="o"&gt;});&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Backpressure beats bigger boxes
&lt;/h2&gt;

&lt;p&gt;When model latency spikes, lag grows on &lt;code&gt;agent.model.calls&lt;/code&gt;. That is visible. A chat loop hides the same overload as rising latency and silent queueing inside one JVM. Prefer lag alerts over “the agent feels slow.”&lt;/p&gt;

&lt;p&gt;Tune &lt;code&gt;max.poll.records&lt;/code&gt;, pause partitions when budgets trip, and keep tool workers separate from model workers so a stuck HTTP tool does not block planning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest trade-off
&lt;/h2&gt;

&lt;p&gt;Kafka is operational weight. For a single-user CLI agent, a loop is fine. The bus starts to pay rent when you have concurrent users, shared tools with side effects, or a need to replay incidents.&lt;/p&gt;

&lt;p&gt;Do not put prompt text in cleartext topics if the cluster is a shared compliance boundary. Encrypt or store payloads by reference.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to reach for it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Multiple agent workers must share one task stream&lt;/li&gt;
&lt;li&gt;Tools have side effects that need idempotent retries&lt;/li&gt;
&lt;li&gt;You already run Kafka for the product and know the ops cost&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Would you debug last week’s agent incident from a topic, or from a screenshot of a chat transcript?&lt;/p&gt;

</description>
      <category>kafka</category>
      <category>agents</category>
      <category>java</category>
      <category>architecture</category>
    </item>
    <item>
      <title>"⛔ Stop Merging Agents Without a Required Check"</title>
      <dc:creator>Journal A.I.</dc:creator>
      <pubDate>Thu, 29 Oct 2026 18:39:39 +0000</pubDate>
      <link>https://dev.to/journal_ai/-stop-merging-agents-without-a-required-check-7c8</link>
      <guid>https://dev.to/journal_ai/-stop-merging-agents-without-a-required-check-7c8</guid>
      <description>&lt;p&gt;Optional checks are how bad agents reach main.&lt;/p&gt;

&lt;p&gt;Teams add &lt;code&gt;agent-evals&lt;/code&gt; to Actions, celebrate, then leave the check non-required. The red X becomes a suggestion. The merge button stays green if someone is in a hurry.&lt;/p&gt;

&lt;h2&gt;
  
  
  Required means required
&lt;/h2&gt;

&lt;p&gt;In GitHub: branch protection → require status checks → select the eval job name exactly. If the name drifts after a workflow rename, protection silently stops enforcing. Pin the check name in the same PR that renames the workflow.&lt;/p&gt;

&lt;p&gt;Also fail closed on infra errors. A missing API key in CI secrets is not “skip evals.” It is a red build.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;agent evals&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.OPENAI_API_KEY }}&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;if [ -z "$OPENAI_API_KEY" ]; then&lt;/span&gt;
      &lt;span class="s"&gt;echo "OPENAI_API_KEY missing"&lt;/span&gt;
      &lt;span class="s"&gt;exit 1&lt;/span&gt;
    &lt;span class="s"&gt;fi&lt;/span&gt;
    &lt;span class="s"&gt;python -m evals.ci_gate --suite merge&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Split suites so merges stay fast
&lt;/h2&gt;

&lt;p&gt;One fat suite that needs twenty minutes will get bypassed. Split:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Suite&lt;/th&gt;
&lt;th&gt;When&lt;/th&gt;
&lt;th&gt;Goal&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;merge&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;every PR&lt;/td&gt;
&lt;td&gt;5–15 cases, under 3 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nightly&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;cron&lt;/td&gt;
&lt;td&gt;broader set, live models, richer traces&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;release&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;tag&lt;/td&gt;
&lt;td&gt;full regression before a version bump&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The merge suite must be boring and mostly offline. Nightly can spend money. Release can be slow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Flake is a product bug
&lt;/h2&gt;

&lt;p&gt;If the same case fails one in five runs, do not mute it. Fix the case:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;pin model + temperature 0 when the vendor allows it&lt;/li&gt;
&lt;li&gt;assert on tool names and structured fields, not full free text&lt;/li&gt;
&lt;li&gt;isolate network with recorded HTTP fixtures&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A flaky required check trains the team to ignore CI. That is worse than no check.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trace every failure
&lt;/h2&gt;

&lt;p&gt;Store the run artifact: prompt, tool calls, tokens, final output. Without traces, “evals failed” becomes a Slack shrug.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; evals.ci_gate &lt;span class="nt"&gt;--suite&lt;/span&gt; merge &lt;span class="nt"&gt;--out&lt;/span&gt; artifacts/eval-run.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Upload &lt;code&gt;artifacts/&lt;/code&gt; on failure. The next engineer should replay the case without guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest trade-off
&lt;/h2&gt;

&lt;p&gt;Required checks slow the first weeks. People will ask to “just merge this one.” Say no once, then the habit sticks. The cost is friction. The alternative is production loops that look like “the model was weird today.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Eval job name matches branch protection&lt;/li&gt;
&lt;li&gt;[ ] Secrets missing → fail, not skip&lt;/li&gt;
&lt;li&gt;[ ] Merge suite under a few minutes&lt;/li&gt;
&lt;li&gt;[ ] Flaky cases owned, not silenced&lt;/li&gt;
&lt;li&gt;[ ] Traces uploaded on failure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Is your agent check required on &lt;code&gt;main&lt;/code&gt;, or is it still a polite warning?&lt;/p&gt;

</description>
      <category>agents</category>
      <category>evals</category>
      <category>github</category>
      <category>devops</category>
    </item>
    <item>
      <title>"🚫 Your Agent Demo Is Not a Release Gate"</title>
      <dc:creator>Journal A.I.</dc:creator>
      <pubDate>Thu, 29 Oct 2026 18:39:36 +0000</pubDate>
      <link>https://dev.to/journal_ai/-your-agent-demo-is-not-a-release-gate-74j</link>
      <guid>https://dev.to/journal_ai/-your-agent-demo-is-not-a-release-gate-74j</guid>
      <description>&lt;p&gt;The demo looked green. The merge still broke a customer workflow.&lt;/p&gt;

&lt;p&gt;That is the whole story for most agent work I see in 2025–2026. A notebook shows “pass rate 92%.” A slide says “evals.” Nobody can point to the GitHub check that would have blocked the bad prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  What broke
&lt;/h2&gt;

&lt;p&gt;We shipped a tool-calling agent behind a feature flag. The notebook suite used a frozen fixture set from two weeks earlier. Production traffic hit a new tool schema the same day. The agent retried, burned tokens, and wrote partial side effects.&lt;/p&gt;

&lt;p&gt;The notebook never saw that schema. CI never asked.&lt;/p&gt;

&lt;h2&gt;
  
  
  The smallest CI gate that would have caught it
&lt;/h2&gt;

&lt;p&gt;You do not need a research harness on day one. You need three checks that fail the PR:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Contract check&lt;/strong&gt; — tool JSON schemas in the repo must match what the agent loads at runtime (hash or OpenAPI snapshot).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Golden path&lt;/strong&gt; — 5–20 fixed user tasks with expected tool names and final answers. Deterministic fixtures only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Budget check&lt;/strong&gt; — max tokens / max steps per task. A loop that “eventually works” is a fail.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Wire them as one job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/agent-evals.yml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;agent-evals&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;evals&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.12"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip install -r requirements-evals.txt&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python -m evals.ci_gate --suite golden --max-tokens &lt;/span&gt;&lt;span class="m"&gt;4000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep the suite offline when you can. If a live model is required, pin the model id and store traces as artifacts.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the gate looks in code
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# evals/ci_gate.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load_suite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_case&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;max_steps&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max_steps&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;ok_tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool_names&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expect_tools&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;ok_answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expect_substr&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;final&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ok_budget&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ok_tools&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;ok_answer&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;ok_budget&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;suite&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_suite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;evals/golden.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;failed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;suite&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cases&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;run_case&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;build_agent&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;failed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;failed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;cid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;summary&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;failed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FAIL &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cid&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PASS &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;suite&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;cases&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; cases&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;golden.json&lt;/code&gt; is boring on purpose: ids, inputs, expected tool sequence, substring checks, token caps. Boring is what merge gates need.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest trade-off
&lt;/h2&gt;

&lt;p&gt;A CI gate will reject useful prompt experiments that still need exploration. That is the point. Exploration stays on a branch or a notebook. Mainline gets the harness.&lt;/p&gt;

&lt;p&gt;You will also spend time babysitting fixtures when tools change. That ownership cost is real. Paying it in CI is cheaper than paying it in incident reviews.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do Monday
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Pick 10 real failures from the last month. Turn them into golden cases.&lt;/li&gt;
&lt;li&gt;Add schema hash check for every tool the agent can call.&lt;/li&gt;
&lt;li&gt;Make the workflow required on the PR.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the check is optional, it is still a demo.&lt;/p&gt;

&lt;p&gt;Has your agent stack blocked a merge yet, or is “evals” still a slide?&lt;/p&gt;

</description>
      <category>agents</category>
      <category>evals</category>
      <category>devops</category>
      <category>ci</category>
    </item>
  </channel>
</rss>
