<?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: Jack M</title>
    <description>The latest articles on DEV Community by Jack M (@jackm-singularity).</description>
    <link>https://dev.to/jackm-singularity</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%2F3953435%2F35a14dd7-6df4-4155-95f8-b475eb620f37.png</url>
      <title>DEV Community: Jack M</title>
      <link>https://dev.to/jackm-singularity</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jackm-singularity"/>
    <language>en</language>
    <item>
      <title>AI Agent Status Updates: Build Progress Users Can Trust Without Exposing Chain of Thought</title>
      <dc:creator>Jack M</dc:creator>
      <pubDate>Mon, 26 Oct 2026 03:34:16 +0000</pubDate>
      <link>https://dev.to/jackm-singularity/ai-agent-status-updates-build-progress-users-can-trust-without-exposing-chain-of-thought-1mnd</link>
      <guid>https://dev.to/jackm-singularity/ai-agent-status-updates-build-progress-users-can-trust-without-exposing-chain-of-thought-1mnd</guid>
      <description>&lt;p&gt;An agent that goes quiet for ten minutes does not feel autonomous. It feels broken. A spinner and a made-up “82% complete” label are not much better: they encourage users to trust a number your system cannot justify.&lt;/p&gt;

&lt;p&gt;The fix is not to stream the model's hidden reasoning. It is to publish a small, evidence-backed account of the work: what phase the run is in, the last verified milestone, whether it needs a person, and what will happen next. This article shows how to build that contract for long-running, tool-using AI agents.&lt;/p&gt;

&lt;h2&gt;
  
  
  The progress problem is really a trust problem
&lt;/h2&gt;

&lt;p&gt;Users ask three practical questions while an agent runs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Is it still alive?&lt;/li&gt;
&lt;li&gt;Did it make meaningful progress?&lt;/li&gt;
&lt;li&gt;Do I need to do anything?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A token stream answers none of these reliably. It can continue while a tool is stuck, and it can stop even though a durable workflow will resume later. Likewise, an LLM-generated status such as “almost finished” is a prediction, not evidence.&lt;/p&gt;

&lt;p&gt;Treat status as a product contract. The user-facing timeline should be derived from workflow events your system can verify: a job was accepted, a connector returned records, validation passed, an approval is waiting, or a final artifact was stored. The model may suggest a label, but it should not be the source of truth for state.&lt;/p&gt;

&lt;p&gt;This distinction also protects privacy. A useful update is “Checked 42 invoices; 3 need review,” not a transcript of the agent's scratch work, retrieved customer text, or chain of thought.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with a state machine, not a progress bar
&lt;/h2&gt;

&lt;p&gt;Long-running agent work has more states than &lt;code&gt;running&lt;/code&gt; and &lt;code&gt;failed&lt;/code&gt;. Model the states explicitly and make transitions durable.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;th&gt;Meaning to the user&lt;/th&gt;
&lt;th&gt;Required evidence&lt;/th&gt;
&lt;th&gt;Useful UI action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;queued&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Work is accepted but has not started&lt;/td&gt;
&lt;td&gt;run record created&lt;/td&gt;
&lt;td&gt;Cancel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;running&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A worker currently owns the run&lt;/td&gt;
&lt;td&gt;lease heartbeat&lt;/td&gt;
&lt;td&gt;Stop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;waiting_on_tool&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The run is awaiting a bounded external call&lt;/td&gt;
&lt;td&gt;tool request ID and deadline&lt;/td&gt;
&lt;td&gt;Cancel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;waiting_on_user&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A decision or missing input is required&lt;/td&gt;
&lt;td&gt;approval or question ID&lt;/td&gt;
&lt;td&gt;Review / answer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;retry_scheduled&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The system will retry later&lt;/td&gt;
&lt;td&gt;retry time and error class&lt;/td&gt;
&lt;td&gt;Cancel / view details&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;succeeded&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The requested result was verified and saved&lt;/td&gt;
&lt;td&gt;output receipt&lt;/td&gt;
&lt;td&gt;Open result&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;failed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The run cannot continue automatically&lt;/td&gt;
&lt;td&gt;failure class and last checkpoint&lt;/td&gt;
&lt;td&gt;Retry / contact support&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cancelled&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Work was intentionally stopped&lt;/td&gt;
&lt;td&gt;cancellation receipt&lt;/td&gt;
&lt;td&gt;Restart&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Avoid a vague &lt;code&gt;paused&lt;/code&gt; state. If the workflow is paused because it needs an address, show &lt;code&gt;waiting_on_user&lt;/code&gt;. If it is paused because a vendor rate-limited it, show &lt;code&gt;retry_scheduled&lt;/code&gt;. Specific state names let a user decide what to do without reading logs.&lt;/p&gt;

&lt;p&gt;Here is a compact TypeScript event shape. It is deliberately about observable work, not hidden reasoning.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;RunEvent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;sequence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;          &lt;span class="c1"&gt;// monotonic per run&lt;/span&gt;
  &lt;span class="nl"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;phase_started&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;milestone&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;approval_needed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;retry_scheduled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;completed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;preparing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;collecting&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;verifying&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;delivering&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;           &lt;span class="c1"&gt;// reviewed, user-safe copy&lt;/span&gt;
  &lt;span class="nl"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;artifact&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tool_receipt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;checkpoint&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;approval&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nl"&gt;action&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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;p&gt;Persist these events in the same transaction that changes workflow state, or attach them to an outbox. A UI event that says “report ready” before the report is stored is worse than no event at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Publish milestones, not a narrated thought process
&lt;/h2&gt;

&lt;p&gt;Choose milestones at boundaries where the system can prove something changed. Good milestones are stable across model versions and useful if a run resumes tomorrow.&lt;/p&gt;

&lt;p&gt;For a document-analysis agent, the timeline might be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Request accepted&lt;/li&gt;
&lt;li&gt;Source file scanned and indexed&lt;/li&gt;
&lt;li&gt;42 records extracted&lt;/li&gt;
&lt;li&gt;Policy checks completed&lt;/li&gt;
&lt;li&gt;Three exceptions need a decision&lt;/li&gt;
&lt;li&gt;Final report saved&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Notice what is absent: “thinking about clause 7” or “I believe this source is probably relevant.” Those claims are neither a dependable progress signal nor an appropriate audit record.&lt;/p&gt;

&lt;p&gt;Each milestone needs an owner. The workflow engine owns checkpoint messages; the connector owns data-fetch receipts; the policy service owns validation results. If no component can produce evidence for an update, leave it out.&lt;/p&gt;

&lt;p&gt;That rule prevents a common failure mode: a chat interface keeps emitting cheerful prose while the actual job stopped moving. Your supervisor should detect stale progress independently of the agent. For example, mark a run &lt;code&gt;stalled&lt;/code&gt; internally when its lease heartbeat is fresh but neither a checkpoint nor a tool receipt has advanced within the expected phase window. The user-facing text can then say, “Still working longer than expected; no action needed yet,” or escalate honestly to “This task needs attention.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Design an honest progress model
&lt;/h2&gt;

&lt;p&gt;Percent complete works only when units of work are known. A migration over 10,000 rows has a denominator. An open-ended research task generally does not.&lt;/p&gt;

&lt;p&gt;Use one of these models instead:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Work shape&lt;/th&gt;
&lt;th&gt;Best indicator&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fixed batch&lt;/td&gt;
&lt;td&gt;completed / total&lt;/td&gt;
&lt;td&gt;&lt;code&gt;318 of 500 files checked&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Known workflow&lt;/td&gt;
&lt;td&gt;completed phase&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Verifying findings (3 of 4 phases)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Open-ended work&lt;/td&gt;
&lt;td&gt;elapsed time plus latest verified event&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Last verified update: sources indexed&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Approval flow&lt;/td&gt;
&lt;td&gt;blocked state and decision impact&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Needs approval before sending 3 emails&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retryable external work&lt;/td&gt;
&lt;td&gt;next retry and preserved checkpoint&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Retrying connector at 14:05; 18 items saved&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Do not convert phase count into a false percentage. Four phases do not necessarily take equal time, and the model can loop inside one phase. If you show an estimate, label it as a range derived from similar completed runs, and hide it when the sample is weak.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;statusCopy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Run&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="nx"&gt;run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;waiting_on_user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Your decision is needed before this task can continue.&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;completedUnits&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;totalUnits&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;completedUnits&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; of &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;totalUnits&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; items verified`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`Last verified update: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;latestMilestone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&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;p&gt;The word &lt;em&gt;verified&lt;/em&gt; matters. It teaches users what the timeline represents and sets a standard your team can test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make waiting visible and actionable
&lt;/h2&gt;

&lt;p&gt;The most damaging silence is not a slow model call. It is an agent waiting for a person while the product looks busy.&lt;/p&gt;

&lt;p&gt;An approval event should include the decision, the consequence, the safe default, and an expiry rule. Keep the payload concise and reviewable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"approval_needed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Three customer records have conflicting addresses."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"question"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Which address should be used for the renewal notice?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"options"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"CRM record"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"latest support ticket"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"skip these records"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"skip these records"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expires_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-26T15:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"evidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"approval"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"apr_8f2"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Never bury an approval in a scrolling chat. Surface it as a run state, send a notification only when action is required, and ensure a resumed run re-reads the approved decision from durable storage. This pairs well with approval gates, but it solves a different problem: making the wait understandable to the person who initiated the work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Transport updates without building a fragile realtime system
&lt;/h2&gt;

&lt;p&gt;Start with the simplest transport that fits your product.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Polling&lt;/strong&gt; is enough for minute-scale background jobs. Return &lt;code&gt;status&lt;/code&gt;, &lt;code&gt;latest_event&lt;/code&gt;, and &lt;code&gt;etag&lt;/code&gt;; back off when nothing changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server-sent events&lt;/strong&gt; work well for one-way timelines and reconnect cleanly with &lt;code&gt;Last-Event-ID&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebSockets&lt;/strong&gt; fit collaborative workspaces where users can also approve, stop, or redirect a run live.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Notifications&lt;/strong&gt; should be sparse: completion, failure, and &lt;code&gt;waiting_on_user&lt;/code&gt; are normally worth interrupting someone; routine milestones are not.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Regardless of transport, the client must tolerate duplicate and out-of-order events. Use the &lt;code&gt;sequence&lt;/code&gt; field, make the event stream append-only, and fetch the authoritative run snapshot after reconnecting. The UI is a projection; the workflow database remains the authority.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build the API around a durable run record
&lt;/h2&gt;

&lt;p&gt;A small API surface is easier to secure and test than a channel full of ad hoc text.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /runs/run_123

{
  "status": "waiting_on_user",
  "phase": "verifying",
  "latest_event_sequence": 17,
  "latest_event": {
    "message": "42 records verified; 3 need a decision",
    "at": "2026-10-26T12:04:12Z"
  },
  "actions": [{ "kind": "approve", "approval_id": "apr_8f2" }]
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep sensitive tool arguments, raw documents, and internal exception stacks behind a permissioned diagnostics view. The default run page should disclose only the minimum a user needs to understand progress and take the next action.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the status contract like an API
&lt;/h2&gt;

&lt;p&gt;Status is part of the user experience, so add tests beyond the happy path:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A run cannot become &lt;code&gt;succeeded&lt;/code&gt; without an output receipt.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;waiting_on_user&lt;/code&gt; run always exposes a decision action and cannot spend tools until it resumes.&lt;/li&gt;
&lt;li&gt;Replayed events create the same final timeline.&lt;/li&gt;
&lt;li&gt;A reconnect receives no duplicate visible milestones.&lt;/li&gt;
&lt;li&gt;A cancelled run cannot later publish a success event.&lt;/li&gt;
&lt;li&gt;A stale worker lease produces an honest recovery state, not permanent “Working…” copy.&lt;/li&gt;
&lt;li&gt;User-safe messages contain no prompt, secret, customer text, or chain of thought.
&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="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;does not claim success before output exists&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;workflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;completed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;runs&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;run_123&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toMatchObject&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;"&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;p&gt;This is also where observability earns its keep. Measure time to first verified milestone, time spent waiting for users, stale-run recovery rate, and the share of runs that finish with a useful artifact. Avoid treating the number of progress messages as a health metric; a noisy timeline can conceal a poor workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical rollout plan
&lt;/h2&gt;

&lt;p&gt;Do not retrofit every agent at once. Start with one workflow where users currently refresh a page or ask support whether a job is stuck.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Define the state machine and ownership of each transition.&lt;/li&gt;
&lt;li&gt;Add durable run records and one evidence-backed milestone per phase.&lt;/li&gt;
&lt;li&gt;Replace generic spinners with the latest verified update.&lt;/li&gt;
&lt;li&gt;Promote approval waits to a clear, resumable state.&lt;/li&gt;
&lt;li&gt;Add stale-run detection and a recovery message.&lt;/li&gt;
&lt;li&gt;Instrument completion quality, not just elapsed time.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The result is deliberately less theatrical than a stream of simulated thinking. It is far more useful: people can leave a long-running agent alone, return with context, and intervene exactly when their judgment is needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keep the language stable across releases
&lt;/h3&gt;

&lt;p&gt;Treat visible status copy as part of your API. Changing &lt;code&gt;waiting_on_user&lt;/code&gt; from “Needs your decision” to “Paused” may seem cosmetic, yet it can break notification rules, customer runbooks, and accessibility labels. Version the event schema, localize messages at the presentation layer, and keep a small compatibility test suite for every public state. That discipline pays off when new models, tools, and workflow engines arrive: the user still receives the same clear promise about what has happened and what they can do next.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Should an AI agent show its chain of thought as progress?
&lt;/h3&gt;

&lt;p&gt;No. Show concise, evidence-backed milestones instead. They are more reliable, safer for sensitive data, and easier for users to act on.&lt;/p&gt;

&lt;h3&gt;
  
  
  How often should an agent send status updates?
&lt;/h3&gt;

&lt;p&gt;Update on meaningful state changes, verified milestones, and action-required moments. For long silent phases, add a bounded heartbeat that says the job is still owned, not a stream of filler messages.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is a percentage complete useful for AI agents?
&lt;/h3&gt;

&lt;p&gt;Only when the work has a known denominator, such as files processed or records checked. Otherwise show the phase, elapsed time, and most recent verified milestone.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the best state for an agent waiting on a person?
&lt;/h3&gt;

&lt;p&gt;Use a specific &lt;code&gt;waiting_on_user&lt;/code&gt; state with the exact question, a safe default, and a resumable approval ID. Do not leave the run labeled &lt;code&gt;running&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can polling work for long-running AI workflows?
&lt;/h3&gt;

&lt;p&gt;Yes. Polling is simple and reliable for background tasks that update every few seconds or minutes. Use ETags and backoff, then move to SSE or WebSockets only when the interaction requires it.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do you detect an AI agent that is stuck?
&lt;/h3&gt;

&lt;p&gt;Track worker leases, tool deadlines, and the time since the last verified milestone. A supervisor outside the model should classify stale runs and surface a recovery state.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>ux</category>
      <category>programming</category>
      <category>devops</category>
    </item>
    <item>
      <title>AI Agent Trace Replay: Turn Production Failures Into Safe Regression Tests</title>
      <dc:creator>Jack M</dc:creator>
      <pubDate>Sun, 25 Oct 2026 03:28:32 +0000</pubDate>
      <link>https://dev.to/jackm-singularity/ai-agent-trace-replay-turn-production-failures-into-safe-regression-tests-4h4l</link>
      <guid>https://dev.to/jackm-singularity/ai-agent-trace-replay-turn-production-failures-into-safe-regression-tests-4h4l</guid>
      <description>&lt;p&gt;Your agent failed once in production. The tempting fix is to patch the prompt, glance at the next successful run, and move on. That is how the same failure returns after a model, tool, or routing change.&lt;/p&gt;

&lt;p&gt;The more useful response is to turn the failed run into a replayable regression test. Not a full rerun against live customer data. Not a screen recording. A small, scrubbed fixture that preserves the decisions and tool boundary that mattered, then proves the fix without sending another email, creating another ticket, or charging another card.&lt;/p&gt;

&lt;p&gt;This guide shows how to build that pipeline. It is aimed at engineers operating tool-using agents in production. The result is a durable feedback loop: incidents become tests, and tests make the next release safer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What agent trace replay is — and is not
&lt;/h2&gt;

&lt;p&gt;An agent trace is an ordered record of a run: user intent, policy version, prompt revision, model calls, tool requests, tool responses, retry decisions, and final outcome. Trace replay uses a captured run as controlled input for a later test.&lt;/p&gt;

&lt;p&gt;That definition matters because &lt;strong&gt;replay can mean different things&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Replay mode&lt;/th&gt;
&lt;th&gt;What stays fixed&lt;/th&gt;
&lt;th&gt;What you learn&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Deterministic fixture replay&lt;/td&gt;
&lt;td&gt;Tool outputs and selected model output&lt;/td&gt;
&lt;td&gt;Whether orchestration, parsing, policies, and recovery still work&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model-in-the-loop replay&lt;/td&gt;
&lt;td&gt;Tool outputs are fixed; model runs again&lt;/td&gt;
&lt;td&gt;Whether a prompt/model change preserves the expected trajectory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Load replay&lt;/td&gt;
&lt;td&gt;Request timing and token shapes&lt;/td&gt;
&lt;td&gt;Whether serving latency, cache behavior, and capacity changed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Live rerun&lt;/td&gt;
&lt;td&gt;Almost nothing&lt;/td&gt;
&lt;td&gt;Usually too risky for debugging a customer incident&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For a regression suite, start with the first mode. It makes a production bug repeatable without repeating the side effect. Add model-in-the-loop replay only after you can score it reliably.&lt;/p&gt;

&lt;p&gt;This is different from a generic evaluation harness. An evaluation suite begins with representative tasks. Trace replay begins with a real failure, preserves the evidence that explains it, and turns a specific incident into a release gate. It is also different from event sourcing: the goal here is not to reconstruct every business object, but to create a minimal, privacy-safe test case for the agent behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure pattern that replay catches
&lt;/h2&gt;

&lt;p&gt;Imagine an operations agent that receives a request to pause a subscription. A billing lookup times out, the agent retries with a malformed account identifier, then tells the customer the subscription was paused. The retry did not change the account, and the customer is still billed.&lt;/p&gt;

&lt;p&gt;An answer-quality check may pass because the message is polite. A unit test for the billing tool may pass because the tool validates normal inputs. The failure lives in the trajectory: timeout, incorrect fallback identifier, write attempt, unverified success claim.&lt;/p&gt;

&lt;p&gt;A replay fixture lets you test that exact sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Return the recorded timeout from the first lookup.&lt;/li&gt;
&lt;li&gt;Provide the recorded account data from the fallback lookup.&lt;/li&gt;
&lt;li&gt;Require an operation receipt before a success response is allowed.&lt;/li&gt;
&lt;li&gt;Assert that the agent either verifies the pause or reports an unresolved state.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The key idea is simple: assert what the agent did and what it was allowed to claim, not only its final prose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Capture the smallest useful trace
&lt;/h2&gt;

&lt;p&gt;Do not dump every prompt and response into a test repository. A useful capture is intentional and bounded. Store only what you need to reproduce the decision boundary.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;TraceEvent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;sequence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;input&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;model&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tool_call&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tool_result&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;policy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;outcome&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nl"&gt;payloadRef&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="c1"&gt;// encrypted or redacted fixture reference&lt;/span&gt;
  &lt;span class="nl"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;operationId&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nl"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;            &lt;span class="c1"&gt;// tamper-evident hash of canonical event data&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use a separate protected store for raw payloads if you genuinely need them. The trace used for test selection can usually carry redacted values, stable identifiers, schemas, response classes, and hashes. Never record chain-of-thought. You do not need it to verify tool order, policy decisions, structured arguments, or an authoritative receipt.&lt;/p&gt;

&lt;p&gt;Capture these fields at minimum:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A correlation ID distinct from the customer and conversation IDs.&lt;/li&gt;
&lt;li&gt;Versions for the prompt, model, tool schema, retrieval corpus, and runtime policy.&lt;/li&gt;
&lt;li&gt;Tool arguments after validation, with secrets and personal data replaced by typed placeholders.&lt;/li&gt;
&lt;li&gt;Tool result class: success, retryable failure, permanent failure, unknown outcome, or rejected.&lt;/li&gt;
&lt;li&gt;External operation ID and authoritative verification result for every write.&lt;/li&gt;
&lt;li&gt;The final customer-visible claim and the rule that allowed it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The recording should be append-only. If a trace is edited for a fixture, retain its source run ID and record each redaction. Otherwise a convenient test fixture can quietly stop representing the incident that justified it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build a side-effect firewall before replaying anything
&lt;/h2&gt;

&lt;p&gt;The dangerous mistake is to point a replay at production tools and assume an idempotency key makes it safe. Idempotency reduces duplicate writes; it does not make an old action appropriate today.&lt;/p&gt;

&lt;p&gt;Use a replay adapter that has an explicit mode and a deny-by-default tool policy.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SAFE_REPLAY_TOOLS&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;customer.lookup&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;billing.status&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;invokeForReplay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ToolCall&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fixture&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FixtureStore&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;SAFE_REPLAY_TOOLS&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;call&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="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;blocked&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Replay may not execute &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;call&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;recorded_fixture_or_verified_sandbox&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;};&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;recorded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fixture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;call&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="nx"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;normalizedArgs&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;recorded&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;missing_fixture&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;recorded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;result&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;p&gt;Treat writes, browser actions, emails, payment actions, deployments, and permission changes as blocked by default. For a write you need to model, return a recorded receipt or run against a dedicated sandbox with synthetic identities. A good replay failure is &lt;code&gt;missing_fixture&lt;/code&gt;; it tells you the test is no longer a faithful simulation. A bad replay silently calls the real service.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turn an incident into a compact fixture
&lt;/h2&gt;

&lt;p&gt;Raw production runs are noisy. Reduce them in a reviewable sequence.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Name the observable failure
&lt;/h3&gt;

&lt;p&gt;Avoid labels such as “agent confused.” Write the event that should never recur: “The agent claimed a subscription was paused without a successful pause receipt.” This becomes the primary assertion.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Keep the causal path, not every span
&lt;/h3&gt;

&lt;p&gt;Retain the user request, relevant retrieval result, tool calls, tool responses, policy decisions, and final output. Remove unrelated telemetry and tool calls that cannot affect the outcome. If deletion changes the failure, put it back.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Replace sensitive values consistently
&lt;/h3&gt;

&lt;p&gt;Map &lt;code&gt;alex@example.com&lt;/code&gt; to &lt;code&gt;user_01@example.invalid&lt;/code&gt; everywhere. Preserve the shape, locale, formats, and relationships that affected parsing. Do not replace a malformed identifier with a clean one when malformed input triggered the bug.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Freeze external responses
&lt;/h3&gt;

&lt;p&gt;Represent each tool result as a fixture, including error bodies and headers the agent reads. A generic 500 does not reproduce a timeout branch that contained a retry hint.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Version the expected contract
&lt;/h3&gt;

&lt;p&gt;Keep the allowed trajectory with the fixture. The contract can specify required calls, forbidden calls, maximum retries, expected state, and required evidence for a success claim.&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="na"&gt;case&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pause-with-unknown-outcome&lt;/span&gt;
&lt;span class="na"&gt;must_call&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;customer.lookup&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;billing.pause&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;billing.get_operation&lt;/span&gt;
&lt;span class="na"&gt;forbid_call&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;customer.notify_success&lt;/span&gt;
&lt;span class="na"&gt;max_calls&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;billing.pause&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="na"&gt;assert&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;final_state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unresolved_or_verified_paused&lt;/span&gt;
  &lt;span class="na"&gt;success_requires&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;billing.get_operation.status == completed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This format is intentionally boring. It lets a reviewer reason about behavior without reverse-engineering a prompt transcript.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use layered assertions, not one brittle snapshot
&lt;/h2&gt;

&lt;p&gt;An exact full transcript comparison makes every harmless wording change look like a regression. A single LLM judge makes every serious policy violation subjective. Combine layers instead.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Structural assertions:&lt;/strong&gt; valid tool names, JSON schema, required fields, and no secret-like values in output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trajectory assertions:&lt;/strong&gt; tool order, retry ceiling, no forbidden tool, and approval step before a sensitive action.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State assertions:&lt;/strong&gt; an independent fixture or sandbox read-back confirms the intended business state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Outcome assertions:&lt;/strong&gt; the response does not promise more than evidence supports.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Semantic assertions:&lt;/strong&gt; use a calibrated judge only for meaning that cannot be expressed deterministically, such as whether an explanation is understandable.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here is a deliberately small trajectory test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toolCalls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;x&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="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;customer.lookup&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;billing.pause&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;billing.get_operation&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toolCalls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;billing.pause&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toHaveLength&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;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;final&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;claim&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;not&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toMatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/paused successfully/i&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;final&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;claim&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toMatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/still verifying|could not confirm/i&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prefer stable properties over wording. If your product truly needs a specific compliance phrase, assert that phrase separately and explain why.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make model-in-the-loop replay measurable
&lt;/h2&gt;

&lt;p&gt;Eventually you will want to replay the same fixture after changing a model or prompt. That run is non-deterministic, so design for variance instead of pretending it does not exist.&lt;/p&gt;

&lt;p&gt;Run the case several times at the production-like temperature. Require every run to satisfy safety invariants, then set a threshold for softer outcomes. For example, all five runs must avoid an unverified success claim, four of five must choose the verified recovery path, and a human reviews any new tool sequence before it becomes accepted behavior.&lt;/p&gt;

&lt;p&gt;Record the model and decoding settings with the result. A changed temperature, tool description, or retrieval revision can cause divergence even when the prompt file is untouched. If a replay diverges, classify it: acceptable alternative, new regression, fixture mismatch, or insufficient specification. Do not hide divergence by updating the snapshot automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put replay in the release path
&lt;/h2&gt;

&lt;p&gt;Not every captured trace belongs in pull-request CI. Start with a small “incident corpus”: the highest-impact, recent, and recurring failures. Tag each case by workflow, risk, tenant shape, and the dependency it guards.&lt;/p&gt;

&lt;p&gt;Run the cheap deterministic fixture suite on every change that touches prompts, policies, tool schemas, orchestration, or response rendering. Run model-in-the-loop cases on a scheduled or pre-release job where repeated samples are affordable. Run load replay separately; it answers latency and capacity questions, not behavioral correctness.&lt;/p&gt;

&lt;p&gt;When a replay fails, show a compact diff:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;case: pause-with-unknown-outcome
expected: verify operation before customer success claim
actual:   billing.pause -&amp;gt; customer.notify_success
first divergence: sequence 7, policy "receipt_required" skipped
changed inputs: prompt v42 -&amp;gt; v43; tool schema unchanged
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That output is useful to the engineer on call. A wall of token traces is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common replay traps
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Treating replay as proof of live behavior.&lt;/strong&gt; A fixture proves behavior against its captured conditions. It does not prove correctness for new customer data, a changed upstream API, or a different model response. Keep ordinary integration tests and sampled production monitoring.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Saving raw customer data forever.&lt;/strong&gt; Expiring raw captures and retaining redacted fixtures reduces risk. Define who can access source traces and how a fixture is deleted when its underlying data must be removed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mocking away the failure.&lt;/strong&gt; If the incident involved a malformed response or a missing field, preserve it. A happy-path mock creates false confidence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Testing only the final answer.&lt;/strong&gt; Agents can say the right thing after taking the wrong action. Test the trajectory and authoritative state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Letting fixtures drift.&lt;/strong&gt; Give every fixture an owner, source trace, schema version, and review date. When a tool contract changes, decide whether the old incident remains a valid regression or should be migrated with an explicit explanation.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical first week
&lt;/h2&gt;

&lt;p&gt;Pick one incident where the agent caused an incorrect claim, a repeated action, an unsafe call, or a costly loop. Capture the trace, redact it, block all side effects, and write three deterministic assertions: one forbidden action, one required evidence check, and one final-state rule. Put that one case in CI.&lt;/p&gt;

&lt;p&gt;Then repeat for the next real incident. In a few weeks, you will have a test corpus that reflects the failures your system actually has—not a generic benchmark someone else designed. That is the durable value of replay: each incident makes the next release a little harder to break in the same way.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is AI agent trace replay?
&lt;/h3&gt;

&lt;p&gt;AI agent trace replay reruns a captured agent scenario using recorded or simulated inputs. It is used to reproduce a failure, compare behavior after a change, and make an incident into a regression test without invoking real side effects.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should an agent replay call real tools?
&lt;/h3&gt;

&lt;p&gt;Usually no. Default to recorded fixtures and deny real writes, emails, payments, deployments, and permission changes. Use an isolated sandbox with synthetic data only when the behavior cannot be tested from a recorded response.&lt;/p&gt;

&lt;h3&gt;
  
  
  What should an agent trace contain?
&lt;/h3&gt;

&lt;p&gt;Keep ordered events, component versions, validated tool arguments, result classes, policy decisions, external operation receipts, and the final claim. Redact sensitive values and avoid storing private reasoning or unnecessary prompt content.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do you test a non-deterministic agent with replay?
&lt;/h3&gt;

&lt;p&gt;Test deterministic safety and trajectory rules on every run, then sample multiple model-in-the-loop runs for softer quality signals. Treat a new tool path as a review event rather than silently accepting a new snapshot.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is trace replay the same as agent observability?
&lt;/h3&gt;

&lt;p&gt;No. Observability helps you inspect what happened. Trace replay turns selected evidence from what happened into a controlled test you can run before the next release. Strong systems use both.&lt;/p&gt;

&lt;h3&gt;
  
  
  How many replay fixtures should a team start with?
&lt;/h3&gt;

&lt;p&gt;Start with one high-impact incident and add cases after real failures. A small, maintained incident corpus is more valuable than a large collection of stale traces no one can explain.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>testing</category>
      <category>devops</category>
    </item>
    <item>
      <title>AI Agent Dependency Lockfiles: Stop Silent Tool and Package Drift</title>
      <dc:creator>Jack M</dc:creator>
      <pubDate>Sat, 24 Oct 2026 09:50:09 +0000</pubDate>
      <link>https://dev.to/jackm-singularity/ai-agent-dependency-lockfiles-stop-silent-tool-and-package-drift-1mh6</link>
      <guid>https://dev.to/jackm-singularity/ai-agent-dependency-lockfiles-stop-silent-tool-and-package-drift-1mh6</guid>
      <description>&lt;p&gt;An agent can behave differently on Tuesday even when nobody changed its prompt or application code. A package resolves to a newer release, an MCP server updates a tool description, a model alias moves, or a tool image is rebuilt. The workflow still starts. The tests may still be green. But the agent is no longer the runtime you reviewed.&lt;/p&gt;

&lt;p&gt;That is why a normal &lt;code&gt;package-lock.json&lt;/code&gt; is necessary but incomplete for an AI feature. It pins part of the application, while the agent also depends on models, prompts, tool servers, policy bundles, retrieval configurations, and external packages. Treat those as one dependency graph. Then record the resolved graph in an &lt;strong&gt;AI agent dependency lockfile&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is not a claim that a lockfile makes an agent safe. It gives you a specific, reviewable answer to a much more useful question: &lt;em&gt;what exactly did this agent run with?&lt;/em&gt; That answer makes bad changes easier to spot, reproduce, and roll back.&lt;/p&gt;

&lt;h2&gt;
  
  
  What belongs in an agent dependency lockfile?
&lt;/h2&gt;

&lt;p&gt;Start with dependencies that can change behavior or permissions without changing the business workflow. A useful lockfile records both the requested selector and the resolved identity.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dependency&lt;/th&gt;
&lt;th&gt;Weak reference&lt;/th&gt;
&lt;th&gt;Useful locked identity&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Model&lt;/td&gt;
&lt;td&gt;&lt;code&gt;fast-model&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;provider, model revision or immutable deployment ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prompt&lt;/td&gt;
&lt;td&gt;&lt;code&gt;support-v3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;repository commit and content digest&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MCP tool&lt;/td&gt;
&lt;td&gt;&lt;code&gt;billing-tools:latest&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;image/package digest, server version, tool-schema digest&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Library&lt;/td&gt;
&lt;td&gt;&lt;code&gt;agent-sdk:^4&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;exact package version and integrity hash&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Policy&lt;/td&gt;
&lt;td&gt;&lt;code&gt;default-policy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;policy bundle digest and decision version&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retrieval config&lt;/td&gt;
&lt;td&gt;&lt;code&gt;help-center&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;index revision, embedding model, and chunking config digest&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The point is not to freeze every choice forever. The point is to separate &lt;strong&gt;intent&lt;/strong&gt; from &lt;strong&gt;resolution&lt;/strong&gt;. &lt;code&gt;latest&lt;/code&gt; can remain an input to an update job, but it should not be the only record of what production executed.&lt;/p&gt;

&lt;h3&gt;
  
  
  The trap: configuration looks less risky than code
&lt;/h3&gt;

&lt;p&gt;Teams often review source changes carefully and accept an environment variable such as &lt;code&gt;MODEL=fast-model&lt;/code&gt; or &lt;code&gt;TOOLS_URL=https://tools.internal&lt;/code&gt; as harmless wiring. In an agent system, that wiring can change output quality, tool availability, permissions, latency, or data handling.&lt;/p&gt;

&lt;p&gt;The same issue appears with agent-installed dependencies. If a coding agent adds a package because it recognizes a familiar name, the pull request can hide a new transitive tree behind one import. If an agent fetches a tool server by a mutable tag, an image rebuild can change the runtime after review. Current public guidance on agent supply chains explicitly includes models, agents, libraries, developer tools, and MCP-server packages in scope. The boundary is wider than &lt;code&gt;requirements.txt&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small lockfile format that is easy to review
&lt;/h2&gt;

&lt;p&gt;Use a format your tools can parse and your reviewers can read. JSON is convenient for CI, though YAML or TOML works too. This example deliberately avoids storing credentials and raw customer data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"lockVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"agent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"refund-assistant"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"workflowCommit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"8e9b1f2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"policy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"selector"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"refund-policy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"resolvedDigest"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sha256:6b4d..."&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"selector"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"support-fast"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"example-provider"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"resolvedId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"support-fast-2026-10-18"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"prompts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"prompts/refund.md"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"gitCommit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"8e9b1f2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"sha256"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"f18c..."&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tools"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"get_order"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"serverImage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"registry.example/tools@sha256:a91d..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"serverVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.4.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"schemaSha256"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ce33..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"scopes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"orders.read"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"packages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"agent-sdk"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"4.2.3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"integrity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sha512-..."&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"retrieval"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"indexRevision"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"kb-2026-10-23-04"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"embeddingModel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"embed-small-r17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"chunkerDigest"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sha256:9d70..."&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details matter here. First, the selector preserves the human-facing intent, while the immutable ID or digest preserves what actually resolved. Second, the lockfile captures only references and hashes. A lockfile should never become a second secret store.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generate the file; do not hand-edit it in production
&lt;/h2&gt;

&lt;p&gt;Create one command that resolves inputs, validates them, and writes a canonical lockfile. A minimal TypeScript-style sketch looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ResolvedTool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;serverImage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;schemaSha256&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;scopes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;lockTool&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ResolvedTool&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&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;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolveImmutableImage&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;mcp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inspect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;digest&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;schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;every&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;allowedTools&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="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Unexpected tool exposed by &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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&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="na"&gt;serverImage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;schemaSha256&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;stableJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="na"&gt;scopes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;minimalScopes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;)&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;p&gt;Canonical serialization is important. If equivalent JSON can produce different bytes, a hash diff becomes noise. Sort keys, normalize line endings, and write the generated file deterministically. Commit it next to the workflow definition just as you commit an ordinary package lockfile.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make dependency diffs a release gate
&lt;/h2&gt;

&lt;p&gt;A lockfile only helps if a changed resolution gets attention. In CI, generate a candidate lockfile, compare it with the committed version, and classify the diff.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;th&gt;Default handling&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Patch-level library update&lt;/td&gt;
&lt;td&gt;test and review&lt;/td&gt;
&lt;td&gt;behavior may still change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New MCP tool or expanded scope&lt;/td&gt;
&lt;td&gt;block pending approval&lt;/td&gt;
&lt;td&gt;expands agent capability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool-schema digest changed&lt;/td&gt;
&lt;td&gt;contract tests + approval&lt;/td&gt;
&lt;td&gt;arguments or semantics may drift&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model revision changed&lt;/td&gt;
&lt;td&gt;evaluation suite + staged rollout&lt;/td&gt;
&lt;td&gt;behavior can shift without API errors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Policy digest changed&lt;/td&gt;
&lt;td&gt;policy-owner approval&lt;/td&gt;
&lt;td&gt;decision boundary changed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retrieval index revision&lt;/td&gt;
&lt;td&gt;freshness and regression checks&lt;/td&gt;
&lt;td&gt;evidence may have changed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Avoid a single “lockfile changed” checkbox. A new read-only tool and a tool that can issue refunds are not equivalent. The classifier should turn a mechanical diff into an appropriate review path.&lt;/p&gt;

&lt;p&gt;Here is a shell-shaped CI outline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentctl lock &lt;span class="nt"&gt;--config&lt;/span&gt; agent.yaml &lt;span class="nt"&gt;--out&lt;/span&gt; agent.lock.next.json
agentctl lock verify &lt;span class="nt"&gt;--file&lt;/span&gt; agent.lock.next.json
agentctl lock diff agent.lock.json agent.lock.next.json &lt;span class="nt"&gt;--format&lt;/span&gt; json &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; diff.json
agentctl lock policy-check diff.json
agentctl &lt;span class="nb"&gt;test &lt;/span&gt;contracts &lt;span class="nt"&gt;--lock&lt;/span&gt; agent.lock.next.json
agentctl &lt;span class="nb"&gt;eval &lt;/span&gt;run &lt;span class="nt"&gt;--suite&lt;/span&gt; refund-safety &lt;span class="nt"&gt;--lock&lt;/span&gt; agent.lock.next.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;policy-check&lt;/code&gt; command should fail closed for prohibited changes, such as a mutable image tag, an unapproved registry, a newly write-capable tool, or a missing integrity digest. It should produce a readable report for permitted changes, not merely a pass/fail bit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep resolution separate from activation
&lt;/h2&gt;

&lt;p&gt;Do not let a production process silently resolve new dependencies at startup. Instead, use a two-step path:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Resolve:&lt;/strong&gt; an update job produces a candidate lockfile from declared selectors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate:&lt;/strong&gt; CI runs package scans, tool-contract tests, workflow tests, and representative evaluations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Approve:&lt;/strong&gt; reviewers accept the classified dependency diff.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Activate:&lt;/strong&gt; deployment references the immutable lockfile digest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observe:&lt;/strong&gt; logs attach the lockfile ID to each run; a canary receives a limited traffic slice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Roll back:&lt;/strong&gt; redeploy the prior known-good lockfile if failure signals cross a threshold.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is deliberately less magical than allowing a runtime to “get the newest compatible tool.” Predictable change is the goal. If fast security patches are needed, the same path can be accelerated with an explicit emergency approval and a short retrospective requirement.&lt;/p&gt;

&lt;h3&gt;
  
  
  Record the lockfile ID in operational evidence
&lt;/h3&gt;

&lt;p&gt;Attach &lt;code&gt;agent_lock_digest&lt;/code&gt; to traces, decision records, and evaluation results. When a support team asks why an agent started choosing a different tool, you can group failures by the exact dependency set instead of guessing from deploy timestamps.&lt;/p&gt;

&lt;p&gt;That also makes incident work smaller. You can reproduce an affected run in a sandbox using the workflow commit plus lockfile, then compare it with the previous digest. A release manifest tells you the bundle identity; the dependency lock tells you which mutable ecosystem references became concrete artifacts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common failure modes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Locking a name, not content.&lt;/strong&gt; &lt;code&gt;tools: 2.4.1&lt;/code&gt; is better than &lt;code&gt;latest&lt;/code&gt;, but a package version alone may not identify the exact artifact. Use an integrity value, immutable image digest, or registry-provided content hash where possible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Treating tool schemas as documentation.&lt;/strong&gt; A schema can alter required fields, defaults, descriptions, or side effects while the server name stays the same. Snapshot the inspected schema and run contract tests against it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Locking only dependencies.&lt;/strong&gt; A model alias, prompt file, retrieval index, and policy bundle can each change an agent’s behavior. Include the components that matter to your threat model and debugging path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Making updates impossible.&lt;/strong&gt; A lockfile that nobody refreshes becomes stale theater. Run a scheduled candidate-resolution job, send bounded diffs to owners, and maintain a tested fast lane for security patches.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Logging secrets by accident.&lt;/strong&gt; Store a tool’s allowed scope and identity, not an access token, endpoint query string, or customer payload. Scan generated lockfiles before commit.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical adoption plan
&lt;/h2&gt;

&lt;p&gt;Begin with one agent that has meaningful external effects. Inventory its model, prompt, tool servers, packages, policy, and retrieval configuration. Add immutable IDs for the two or three components most likely to drift. Then make one CI rule visible: any newly write-capable tool requires a human approval.&lt;/p&gt;

&lt;p&gt;After that, add schema snapshots and trace correlation. Only when those pieces work should you expand the lock to every runtime dependency. The artifact should reduce ambiguity for engineers, not create a giant metadata file nobody can review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use the lockfile with, not instead of, an inventory
&lt;/h2&gt;

&lt;p&gt;A lockfile answers “what did this release resolve?” An inventory answers “what agent components exist across the organization?” Keep both. The inventory is useful for ownership, patch notifications, and discovering an unmaintained tool server; the lockfile is useful for exact runtime reproduction. Mixing the two creates a document that is too broad for a deploy and too stale for security operations.&lt;/p&gt;

&lt;p&gt;Think of the lockfile as a release-level snapshot and the inventory as a portfolio-level map. A tool may appear in the inventory with an owner, risk classification, and approved registry. Each agent lockfile then states the immutable tool artifact, schema digest, and scopes it actually used. This distinction gives a security reviewer a short answer during a release review and gives an operations team a larger answer when a vulnerability advisory arrives.&lt;/p&gt;

&lt;p&gt;When a dependency advisory appears, first query the inventory to identify candidate owners and workloads. Next, query deployment records for lockfile digests that reference the affected artifact. That keeps response work evidence-based: do not restart every agent merely because a similarly named package exists somewhere in a repository.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a good review looks like
&lt;/h2&gt;

&lt;p&gt;Reviewers should be able to answer four questions from the pull request: What changed? Which capability or behavior could change? What tests exercised the new resolution? How do we return to the previous known-good digest? Include those answers in the generated diff summary.&lt;/p&gt;

&lt;p&gt;For example, a tool image update with an unchanged schema may need integration tests and a vulnerability scan. A schema diff adding &lt;code&gt;issue_refund&lt;/code&gt; requires a capability-owner approval even if the image has no known CVEs. A new model revision needs representative task evaluation, because its interface can be stable while its decisions move. The lockfile makes these different kinds of changes visible in one place without pretending they carry the same risk.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is an AI agent dependency lockfile different from a package lockfile?
&lt;/h3&gt;

&lt;p&gt;Yes. A package lockfile resolves language dependencies. An agent lockfile extends the same idea to behavior-relevant dependencies such as models, prompts, MCP tools, policies, and retrieval revisions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should every model request use a pinned model version?
&lt;/h3&gt;

&lt;p&gt;For production workflows where repeatability matters, record the resolved deployment or revision identity when your provider exposes one. If only an alias exists, record the alias, provider metadata available at resolution time, and the evaluation evidence used for the rollout.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I version an MCP server safely?
&lt;/h3&gt;

&lt;p&gt;Pin its immutable image or package integrity value, capture the inspected tool-schema digest, test its tool contracts, and require approval when the exposed tools or scopes expand.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can a lockfile prevent a malicious dependency?
&lt;/h3&gt;

&lt;p&gt;No. It helps you know and reproduce what was selected. Combine it with trusted registries, integrity verification, vulnerability scanning, least-privilege credentials, code review, and runtime controls.&lt;/p&gt;

&lt;h3&gt;
  
  
  How often should we refresh an agent lockfile?
&lt;/h3&gt;

&lt;p&gt;Generate a candidate on a predictable cadence and when declared dependencies change. Apply updates only after the appropriate checks; urgency can change the approval speed, not the evidence you retain.&lt;/p&gt;

&lt;h3&gt;
  
  
  What should happen when a lockfile changes during an incident?
&lt;/h3&gt;

&lt;p&gt;Compare the new and previous resolved identities, correlate failures with the lockfile digest, deploy the last known-good lockfile if justified, then preserve the diff and test results for follow-up.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do small teams need this much process?
&lt;/h3&gt;

&lt;p&gt;Start small: pin package and tool identities, commit one generated file, and review capability changes. The same habit scales better than reconstructing a changed agent runtime from scattered environment variables later.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>security</category>
      <category>devops</category>
    </item>
    <item>
      <title>AI Agent Reconciliation: What to Do When a Tool Times Out After It May Have Succeeded</title>
      <dc:creator>Jack M</dc:creator>
      <pubDate>Fri, 23 Oct 2026 03:35:27 +0000</pubDate>
      <link>https://dev.to/jackm-singularity/ai-agent-reconciliation-what-to-do-when-a-tool-times-out-after-it-may-have-succeeded-442d</link>
      <guid>https://dev.to/jackm-singularity/ai-agent-reconciliation-what-to-do-when-a-tool-times-out-after-it-may-have-succeeded-442d</guid>
      <description>&lt;p&gt;A timeout after an agent sends a refund, deploy, booking, or customer message is not a failure. It is an &lt;strong&gt;unknown outcome&lt;/strong&gt;: the remote system may have completed the work after your connection stopped waiting. If the agent retries because it sees the word “timeout,” one intended action can become two real-world actions.&lt;/p&gt;

&lt;p&gt;The fix is not a better prompt that says “be careful.” Put a reconciliation protocol in the tool layer: assign one durable operation key, record the intended effect before dispatch, query for evidence after an ambiguous result, and only retry when the evidence says the effect did not happen. This guide shows the state model, API contracts, and tests that make that rule practical.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a timeout is not a failed action
&lt;/h2&gt;

&lt;p&gt;There are two timelines in every state-changing tool call:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The caller sends a request and waits for an acknowledgement.&lt;/li&gt;
&lt;li&gt;The downstream service receives the request and applies an effect.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A timeout only tells you that timeline one ended without a useful answer. It says nothing conclusive about timeline two. A worker can crash after sending the request. A load balancer can drop the response. A payment service can commit just after the client deadline. In each case, immediately sending the same logical request again can duplicate the effect.&lt;/p&gt;

&lt;p&gt;This matters more for agents than ordinary forms. Agents are designed to recover and continue. They can interpret an error as a reason to retry, call a fallback tool, or resume a durable workflow later. A natural-language instruction cannot be the final authority over a write to an external system.&lt;/p&gt;

&lt;p&gt;Treat these outcomes as separate states:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool result&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;Next move&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;confirmed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The target returned a receipt or authoritative result.&lt;/td&gt;
&lt;td&gt;Continue.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rejected&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The target proved it did not accept the operation.&lt;/td&gt;
&lt;td&gt;Correct input or stop.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;unknown&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A timeout, connection reset, process crash, or malformed acknowledgement occurred.&lt;/td&gt;
&lt;td&gt;Reconcile; do not retry.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;not_found&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Reconciliation can prove the effect did not land.&lt;/td&gt;
&lt;td&gt;Retry with the same operation key if safe.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;conflict&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Evidence is incomplete or contradictory.&lt;/td&gt;
&lt;td&gt;Escalate; do not guess.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The important design choice is exposing &lt;code&gt;unknown&lt;/code&gt; as a first-class outcome. Do not translate it into &lt;code&gt;false&lt;/code&gt;, throw a generic exception, or let the model decide what it means.&lt;/p&gt;

&lt;h2&gt;
  
  
  The narrow problem this solves
&lt;/h2&gt;

&lt;p&gt;Idempotency is necessary, but it is not the whole recovery story. It prevents many duplicate writes when a downstream API accepts an idempotency key. Reconciliation answers a different question: &lt;em&gt;what actually happened to this intended business operation?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That distinction matters when an integration does not support client keys, when a write spans several systems, or when a successful response was lost. It also prevents the opposite bug: an agent tells a customer an action failed even though it succeeded.&lt;/p&gt;

&lt;p&gt;Use this pattern for consequential writes such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;creating a charge, refund, booking, order, ticket, or deployment;&lt;/li&gt;
&lt;li&gt;sending an external email, Slack message, or webhook;&lt;/li&gt;
&lt;li&gt;changing a permission, subscription, or account setting;&lt;/li&gt;
&lt;li&gt;deleting or publishing a customer-visible record.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pure reads can normally retry. A deterministic update such as “set plan to Pro” is safer than “append a note,” but it still needs an operation record if the user needs a truthful outcome.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the application own effect state
&lt;/h2&gt;

&lt;p&gt;The model should propose an action. Your application should own its identity, authorization, execution, and recovery. A minimal operation ledger looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;agent_operations&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;operation_id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;tenant_id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;run_id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;tool_name&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;request_hash&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;state&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="k"&gt;check&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'prepared'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'dispatched'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'confirmed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'unknown'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'reconciling'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'not_found'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'escalated'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'cancelled'&lt;/span&gt;
  &lt;span class="p"&gt;)),&lt;/span&gt;
  &lt;span class="n"&gt;provider_reference&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;receipt&lt;/span&gt; &lt;span class="n"&gt;jsonb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;reconcile_after&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="k"&gt;unique&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;operation_id&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;p&gt;&lt;code&gt;operation_id&lt;/code&gt; identifies one intended business effect, not one HTTP attempt. Store a hash of the normalized request too. If a resumed agent presents the same operation ID with different arguments, reject it. That is a serious programming or orchestration error, not a request to “try again.”&lt;/p&gt;

&lt;p&gt;Create the row before making the external call. Otherwise, a crash in the gap between dispatch and persistence recreates the very ambiguity you are trying to control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design tool contracts for honest recovery
&lt;/h2&gt;

&lt;p&gt;An agent-facing write tool should not return a raw network exception. Return a small structured result that forces the workflow into a safe branch.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;EffectResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;confirmed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;operationId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;receipt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rejected&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;operationId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;operationId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;retryAllowed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;nextCheckAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;escalated&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;operationId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The wrapper receives a stable &lt;code&gt;operationId&lt;/code&gt;, writes it to the ledger, and passes it downstream as an idempotency header or external reference whenever possible.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sendRefund&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RefundInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;operationId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;EffectResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;operations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;operationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;try&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;billing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;refund&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;operationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;agent_operation_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;operationId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;operations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;confirm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;operationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;confirmed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;operationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;receipt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;refundId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&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="nf"&gt;isDefiniteRejection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;operations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;operationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;safeReason&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rejected&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;operationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;safeReason&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;operations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;markUnknown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;operationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;operationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;retryAllowed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;nextCheckAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;};&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;p&gt;Notice what is missing: there is no &lt;code&gt;catch { retry() }&lt;/code&gt;. The tool owns transport uncertainty so the agent never treats an unconfirmed write as a fresh task.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reconcile before any retry
&lt;/h2&gt;

&lt;p&gt;Reconciliation is a targeted read of an authoritative system of record. The best API pair is a write that accepts your client-generated key and a read such as &lt;code&gt;GET /refunds?operation_id=...&lt;/code&gt;. That read should return the effect’s lifecycle state, not merely recent activity that happens to look similar.&lt;/p&gt;

&lt;p&gt;Use this decision flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;prepare operation → dispatch write → acknowledgement?
                                 ├─ yes → confirm receipt → continue
                                 └─ no  → mark unknown → query by operation ID
                                                     ├─ found → confirm
                                                     ├─ absent after safe window → retry same key
                                                     └─ partial/ambiguous → escalate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The “safe window” is integration-specific. A provider with eventual consistency may need several read attempts with exponential backoff before absence means anything. Record the policy per tool: maximum reconciliation delay, available lookup key, whether a retry is safe, and who owns escalation.&lt;/p&gt;

&lt;p&gt;For systems that cannot query by a client key, look for a compound business identity: a draft ID, customer ID plus immutable request nonce, or a provider correlation field. If you cannot reliably identify the effect, the correct automated outcome is &lt;code&gt;escalated&lt;/code&gt;, not a duplicate write disguised as resilience.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do not confuse a similar record with proof
&lt;/h3&gt;

&lt;p&gt;A tempting shortcut is to search for “a recent refund for this customer” or “a message with roughly this text.” That can turn reconciliation into a new source of bad decisions. A customer may have made two valid requests, an operator may have sent the message manually, or a background job may have changed the same record.&lt;/p&gt;

&lt;p&gt;Define the minimum evidence needed to confirm every integration. Prefer a provider receipt that carries the operation key. If that is unavailable, require a combination of immutable business ID, normalized request hash, target identity, and a time bound that is documented by the provider. Mark the result as &lt;code&gt;conflict&lt;/code&gt; when evidence is merely plausible rather than conclusive.&lt;/p&gt;

&lt;p&gt;This is also where tenant boundaries matter. An operation lookup must be scoped by the requesting tenant before its evidence reaches an agent or reviewer. A global search for a matching customer email can accidentally confirm another tenant’s action. Keep the target reference, hashes, and receipts in application-owned storage, and authorize reconciliation reads exactly as you authorize the original write.&lt;/p&gt;

&lt;h3&gt;
  
  
  Give each operation a small state machine
&lt;/h3&gt;

&lt;p&gt;Do not let every caller update the ledger with arbitrary strings. Define allowed transitions such as &lt;code&gt;prepared → dispatched → confirmed&lt;/code&gt;, &lt;code&gt;dispatched → unknown → reconciling → confirmed&lt;/code&gt;, and &lt;code&gt;unknown → reconciling → escalated&lt;/code&gt;. Disallow &lt;code&gt;confirmed → dispatched&lt;/code&gt; entirely. The guard belongs in the database transaction or service method, not in the prompt or UI.&lt;/p&gt;

&lt;p&gt;This protects against races between a late provider webhook, a queue retry, and a human reviewer. When a webhook supplies a valid receipt after the reconciliation job has started, it can safely claim &lt;code&gt;confirmed&lt;/code&gt;; the job then sees the terminal state and exits. When a reviewer declines an uncertain deletion, that decision becomes a terminal &lt;code&gt;cancelled&lt;/code&gt; record rather than an instruction an agent can reinterpret later.&lt;/p&gt;

&lt;p&gt;Include an optimistic version or row lock on transitions, and append an immutable event for each transition. The current state makes the workflow fast to query; the event history explains why it reached that state. This is especially valuable during incident review, where you need to distinguish “the provider acted late” from “two workers independently dispatched the same effect.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Use a reconciliation queue, not a blocked agent turn
&lt;/h2&gt;

&lt;p&gt;Avoid holding a model session open while waiting for a slow target. When a wrapper returns &lt;code&gt;unknown&lt;/code&gt;, enqueue a durable reconciliation job keyed by &lt;code&gt;operation_id&lt;/code&gt;. That job should lock the operation, perform a bounded read-back sequence, and write one terminal state.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;reconcile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;operationId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&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;op&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;operations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lockForReconciliation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;operationId&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;confirmed&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;rejected&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;escalated&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;op&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&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;evidence&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findByOperationId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;op&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;operation_id&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="nx"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;found&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;operations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;confirm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;op&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;operation_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;evidence&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="nx"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;not_found&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;canRetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;op&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;operations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;markNotFound&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;op&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;operation_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;replay_operation&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;operationId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;op&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;operation_id&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;operations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;escalate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;op&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;operation_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reason&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;p&gt;The replay uses the same operation ID and the same normalized request. Do not restart the whole agent run; that can repeat unrelated successful steps. If a workflow includes five writes, reconcile each effect independently and allow downstream steps only after their prerequisites are confirmed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add evidence, boundaries, and human decisions
&lt;/h2&gt;

&lt;p&gt;An operations table without useful evidence becomes a second mystery. Store redacted dispatch time, provider request ID, response status, error class, attempt count, reconciliation reads, and final receipt. Never store secrets or unredacted customer content merely to debug retries.&lt;/p&gt;

&lt;p&gt;Classify each tool in a policy registry:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Action class&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;th&gt;On unknown outcome&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Safe replay&lt;/td&gt;
&lt;td&gt;Set a named record to a known value&lt;/td&gt;
&lt;td&gt;Reconcile, then replay same key.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dedupe-supported&lt;/td&gt;
&lt;td&gt;Refund API with idempotency key&lt;/td&gt;
&lt;td&gt;Reconcile; same key may be replayed.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compensatable&lt;/td&gt;
&lt;td&gt;Create ticket then update local state&lt;/td&gt;
&lt;td&gt;Reconcile; compensate only with evidence.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Irreversible&lt;/td&gt;
&lt;td&gt;Send a legal notice or delete a record&lt;/td&gt;
&lt;td&gt;Reconcile, then human review.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Require an approval or incident handoff for conflicts involving money, irreversible deletion, permissions, or customer communications. The reviewer needs the intended request, operation ID, timeline, evidence, and choices—not hidden chain-of-thought or a vague “agent failed” message.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the failures that happy paths hide
&lt;/h2&gt;

&lt;p&gt;Your integration tests should simulate a target that commits the effect and drops the response. That is the bug that ordinary mock tests miss.&lt;/p&gt;

&lt;p&gt;At minimum, test these invariants:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the same operation ID with changed input is rejected;&lt;/li&gt;
&lt;li&gt;a timeout produces &lt;code&gt;unknown&lt;/code&gt;, never automatic replay;&lt;/li&gt;
&lt;li&gt;a reconciliation read that finds a receipt reaches &lt;code&gt;confirmed&lt;/code&gt; without a second dispatch;&lt;/li&gt;
&lt;li&gt;an absent result only enables retry after the configured consistency window;&lt;/li&gt;
&lt;li&gt;two workers reconciling the same operation produce one terminal state;&lt;/li&gt;
&lt;li&gt;a resumed workflow uses existing operations rather than creating new ones;&lt;/li&gt;
&lt;li&gt;escalation contains redacted evidence and no model reasoning trace.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a simple test double, let &lt;code&gt;POST /refunds&lt;/code&gt; persist a refund and then throw a timeout. Run the wrapper twice. The expected result is one refund record, one operation ID, and a second call that returns the original receipt or waits for reconciliation.&lt;/p&gt;

&lt;p&gt;Track unknown-outcome rate by tool, time-to-reconciliation, escalations, duplicate effects detected after the fact, and disagreements between an agent’s message and the reconciled state. Those metrics reveal fragile integrations before a rare timeout becomes a customer incident.&lt;/p&gt;

&lt;h2&gt;
  
  
  A rollout plan that does not freeze delivery
&lt;/h2&gt;

&lt;p&gt;Start with one high-impact tool rather than wrapping every integration at once.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Inventory state-changing tools and rank them by irreversibility and customer impact.&lt;/li&gt;
&lt;li&gt;Add operation IDs and ledger rows in observe-only mode; do not change retry behavior yet.&lt;/li&gt;
&lt;li&gt;Capture whether each provider supports idempotency keys, lookup by key, and reliable receipts.&lt;/li&gt;
&lt;li&gt;Switch the selected tool so ambiguous outcomes become &lt;code&gt;unknown&lt;/code&gt; and enter reconciliation.&lt;/li&gt;
&lt;li&gt;Add alerts for operations stuck past the tool’s expected window.&lt;/li&gt;
&lt;li&gt;Run timeout, crash, duplicate-delivery, and delayed-consistency drills before expanding coverage.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This sequence produces evidence before policy changes. It also makes conversations with API owners concrete: ask for a client-generated key, a lookup endpoint, and a receipt—not a promise that timeouts are rare.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is an AI agent timeout the same as a failed tool call?
&lt;/h3&gt;

&lt;p&gt;No. A timeout only means the caller did not receive a conclusive response. The target may have applied the effect, so a state-changing call should enter an unknown-outcome reconciliation path.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is an operation key for an AI agent?
&lt;/h3&gt;

&lt;p&gt;It is a durable ID for one intended business effect. Reuse it across attempts, send it to the target when possible, and use it to find the effect during reconciliation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Are idempotency keys enough for agent retries?
&lt;/h3&gt;

&lt;p&gt;They are the best protection when every target honors them, but you still need a ledger and read-back path to report the outcome honestly and handle APIs without key support.&lt;/p&gt;

&lt;h3&gt;
  
  
  When can an agent retry a timed-out action?
&lt;/h3&gt;

&lt;p&gt;Only after a provider receipt or authoritative lookup proves the effect did not happen, or when the action is intrinsically safe to replay. Use the same operation key, not a new one.&lt;/p&gt;

&lt;h3&gt;
  
  
  What should happen when reconciliation cannot determine the result?
&lt;/h3&gt;

&lt;p&gt;Stop automatic execution and escalate with a redacted operation timeline. Guessing can create a duplicate charge, message, deletion, or permission change.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should the language model decide whether an action succeeded?
&lt;/h3&gt;

&lt;p&gt;No. Let the model propose the action and explain the user-facing result, but derive success from deterministic receipts and reconciled system state.&lt;/p&gt;

&lt;h3&gt;
  
  
  How is reconciliation different from a workflow retry?
&lt;/h3&gt;

&lt;p&gt;A workflow retry repeats a task. Reconciliation checks whether the original business effect happened and chooses confirm, safe replay, compensation, or escalation for that one operation.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>tutorial</category>
      <category>devops</category>
    </item>
    <item>
      <title>MCP Schema Versioning: Keep AI Tool Calls Working Through API Changes</title>
      <dc:creator>Jack M</dc:creator>
      <pubDate>Thu, 22 Oct 2026 03:29:41 +0000</pubDate>
      <link>https://dev.to/jackm-singularity/mcp-schema-versioning-keep-ai-tool-calls-working-through-api-changes-2j9n</link>
      <guid>https://dev.to/jackm-singularity/mcp-schema-versioning-keep-ai-tool-calls-working-through-api-changes-2j9n</guid>
      <description>&lt;p&gt;An agent that worked yesterday can start failing after a change that looks harmless in a pull request: rename &lt;code&gt;location&lt;/code&gt; to &lt;code&gt;query&lt;/code&gt;, make a field required, or reshape a result object. The model has not suddenly become worse. It is still following the tool contract it saw before the deployment.&lt;/p&gt;

&lt;p&gt;That is why MCP schema versioning needs the same care as a public API migration. Tool names, descriptions, input JSON Schema, output shape, and runtime behavior all influence an agent's next action. A server can successfully deploy while a cached client, a long-running workflow, or a prompt template continues to make yesterday's call.&lt;/p&gt;

&lt;p&gt;This guide shows a practical way to evolve MCP tools without turning routine backend work into mysterious agent failures. The goal is not to freeze every tool forever. It is to make changes visible, compatible when possible, and reversible when they are not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate protocol versions from tool-schema versions
&lt;/h2&gt;

&lt;p&gt;MCP has protocol-version compatibility rules. Clients and servers use those rules to agree on the protocol revision they speak. That does &lt;strong&gt;not&lt;/strong&gt; automatically version your application tools.&lt;/p&gt;

&lt;p&gt;For example, both of these servers might support the same MCP protocol revision:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Yesterday&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"find_customer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"inputSchema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Today&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"find_customer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"inputSchema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"tenant_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tenant_id"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The protocol can be perfectly compatible while old agent calls now fail validation. Treat the tool schema as its own public contract. Give it an owner, a baseline, a change review, and a migration path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Know which changes break agents
&lt;/h2&gt;

&lt;p&gt;Start every tool change by asking a narrow question: can a call that was valid yesterday be rejected or mean something different today?&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;th&gt;Compatibility&lt;/th&gt;
&lt;th&gt;Safe response&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Add an optional input with a default&lt;/td&gt;
&lt;td&gt;Usually compatible&lt;/td&gt;
&lt;td&gt;Release normally; snapshot the change.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Add a new tool&lt;/td&gt;
&lt;td&gt;Usually compatible&lt;/td&gt;
&lt;td&gt;Keep existing tools stable and test tool selection.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Add a required input&lt;/td&gt;
&lt;td&gt;Breaking&lt;/td&gt;
&lt;td&gt;Keep the old input contract or introduce a versioned tool.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rename/remove an input&lt;/td&gt;
&lt;td&gt;Breaking&lt;/td&gt;
&lt;td&gt;Accept the old name during a deprecation window.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Narrow a type or enum&lt;/td&gt;
&lt;td&gt;Breaking&lt;/td&gt;
&lt;td&gt;Preserve old values or map them explicitly.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rename/remove an output field&lt;/td&gt;
&lt;td&gt;Often silently breaking&lt;/td&gt;
&lt;td&gt;Return both fields until clients migrate.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Change a description&lt;/td&gt;
&lt;td&gt;Behavior-changing&lt;/td&gt;
&lt;td&gt;Review it like code; models use descriptions to choose tools.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keep the schema but change the business meaning&lt;/td&gt;
&lt;td&gt;Breaking&lt;/td&gt;
&lt;td&gt;Publish a new contract and require a migration.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The last two are easy to miss. A description is not merely documentation when it enters an agent's context. And a JSON Schema diff cannot see that &lt;code&gt;status: "complete"&lt;/code&gt; now means “queued” instead of “executed.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Make dynamic discovery a controlled deployment boundary
&lt;/h2&gt;

&lt;p&gt;MCP makes tool discovery convenient, but convenience can hide a release. If an agent fetches a live &lt;code&gt;tools/list&lt;/code&gt; response on every connection, an upstream definition can change between two user sessions without a code deployment in the agent service.&lt;/p&gt;

&lt;p&gt;Choose deliberately which of these modes each integration uses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pinned:&lt;/strong&gt; the agent uses an approved tool manifest for its release. This is the safest default for write-capable tools.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitored dynamic:&lt;/strong&gt; the agent discovers live tools, but a fingerprint diff creates an alert and can disable a changed high-risk tool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fully dynamic:&lt;/strong&gt; the agent accepts current definitions immediately. Reserve this for low-risk, read-only experimentation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pinning does not mean copying a tool implementation. It means recording the interface your agent was evaluated against. Refresh that manifest through the same review process as an SDK upgrade. For dynamic integrations, record the discovered fingerprint with the call trace; otherwise, a later investigation cannot distinguish a prompt regression from an upstream contract change.&lt;/p&gt;

&lt;p&gt;This boundary also helps with tool selection. Adding ten unrelated tools may not invalidate any JSON payload, yet it can change which tool the model chooses. Keep the exposed tool set small per workflow, and run a representative selection test whenever the manifest changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build a schema snapshot before you need it
&lt;/h2&gt;

&lt;p&gt;Do not discover the old contract by reading a production incident. In CI, call &lt;code&gt;tools/list&lt;/code&gt;, normalize the result, and commit a snapshot for every supported server and environment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createHash&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:crypto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;stableToolFingerprint&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;unknown&lt;/span&gt;&lt;span class="p"&gt;)&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;canonical&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&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="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;sort&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;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;canonical&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&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;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&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="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&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="na"&gt;fingerprint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;stableToolFingerprint&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;name&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="na"&gt;description&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;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;inputSchema&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;inputSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;outputSchema&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;outputSchema&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;p&gt;In real code, use a recursive canonical JSON serializer; sorting only top-level keys is not sufficient. Keep two artifacts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A readable JSON file for review.&lt;/li&gt;
&lt;li&gt;A fingerprint manifest for a fast deploy check.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Avoid storing credentials, tenant examples, or live tool results in the snapshot. The contract should be enough to detect a change without copying sensitive data into the repository.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diff by risk, not just by bytes
&lt;/h2&gt;

&lt;p&gt;A byte diff says that something changed. A compatibility classifier says whether it can break work.&lt;/p&gt;

&lt;p&gt;Your classifier should flag at least these conditions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a tool disappeared or was renamed;&lt;/li&gt;
&lt;li&gt;a property was removed or became required;&lt;/li&gt;
&lt;li&gt;a type, numeric range, regex, or enum narrowed;&lt;/li&gt;
&lt;li&gt;a default value changed;&lt;/li&gt;
&lt;li&gt;an output property disappeared or changed type;&lt;/li&gt;
&lt;li&gt;a description changed for a high-impact tool;&lt;/li&gt;
&lt;li&gt;an authorization, side-effect, or idempotency expectation changed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Put the result in the pull request. A useful summary is more actionable than a large JSON patch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BREAKING: find_customer now requires tenant_id
COMPATIBLE: list_invoices gained optional page_size (default 50)
REVIEW: refund_payment description changed; execution remains side-effecting
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For tools that create records, move money, send messages, or alter infrastructure, make a breaking diff block the release until an approved migration plan is attached.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prefer adapters over abrupt replacements
&lt;/h2&gt;

&lt;p&gt;Suppose &lt;code&gt;location&lt;/code&gt; is no longer precise enough and your API now needs latitude and longitude. Do not immediately force every agent to learn the new fields. Accept the old call, translate it, and emit a deprecation signal that your logs can count.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;WeatherArgs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;c&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;f&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;lng&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;c&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;f&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;normalizeWeatherArgs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WeatherArgs&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;location&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;legacy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;place&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;unit&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;c&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;current&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;lng&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lng&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;unit&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;c&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&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;p&gt;The adapter is a temporary compatibility boundary, not permanent clutter. Give it a removal date and measure use of the legacy branch. If old calls are still common, find out where they originate: a scheduled workflow, a prompt stored in a database, a client that caches tools, or an integration you do not control.&lt;/p&gt;

&lt;p&gt;When the semantics truly differ, use a new tool name such as &lt;code&gt;get_weather_by_coordinates&lt;/code&gt; rather than overloading the old name. An agent can reason about two clear options better than one tool with surprising behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Version outputs as carefully as inputs
&lt;/h2&gt;

&lt;p&gt;Input errors usually fail loudly. Output changes often fail silently.&lt;/p&gt;

&lt;p&gt;Imagine a tool used to return this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"ticket_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"T-42"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then a refactor changes it to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"accepted"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"job_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"J-96"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An agent may interpret missing &lt;code&gt;ok&lt;/code&gt; as success, retry unnecessarily, or tell the user a task is complete before it runs. During a transition, return an envelope that preserves the old fields while introducing the new ones:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ticket_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"T-42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"accepted"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"job_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"J-96"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"deprecation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"fields"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ticket_id"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"remove_after"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"release-2027.03"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use a release identifier or a policy window in that metadata, rather than a vague promise to remove it “soon.” Validate that old and new fields always agree while both exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test three layers of the contract
&lt;/h2&gt;

&lt;p&gt;Schema validation alone is necessary but not enough. Add three test layers to each MCP tool.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Snapshot tests
&lt;/h3&gt;

&lt;p&gt;Compare &lt;code&gt;tools/list&lt;/code&gt; against the approved manifest. This catches accidental schema regeneration after a Pydantic, Zod, OpenAPI, or SDK upgrade.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Compatibility fixtures
&lt;/h3&gt;

&lt;p&gt;Run old valid payloads and current payloads through the handler. The old fixture should either succeed through the adapter or fail with an explicit, actionable migration error. Never let an old call silently map to a different action.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Behavioral contract tests
&lt;/h3&gt;

&lt;p&gt;Assert the promise that JSON Schema cannot express: a read tool does not write, a dry run does not execute, a retry does not duplicate a charge, and a returned &lt;code&gt;accepted&lt;/code&gt; state does not mean &lt;code&gt;completed&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;keeps legacy lookup behavior during migration&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;callTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;find_customer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;a@example.test&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isError&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;structuredContent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toMatchObject&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;a@example.test&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;structuredContent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;deprecation&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBeDefined&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;p&gt;Use synthetic tenants and fake side-effect adapters for these tests. Contract tests should be safe enough to run on every pull request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Roll out schema changes in stages
&lt;/h2&gt;

&lt;p&gt;A careful rollout makes the blast radius small and observable.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Inventory consumers.&lt;/strong&gt; Find interactive agents, scheduled jobs, API clients, prompt templates, and external integrations that call the tool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ship additive support.&lt;/strong&gt; Add the new shape or new tool while the old contract remains valid.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Release the snapshot and fixtures.&lt;/strong&gt; Make the intended diff visible in code review.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Canary by tenant or workflow.&lt;/strong&gt; Send a limited, reversible population to the new contract.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observe tool errors and outcome errors.&lt;/strong&gt; Track validation failures, fallback use, retries, and task completion—not only HTTP status.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Notify and deprecate.&lt;/strong&gt; State the replacement, an example payload, and the removal condition.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remove only after evidence.&lt;/strong&gt; Delete the adapter after legacy traffic stays at zero for the defined window.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Keep a server build identifier and tool-manifest fingerprint in every trace. When a failure appears, this lets an engineer answer “which contract did the agent see?” without reconstructing a deployment timeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small release checklist
&lt;/h2&gt;

&lt;p&gt;Before merging an MCP tool change, answer these questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does any formerly valid request now fail or mean something different?&lt;/li&gt;
&lt;li&gt;Did input, output, description, side effect, authorization, or idempotency change?&lt;/li&gt;
&lt;li&gt;Is the schema snapshot updated and classified by compatibility risk?&lt;/li&gt;
&lt;li&gt;Do old and new fixtures run in CI?&lt;/li&gt;
&lt;li&gt;Is there an adapter, a new tool name, or an explicit migration error?&lt;/li&gt;
&lt;li&gt;Can the change be canaried and rolled back without deleting state?&lt;/li&gt;
&lt;li&gt;Is there an owner and a measurable removal condition for deprecated behavior?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If any answer is “no,” the tool is not ready for a routine deployment. That is not bureaucracy. It is how you prevent an API change from masquerading as an agent reliability problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is MCP schema versioning?
&lt;/h3&gt;

&lt;p&gt;MCP schema versioning is the discipline of tracking and evolving an MCP tool's name, description, input schema, output shape, and behavior without breaking agents that use an older contract. It is separate from the MCP protocol version negotiated by clients and servers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is adding a required MCP tool parameter a breaking change?
&lt;/h3&gt;

&lt;p&gt;Yes. Any older call that omits the new parameter can fail validation. Prefer an optional field with a safe default, a compatibility adapter, or a new tool name when the new information is essential.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should MCP tool descriptions be versioned too?
&lt;/h3&gt;

&lt;p&gt;Yes. Agents use descriptions to select tools and construct arguments, so a description change can alter behavior even if the JSON Schema is identical. Snapshot descriptions and require review for changes to sensitive or side-effecting tools.&lt;/p&gt;

&lt;h3&gt;
  
  
  How long should an MCP schema deprecation window last?
&lt;/h3&gt;

&lt;p&gt;Base it on observed legacy traffic and the longest-running consumer in your system. Define a minimum window, publish a removal condition, and remove the adapter only after telemetry shows the old shape is unused for that period.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can JSON Schema tests catch every breaking agent change?
&lt;/h3&gt;

&lt;p&gt;No. They catch structural differences, but not changed business meaning, tool-selection behavior, authorization assumptions, or unwanted side effects. Pair schema snapshots with compatibility fixtures and behavioral contract tests.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I create a new MCP tool instead of changing an old one?
&lt;/h3&gt;

&lt;p&gt;Create a new tool when the input or output semantics change materially. For a small additive change, keep the existing tool and use an optional field. A clearly named second tool is safer than one overloaded tool whose old calls now do something surprising.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>api</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>AI Code Invariant Tests: Catch Regressions That Green Test Suites Miss</title>
      <dc:creator>Jack M</dc:creator>
      <pubDate>Wed, 21 Oct 2026 03:31:59 +0000</pubDate>
      <link>https://dev.to/jackm-singularity/ai-code-invariant-tests-catch-regressions-that-green-test-suites-miss-3375</link>
      <guid>https://dev.to/jackm-singularity/ai-code-invariant-tests-catch-regressions-that-green-test-suites-miss-3375</guid>
      <description>&lt;p&gt;An AI coding agent can make every visible test pass and still break the one rule your product cannot afford to violate.&lt;/p&gt;

&lt;p&gt;That is the uncomfortable failure mode behind many “safe” agent refactors. The diff looks tidy. Unit tests are green. The new code may even have more coverage than before. But a permission check moves after a database lookup, a retry stops being idempotent, or a list endpoint turns into an accidental N+1 query. Nothing in a happy-path test has to notice.&lt;/p&gt;

&lt;p&gt;The fix is not a longer prompt asking the agent to be careful. It is to name the behavior that must remain true, then test it as an &lt;strong&gt;invariant&lt;/strong&gt;. This article shows a practical, framework-neutral way to add invariant tests around AI-generated changes—especially where money, access, state, and latency matter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why green tests can still hide an AI-generated regression
&lt;/h2&gt;

&lt;p&gt;Most unit tests answer a narrow question: “Does this example produce the expected output?” That is useful, but it leaves room for an implementation that passes known examples while breaking a rule between examples.&lt;/p&gt;

&lt;p&gt;Coding agents amplify that gap. They are effective at repairing the assertion directly in front of them. If a refactor changes a test, they may update the test too. The suite turns green, but the original contract has quietly moved.&lt;/p&gt;

&lt;p&gt;Recent developer discussions around agentic refactoring repeatedly describe the same pain: tests can remain green while an agent weakens an alert, rewrites an assertion, or creates a slow query loop. Research and benchmark work on coding agents also points toward specification-first and adversarial validation rather than trusting generated code by default.&lt;/p&gt;

&lt;p&gt;An invariant is different from a single expected result. It is a property that must hold across many inputs and implementation paths.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Ordinary example test&lt;/th&gt;
&lt;th&gt;Invariant test&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A member can read their own project.&lt;/td&gt;
&lt;td&gt;A request can never return a project from another organization.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retrying this request returns &lt;code&gt;200&lt;/code&gt;.&lt;/td&gt;
&lt;td&gt;Retrying the same operation never creates a second charge.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A search result is sorted in this fixture.&lt;/td&gt;
&lt;td&gt;Results are always sorted by the promised key, including ties and empty input.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;This endpoint completes quickly in a test.&lt;/td&gt;
&lt;td&gt;Loading 50 records does not issue more than a bounded number of queries.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The aim is not to replace example tests. Keep them. Invariants give them a harder-to-game companion.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pick invariants before asking an agent to refactor
&lt;/h2&gt;

&lt;p&gt;Do not begin with “clean up this module.” Start with a short, testable list of things that cannot change. This becomes the acceptance contract for the task and a small map for review.&lt;/p&gt;

&lt;p&gt;For a typical application, useful categories are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Authorization:&lt;/strong&gt; one tenant or role never sees another’s data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State:&lt;/strong&gt; records move only through allowed states.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Money and side effects:&lt;/strong&gt; retries do not duplicate emails, charges, or writes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data integrity:&lt;/strong&gt; identifiers, totals, and ownership stay consistent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reliability:&lt;/strong&gt; failures leave the system in a recoverable state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance:&lt;/strong&gt; a refactor does not multiply database calls or network work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compatibility:&lt;/strong&gt; a public API preserves its documented shape and meaning.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Write each one in plain language first. A good invariant has a subject, a boundary, and an observable result:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;For every invoice event with the same idempotency key, at most one payment record exists.&lt;/p&gt;

&lt;p&gt;For every authenticated request, every returned document belongs to the requester’s organization.&lt;/p&gt;

&lt;p&gt;For a valid workflow, terminal states cannot transition back to &lt;code&gt;pending&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Avoid vague rules such as “keep the code secure” or “do not change behavior.” They are review intentions, not executable claims.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turn an invariant into a test oracle
&lt;/h2&gt;

&lt;p&gt;An oracle is the thing that says whether behavior is correct. Existing snapshot tests are weak oracles when an agent can update them. For critical changes, use an oracle the change is not allowed to edit without an explicit human decision.&lt;/p&gt;

&lt;p&gt;One practical pattern is to put invariant tests in a protected directory and declare them read-only for the task.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;repo/
  src/
  tests/
  safety-contracts/
    organization-isolation.spec.ts
    payment-idempotency.spec.ts
    workflow-transitions.spec.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your task brief can then be concrete:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Refactor src/billing/renewal.ts to remove duplication.

Constraints:
- Do not edit safety-contracts/.
- Run npm test and npm run test:contracts after each logical step.
- Preserve one payment per idempotency key.
- Report changed files, commands, and any failing contract.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That instruction alone is not protection. Enforce it in CI: fail a pull request that changes protected contract files unless a designated reviewer approves it. The important part is separation—an agent should not be able to write both the implementation and the only proof it is safe without making that visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example: protect tenant isolation with generated inputs
&lt;/h2&gt;

&lt;p&gt;Imagine a document query that an agent is about to “simplify.” A normal example test might check that one user can fetch a document. An invariant test should try many organizations and document IDs, then assert that every returned record belongs to the caller’s organization.&lt;/p&gt;

&lt;p&gt;Here is TypeScript-style pseudocode using a property-testing library:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;fc&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fast-check&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;never returns another organization's document&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;fc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asyncProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;fc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="nx"&gt;fc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="nx"&gt;fc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requesterOrgId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;otherOrgId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;documentId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;fc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pre&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requesterOrgId&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;otherOrgId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;seedDocument&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;documentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;organizationId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;otherOrgId&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;listDocuments&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;organizationId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;requesterOrgId&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;every&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;organizationId&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;requesterOrgId&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;),&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;p&gt;The exact library is optional. You can loop over generated cases yourself. The principle matters: test the rule across values the author did not hand-pick.&lt;/p&gt;

&lt;p&gt;For a multi-tenant system, add negative cases too:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a guessed ID from another organization&lt;/li&gt;
&lt;li&gt;an empty role list&lt;/li&gt;
&lt;li&gt;an admin role from the wrong organization&lt;/li&gt;
&lt;li&gt;soft-deleted records&lt;/li&gt;
&lt;li&gt;pagination cursors created by another tenant&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are the cases a polished refactor often fails because the happy path still works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example: protect idempotency when an agent changes retries
&lt;/h2&gt;

&lt;p&gt;Retries are a classic regression surface. An agent may correctly add timeout handling but accidentally put the side effect before the deduplication check.&lt;/p&gt;

&lt;p&gt;The invariant is simple: repeating a request with the same stable key produces one durable effect.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;creates at most one payment for the same request key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&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;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;payment:order-42&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nf"&gt;createPayment&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;order-42&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nf"&gt;createPayment&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;order-42&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="p"&gt;}),&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;payments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payments&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveLength&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;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payments&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2500&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;p&gt;Run it with sequential and concurrent calls. Then add a failure injection between “reserve key” and “write payment.” If the invariant cannot survive a timeout, it is not really an invariant—it is a best-case assumption.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test state machines instead of only endpoints
&lt;/h2&gt;

&lt;p&gt;Many business rules are state transitions disguised as API behavior. A support ticket should not reopen after it is permanently closed. An order should not ship before payment. A deleted user should not receive a new session.&lt;/p&gt;

&lt;p&gt;Model the allowed transitions in one small table before the refactor:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;From&lt;/th&gt;
&lt;th&gt;Allowed next states&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;draft&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;submitted&lt;/code&gt;, &lt;code&gt;cancelled&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;submitted&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;approved&lt;/code&gt;, &lt;code&gt;rejected&lt;/code&gt;, &lt;code&gt;cancelled&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;approved&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;fulfilled&lt;/code&gt;, &lt;code&gt;cancelled&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fulfilled&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Then test every pair, not just the common routes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;draft&lt;/span&gt;&lt;span class="p"&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;submitted&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;cancelled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;submitted&lt;/span&gt;&lt;span class="p"&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;approved&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;rejected&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;cancelled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;approved&lt;/span&gt;&lt;span class="p"&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;fulfilled&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;cancelled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;rejected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="na"&gt;cancelled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="na"&gt;fulfilled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&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="k"&gt;from&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Status&lt;/span&gt;&lt;span class="p"&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;to&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;allStatuses&lt;/span&gt;&lt;span class="p"&gt;)&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;canMove&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;changeStatus&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resolves&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;canMove&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;toBeDefined&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;toThrow&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]();&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;p&gt;Use your test framework’s normal assertion style; the point is to enumerate the system’s real state space. If the agent replaces conditionals, extracts services, or switches an ORM call, this test protects the business meaning rather than its old implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add performance invariants where refactors hide cost
&lt;/h2&gt;

&lt;p&gt;Correct output is not the whole contract. A coding agent can preserve results while turning one query into one query per row.&lt;/p&gt;

&lt;p&gt;For known hot paths, count the work. Inject a database query counter in an integration test, execute with a small and a medium fixture, and set a reasonable ceiling.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;loads project members without an N+1 query&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&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;meter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;trackDatabaseQueries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;seedProjectWithMembers&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;memberCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&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;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getProjectMembers&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;project-1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;meter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;toBeLessThanOrEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&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;p&gt;Do not turn every test into a brittle microbenchmark. A query ceiling, outbound-call count, or payload-size limit is usually more stable and more useful than an elapsed-millisecond assertion in CI.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small contract loop for AI-assisted changes
&lt;/h2&gt;

&lt;p&gt;Here is a repeatable workflow that does not require a formal-methods team.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Name the risk.&lt;/strong&gt; Identify what can be lost, duplicated, leaked, or slowed down.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write one invariant.&lt;/strong&gt; Make it observable in code or a controlled integration environment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prove the test can fail.&lt;/strong&gt; Temporarily introduce the forbidden behavior or use a mutation tool. If the test stays green, strengthen it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Protect the oracle.&lt;/strong&gt; Keep contract tests out of the agent’s editable scope for the task, or require explicit review for any change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Give the agent a bounded goal.&lt;/strong&gt; Specify files, behavior, test commands, and prohibited changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run the contract suite in CI.&lt;/strong&gt; Store the command output with the pull request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review the invariant, not only the diff.&lt;/strong&gt; Ask whether the rule still means what the product needs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The third step is the one teams skip. A test that never demonstrates a failure can be theater. For a payment invariant, change the uniqueness guard in a local branch and confirm the test catches two rows. For tenant isolation, deliberately remove the organization predicate and watch the contract fail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give coding agents a task contract they can follow
&lt;/h2&gt;

&lt;p&gt;Clear instructions help, especially when they connect a change to a non-negotiable rule. Here is a concise template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Goal: extract the notification scheduling logic from src/jobs/run.ts.

Behavioral contracts:
1. A job with the same idempotency key sends at most one notification.
2. A cancelled job never sends a notification.
3. A tenant can never schedule a job for another tenant.

Boundaries:
- Do not modify safety-contracts/.
- Do not add dependencies.
- Keep the public handler signature unchanged.

Verification:
- npm run typecheck
- npm test
- npm run test:contracts
- Summarize any command that fails; do not weaken tests to make them pass.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is stronger than “refactor safely” because it tells the agent what success means. It is also easier for a reviewer to reject a superficially green change: the contract is visible before the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes when adding invariant tests
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Testing implementation details.&lt;/strong&gt; A contract should survive a refactor. Assert a durable result—number of writes, access boundary, state transition—not which private helper ran.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Using only random input.&lt;/strong&gt; Generated input expands coverage, but important named cases still matter. Keep regression fixtures for incidents and known edge cases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Letting the same change rewrite every oracle.&lt;/strong&gt; If an agent alters production code, snapshots, unit tests, and contract tests in one sweep, reviewers lose their independent evidence. Separate those changes or require a stronger review path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Making every rule a blocking invariant.&lt;/strong&gt; Start with the small set whose failure would cause financial loss, data exposure, irreversible writes, or serious reliability damage. A hundred weak contracts will be ignored.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistaking test coverage for behavioral coverage.&lt;/strong&gt; Coverage reports show which lines ran. They do not show whether the line preserved ownership, idempotency, or a state boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this fits in an AI-assisted engineering workflow
&lt;/h2&gt;

&lt;p&gt;Invariant tests sit beside, not above, the rest of your quality system:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Types and schemas catch malformed values.&lt;/li&gt;
&lt;li&gt;Unit tests catch local logic mistakes.&lt;/li&gt;
&lt;li&gt;Integration tests prove boundaries with real infrastructure.&lt;/li&gt;
&lt;li&gt;Contract tests protect cross-module promises.&lt;/li&gt;
&lt;li&gt;Invariant tests protect rules across many inputs and paths.&lt;/li&gt;
&lt;li&gt;Code review checks intent, readability, and operational consequences.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For AI-assisted work, the practical payoff is trustable speed. You can let an agent make a broad mechanical improvement when the most important promises are executable and independently checked. That is safer than restricting agents to trivial tasks—and safer than trusting a green checkmark with no stated contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is an invariant test for AI-generated code?
&lt;/h3&gt;

&lt;p&gt;It checks a rule that must stay true across many inputs and execution paths, such as tenant isolation, one payment per idempotency key, or valid state transitions. It complements example-based unit tests.&lt;/p&gt;

&lt;h3&gt;
  
  
  Are invariant tests the same as property-based tests?
&lt;/h3&gt;

&lt;p&gt;No. Property-based testing is a useful technique for generating many inputs. An invariant is the behavior you want to preserve. You can test an invariant with generated values, fixed fixtures, integration tests, or all three.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which invariants should a small team add first?
&lt;/h3&gt;

&lt;p&gt;Start with data isolation, authorization, idempotency, money movement, irreversible deletes, and the most expensive performance paths. Choose rules where one regression would create real user or business harm.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should an AI coding agent be allowed to edit invariant tests?
&lt;/h3&gt;

&lt;p&gt;It can propose a change, but treat that change as a contract decision rather than routine cleanup. Require explicit review and a reason the product rule itself changed; otherwise the agent can make failing evidence disappear.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I know an invariant test is strong enough?
&lt;/h3&gt;

&lt;p&gt;Prove it can fail. Temporarily introduce the forbidden behavior, or use mutation testing, and confirm the contract catches it. If it stays green, it is not protecting the rule yet.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do invariant tests make refactoring slower?
&lt;/h3&gt;

&lt;p&gt;They take time to write, but they reduce the slowest part of refactoring: diagnosing a production regression after a large, plausible-looking change. Start with high-risk paths and keep each contract narrow.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>testing</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>RAG Context Budget: Pack Evidence Without Burying the Answer</title>
      <dc:creator>Jack M</dc:creator>
      <pubDate>Tue, 20 Oct 2026 03:31:48 +0000</pubDate>
      <link>https://dev.to/jackm-singularity/rag-context-budget-pack-evidence-without-burying-the-answer-3ie6</link>
      <guid>https://dev.to/jackm-singularity/rag-context-budget-pack-evidence-without-burying-the-answer-3ie6</guid>
      <description>&lt;p&gt;When a RAG answer is wrong, the reflex is often to raise &lt;code&gt;top_k&lt;/code&gt;. More retrieved chunks feels safer. In production, it can do the opposite: the one useful passage gets surrounded by repeated, stale, or merely related text, while the model has less room to answer.&lt;/p&gt;

&lt;p&gt;A large context window is capacity, not a retrieval strategy. Treat the input as a &lt;strong&gt;RAG context budget&lt;/strong&gt;: reserve room for the answer, retrieve widely, then admit only the evidence that earns its token cost. This guide shows a vendor-neutral policy you can implement around any vector store and model API.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mistake: confusing retrieval recall with prompt admission
&lt;/h2&gt;

&lt;p&gt;Retrieval and generation have different jobs. A retriever should find a broad candidate set so a good passage is not missed. The prompt assembler should build a small, diverse, attributable evidence set that a model can use.&lt;/p&gt;

&lt;p&gt;Those are not the same &lt;code&gt;top_k&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For example, a support assistant may retrieve 30 candidates to protect recall, rerank the best 12, and send four short, non-overlapping sections to the model. A policy question may need the newest authoritative policy plus one exception. A “list every affected region” question may need more coverage and a different answer shape.&lt;/p&gt;

&lt;p&gt;Sending every candidate creates four problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Attention dilution:&lt;/strong&gt; Related passages repeat terms without answering the question.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unstable cost and latency:&lt;/strong&gt; A broad request can quietly turn into a huge input.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Crowded-out answers:&lt;/strong&gt; The model has little output room for a careful explanation or citations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weak debugging:&lt;/strong&gt; A trace says “top 20 retrieved,” but not which evidence actually mattered.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Recent retrieval systems increasingly select useful highlights rather than whole pages. That direction is sensible, but it does not remove the engineering decision: your application still needs an explicit policy for what may enter the generation prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define the budget before tuning chunks
&lt;/h2&gt;

&lt;p&gt;Start from the smallest context you want to support reliably, not from a model’s maximum window. Reserve fixed and variable space first:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Allocation&lt;/th&gt;
&lt;th&gt;What it contains&lt;/th&gt;
&lt;th&gt;Why it needs a cap&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Instructions&lt;/td&gt;
&lt;td&gt;safety rules, format, tool policy&lt;/td&gt;
&lt;td&gt;grows with every feature request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Conversation&lt;/td&gt;
&lt;td&gt;relevant turns and user constraints&lt;/td&gt;
&lt;td&gt;can become the dominant input&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Evidence&lt;/td&gt;
&lt;td&gt;retrieved passages and metadata&lt;/td&gt;
&lt;td&gt;must compete fairly with the answer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Answer reserve&lt;/td&gt;
&lt;td&gt;response plus citations&lt;/td&gt;
&lt;td&gt;prevents truncation and rushed output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Safety margin&lt;/td&gt;
&lt;td&gt;tokenizer and message-format overhead&lt;/td&gt;
&lt;td&gt;avoids boundary failures&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The basic equation is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;evidence_budget = input_limit
                - instruction_tokens
                - conversation_tokens
                - answer_reserve
                - safety_margin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use the provider tokenizer for the final count. A rough estimator is fine while choosing candidates, but do not assume words equal tokens or that chat-message formatting is free.&lt;/p&gt;

&lt;p&gt;The budget is a product decision too. If the user asks for a comparison with sources, reserve enough output for distinctions and citations. If the task returns a short JSON classification, the answer reserve can be smaller. A single global budget hides that trade-off.&lt;/p&gt;

&lt;h2&gt;
  
  
  Admit evidence in stages, not by a fixed top-k
&lt;/h2&gt;

&lt;p&gt;A practical context packer has four gates:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Retrieve candidates broadly.&lt;/strong&gt; Apply tenant, access-control, language, and freshness filters before similarity search where possible. Never let a relevance score override an authorization decision.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rerank against the actual question.&lt;/strong&gt; Embedding similarity is a candidate generator, not proof that a passage answers the request. A cross-encoder, rules, or a small model can score query-passage relevance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remove redundancy.&lt;/strong&gt; Adjacent chunks, copied help-center pages, and syndicated content often say the same thing. Keep the best representative and retain its source identity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pack to the remaining token budget.&lt;/strong&gt; Prefer authority, answer coverage, recency when relevant, and source diversity—not score alone.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This means a lower-scoring chunk can win a place because it covers a missing part of a multi-part question. Likewise, a high-scoring duplicate should lose once its fact is already represented.&lt;/p&gt;

&lt;h3&gt;
  
  
  Make the query class explicit
&lt;/h3&gt;

&lt;p&gt;One cap cannot serve every intent. Begin with a small classifier that chooses a policy, and log its decision:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Query class&lt;/th&gt;
&lt;th&gt;Primary goal&lt;/th&gt;
&lt;th&gt;Packing rule&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Point lookup&lt;/td&gt;
&lt;td&gt;precise fact&lt;/td&gt;
&lt;td&gt;one authoritative section; add a second only for verification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How-to&lt;/td&gt;
&lt;td&gt;ordered steps&lt;/td&gt;
&lt;td&gt;preserve a complete procedure and its prerequisites&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Comparison&lt;/td&gt;
&lt;td&gt;distinctions&lt;/td&gt;
&lt;td&gt;reserve space for each option and a shared criterion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aggregation&lt;/td&gt;
&lt;td&gt;coverage&lt;/td&gt;
&lt;td&gt;collect distinct sources; state what may be incomplete&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Troubleshooting&lt;/td&gt;
&lt;td&gt;diagnosis&lt;/td&gt;
&lt;td&gt;favor version, error, and environment matches over generic advice&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Start with deterministic routing based on obvious cues and test data. You can later use a classifier, but its output should remain observable and easy to override. A wrong class should fail safely by returning a narrower answer, asking a question, or disclosing limits—not by stuffing every result into the prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small packing policy in Python
&lt;/h2&gt;

&lt;p&gt;The example below accepts already-authorized, reranked candidates. It leaves a fixed answer reserve, rejects near-duplicates, and gives each source a small diversity allowance. It is intentionally simple: production systems also need timeout handling, tracing, and model-specific token counting.&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Chunk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;source_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;relevance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
    &lt;span class="n"&gt;novelty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;       &lt;span class="c1"&gt;# 0..1; lower means it repeats accepted evidence
&lt;/span&gt;    &lt;span class="n"&gt;authority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;     &lt;span class="c1"&gt;# 0..1; derived from your source policy
&lt;/span&gt;    &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;pack_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;evidence_budget&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;per_source_limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;accepted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;used&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source_counts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="c1"&gt;# Relevance gets us close; novelty and authority stop a score-only pack.
&lt;/span&gt;    &lt;span class="n"&gt;ranked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relevance&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.25&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;novelty&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.15&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;authority&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;reverse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&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;chunk&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ranked&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;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;novelty&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.20&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;source_counts&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="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;per_source_limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;used&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;chunk&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;gt;&lt;/span&gt; &lt;span class="n"&gt;evidence_budget&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;accepted&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="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;used&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tokens&lt;/span&gt;
        &lt;span class="n"&gt;source_counts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;source_counts&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="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;accepted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;used&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not mistake this heuristic for ground truth. The purpose is to make an admission policy visible and testable. Your &lt;code&gt;novelty&lt;/code&gt; signal could be cosine-distance from accepted chunks, heading overlap, a deduplication hash, or an evaluator. Your authority signal should come from a source registry rather than an LLM’s opinion.&lt;/p&gt;

&lt;p&gt;If one excellent section is larger than the budget, do not blindly slice it at a character boundary. Prefer a precomputed passage, a parent section with an anchored excerpt, or a targeted extraction step. Keep the citation identifier with every excerpt so the answer can point back to what it used.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reserve room for the answer before assembling evidence
&lt;/h2&gt;

&lt;p&gt;The most subtle failure is calculating evidence first and letting the answer take whatever remains. That makes answer quality depend on retrieval volume.&lt;/p&gt;

&lt;p&gt;Instead, choose an answer contract first. A short factual answer may reserve 300 tokens. A troubleshooting response with diagnosis, safe steps, and cited evidence may reserve 900 or more. A structured output contract should reserve room for its full schema and error case.&lt;/p&gt;

&lt;p&gt;Set an evidence ceiling below the model limit even if you could fit more. This makes cost and latency predictable, and it makes behavior portable when you later route a request to a smaller model. If the candidate pack does not fit, apply a known fallback:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;return the highest-authority answer with a scope note;&lt;/li&gt;
&lt;li&gt;ask the user to narrow an aggregation request;&lt;/li&gt;
&lt;li&gt;run a second retrieval pass for a missing sub-question; or&lt;/li&gt;
&lt;li&gt;summarize a trustworthy parent document, while preserving provenance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Avoid silently dropping the last chunks. Tail-drop creates position bias and makes results depend on incidental sort order.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep instructions, evidence, and tool output separate
&lt;/h2&gt;

&lt;p&gt;Context packing is also a safety boundary. A retrieved document is evidence, not an instruction. Label it as untrusted source material, keep it outside the system instruction channel, and do not let its text decide which tools are available. This matters even for an internal knowledge base: imported tickets, copied documentation, and web pages can contain text that looks like a command.&lt;/p&gt;

&lt;p&gt;Use a small envelope for every admitted passage, such as a source ID, title, version or fetch time, access label, and the excerpt itself. The model can cite the ID, while application code resolves it to a safe link. Do not invite the model to invent an origin from raw text.&lt;/p&gt;

&lt;p&gt;Tool results deserve their own cap. A database query that returns 5,000 rows is not a better answer than a bounded aggregation with a continuation path. For tools, define a response schema, a maximum row or byte count, a pagination contract, and a redaction step before results reach the packer. If an action needs additional evidence, make a second bounded call instead of carrying every prior result through the conversation.&lt;/p&gt;

&lt;p&gt;This separation makes incident review practical. You can answer three useful questions from a trace: which instruction governed the response, which sources supported it, and which tool result changed its state. A single concatenated prompt cannot answer those questions reliably.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evaluate the policy, not just the final answer
&lt;/h2&gt;

&lt;p&gt;An answer score alone cannot tell you whether the failure came from retrieval, reranking, packing, or generation. Add a trace for every sampled request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"query_class"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"troubleshooting"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"evidence_budget"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"candidate_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reranked_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"accepted_chunk_ids"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"docs-18#4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"release-7#2"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"evidence_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1412&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"answer_reserve"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;900&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dropped"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"duplicate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"budget"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"source_cap"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then evaluate representative questions by query class. For each, record whether the expected evidence was retrieved, admitted, cited, and actually supports the answer. This separates four common diagnoses:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Observation&lt;/th&gt;
&lt;th&gt;Likely issue&lt;/th&gt;
&lt;th&gt;Next action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Needed source never appears&lt;/td&gt;
&lt;td&gt;retrieval recall&lt;/td&gt;
&lt;td&gt;improve indexing, filters, or candidate depth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Needed source is retrieved but excluded&lt;/td&gt;
&lt;td&gt;packing/reranking&lt;/td&gt;
&lt;td&gt;inspect budget, diversity, and score rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Source is included but ignored&lt;/td&gt;
&lt;td&gt;prompt or ordering&lt;/td&gt;
&lt;td&gt;improve instructions, placement, or answer format&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Source is cited but answer is wrong&lt;/td&gt;
&lt;td&gt;generation/verification&lt;/td&gt;
&lt;td&gt;add claim checks or require abstention&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Measure cost, latency, groundedness, and task success together. Lowering context tokens is not a win if correct answers disappear. Conversely, sending more text is not a win if it only increases cost and makes attribution worse.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ship it with a narrow rollout
&lt;/h2&gt;

&lt;p&gt;Use a shadow trace first: calculate the proposed pack while still sending the current one. Compare token counts and whether expected evidence would have been admitted. Next, run a small traffic cohort with a kill switch.&lt;/p&gt;

&lt;p&gt;Test deliberately awkward cases: duplicated policy pages, an old and a new version, a query that needs two documents, a broad “list all” request, and a long conversation with a short current question. Include authorization changes in this suite. A perfect pack that leaks a cross-tenant passage is still a failure.&lt;/p&gt;

&lt;p&gt;The goal is not the smallest possible prompt. It is the smallest &lt;strong&gt;sufficient&lt;/strong&gt; evidence set for a task, with a predictable answer reserve and a record of why each passage was admitted.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is a RAG context budget?
&lt;/h3&gt;

&lt;p&gt;It is the portion of an LLM request deliberately reserved for retrieved evidence after instructions, relevant conversation, the answer, and a safety margin have been allocated. It turns context assembly into an explicit policy instead of a fixed &lt;code&gt;top_k&lt;/code&gt; setting.&lt;/p&gt;

&lt;h3&gt;
  
  
  How many chunks should a RAG system send to the model?
&lt;/h3&gt;

&lt;p&gt;There is no universal number. Retrieve enough candidates for recall, then evaluate how many diverse passages each query class needs. A point lookup may need one authoritative section; an aggregation question may need several. Use a token ceiling and test against real tasks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I increase top-k when RAG answers are wrong?
&lt;/h3&gt;

&lt;p&gt;Not automatically. First find out whether the needed source was missing, retrieved but excluded, included but ignored, or cited incorrectly. Raising &lt;code&gt;top_k&lt;/code&gt; helps only the first case and can make the other three worse.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I prevent duplicate chunks in RAG context?
&lt;/h3&gt;

&lt;p&gt;Store stable source and section identities, detect overlap or semantic similarity among candidates, and cap passages per source. Preserve the strongest chunk and its provenance rather than sending several near-identical versions of the same fact.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does a larger context window remove the need for RAG packing?
&lt;/h3&gt;

&lt;p&gt;No. Larger windows can hold more material, but they do not make every passage relevant, authorized, current, or easy for a model to use. Packing still controls cost, attention, provenance, and answer space.&lt;/p&gt;

&lt;h3&gt;
  
  
  What should I log for a RAG context budget?
&lt;/h3&gt;

&lt;p&gt;Log the query class, token allocations, candidate and accepted IDs, source versions, drop reasons, latency, and the final citations. Avoid storing sensitive raw text unless retention and access rules explicitly allow it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>tutorial</category>
      <category>database</category>
    </item>
    <item>
      <title>AI Agent Decision Logs: Record Evidence Without Storing Chain of Thought</title>
      <dc:creator>Jack M</dc:creator>
      <pubDate>Sat, 10 Oct 2026 05:50:28 +0000</pubDate>
      <link>https://dev.to/jackm-singularity/ai-agent-decision-logs-record-evidence-without-storing-chain-of-thought-3gp0</link>
      <guid>https://dev.to/jackm-singularity/ai-agent-decision-logs-record-evidence-without-storing-chain-of-thought-3gp0</guid>
      <description>&lt;p&gt;An agent can update a customer record, trigger a refund, or route a support case while every dashboard stays green. Then someone asks the only question that matters: &lt;strong&gt;why was this action allowed, and what actually changed?&lt;/strong&gt; A trace may show tokens and latency. An application log may show an HTTP 200. Neither is a dependable answer.&lt;/p&gt;

&lt;p&gt;The missing piece is an &lt;strong&gt;AI agent decision log&lt;/strong&gt;: a small, structured record for each meaningful choice. It links the task, the bounded evidence, the policy and release in force, the chosen action, and the independently verified outcome. It is not a transcript, and it should not be a hidden-reasoning archive.&lt;/p&gt;

&lt;p&gt;This guide shows how to build that record so an engineer can debug an incident, an operator can review a risky action, and a customer-data team does not inherit a second copy of every prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  A trace is not a decision record
&lt;/h2&gt;

&lt;p&gt;Keep both. They answer different questions.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Artifact&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;th&gt;Usually misses&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Trace&lt;/td&gt;
&lt;td&gt;latency, retries, spans, model calls&lt;/td&gt;
&lt;td&gt;business intent and policy verdict&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool log&lt;/td&gt;
&lt;td&gt;request/response diagnostics&lt;/td&gt;
&lt;td&gt;whether the response changed the target state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prompt release manifest&lt;/td&gt;
&lt;td&gt;which behavior bundle was deployed&lt;/td&gt;
&lt;td&gt;why this request took this branch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Decision log&lt;/td&gt;
&lt;td&gt;why the system was allowed to act and what was verified&lt;/td&gt;
&lt;td&gt;low-level performance detail&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Suppose a support agent changes a renewal status. A trace can show &lt;code&gt;update_subscription&lt;/code&gt; completed in 180 ms. A useful decision log can say that the request came from an authenticated account owner, the account was eligible under &lt;code&gt;renewal-policy-v4&lt;/code&gt;, the action stayed below the risk threshold, the expected version was 27, and the billing API confirmed version 28 with the requested status.&lt;/p&gt;

&lt;p&gt;That is enough to investigate and reproduce the decision without pretending that a model's private intermediate text is a reliable audit artifact.&lt;/p&gt;

&lt;h2&gt;
  
  
  Record decisions, not every model turn
&lt;/h2&gt;

&lt;p&gt;Logging every token is expensive, risky, and noisy. It also makes review harder. Define a &lt;strong&gt;decision boundary&lt;/strong&gt;: log when the agent crosses from interpretation into a consequential branch or side effect.&lt;/p&gt;

&lt;p&gt;Good boundaries include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;selecting a workflow or an external tool;&lt;/li&gt;
&lt;li&gt;classifying a request into an approval or risk tier;&lt;/li&gt;
&lt;li&gt;changing a record, sending a message, or charging money;&lt;/li&gt;
&lt;li&gt;denying an action because policy blocked it;&lt;/li&gt;
&lt;li&gt;escalating to a human;&lt;/li&gt;
&lt;li&gt;declaring a long-running task complete.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not make a decision log for each retrieval chunk, retry, or sentence of a draft. Put those in traces and link their stable IDs when they matter. A compact log should be readable when an incident is active.&lt;/p&gt;

&lt;h3&gt;
  
  
  Start with one question
&lt;/h3&gt;

&lt;p&gt;Before adding fields, ask: &lt;em&gt;Could a teammate who was absent reconstruct the reason, authority, and result of this action from this one record?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If the answer is no, add the missing reference. If the answer needs the full prompt, full customer record, and every generated token, redesign the workflow so the record can cite safe, scoped evidence instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical decision-log schema
&lt;/h2&gt;

&lt;p&gt;The schema needs stable identities, not a giant JSON blob. Here is a TypeScript shape for a material action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;DecisionLog&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;occurredAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;route&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;allow&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hold&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;block&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;complete&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;requestRef&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nl"&gt;release&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;manifestId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;policyVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nl"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;action&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;targetRef&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nl"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;allow&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hold&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;block&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nl"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;verified&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rejected&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;resultRef&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nl"&gt;redactionVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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;p&gt;Three choices matter here.&lt;/p&gt;

&lt;p&gt;First, &lt;code&gt;requestRef&lt;/code&gt;, &lt;code&gt;targetRef&lt;/code&gt;, and &lt;code&gt;resultRef&lt;/code&gt; point to access-controlled data; they do not copy it. A support transcript or invoice belongs in its existing system with its existing retention rules.&lt;/p&gt;

&lt;p&gt;Second, store versioned identifiers for the release, model, and policy. An answer may be acceptable under one policy version and blocked under the next. Without IDs, you cannot explain a historical decision honestly.&lt;/p&gt;

&lt;p&gt;Third, store the &lt;em&gt;verdict and its public reasons&lt;/em&gt;, such as &lt;code&gt;account_owner_verified&lt;/code&gt; or &lt;code&gt;refund_below_limit&lt;/code&gt;. Do not store a fabricated summary of hidden reasoning. Design the policy engine to emit explicit reason codes that are safe to expose to operators.&lt;/p&gt;

&lt;h2&gt;
  
  
  Capture evidence before the action, then verify after it
&lt;/h2&gt;

&lt;p&gt;The safest shape is a two-phase record. Write a planned decision before an external side effect; append the verified result after it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;changeRenewal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RenewalInput&lt;/span&gt;&lt;span class="p"&gt;)&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;decision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;decide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// typed facts + deterministic policy&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;record&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;decisions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;"&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="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verdict&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;allow&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;record&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;billing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateRenewal&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;subscriptionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscriptionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;expectedVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;decisions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendOutcome&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;verified&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;unknown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;resultRef&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`billing-event:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;eventId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&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;p&gt;Why not write the record only after success? Because a timeout, worker crash, or duplicate retry is itself important. The planned record gives you a durable idempotency key. The outcome distinguishes &lt;strong&gt;attempted&lt;/strong&gt;, &lt;strong&gt;verified&lt;/strong&gt;, and &lt;strong&gt;unknown&lt;/strong&gt; instead of silently turning uncertainty into success.&lt;/p&gt;

&lt;p&gt;For high-risk actions, send &lt;code&gt;hold&lt;/code&gt; records to an approval queue. The reviewer should see the task summary, safe evidence links, policy reason codes, expected change, and expiry. Their approval becomes another append-only event, not an edit to history.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep private data out of the log by default
&lt;/h2&gt;

&lt;p&gt;A decision log can become a data leak if it stores raw prompts, retrieval text, tool arguments, or secrets. Use a privacy budget as deliberately as a token budget.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Store directly&lt;/th&gt;
&lt;th&gt;Store by reference or hash&lt;/th&gt;
&lt;th&gt;Never store in the decision log&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;policy version, verdict, time, action type&lt;/td&gt;
&lt;td&gt;request ID, document ID, sanitized evidence hash&lt;/td&gt;
&lt;td&gt;API keys, access tokens, raw passwords&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;model and release IDs&lt;/td&gt;
&lt;td&gt;encrypted artifact location&lt;/td&gt;
&lt;td&gt;full hidden reasoning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;idempotency key, result status&lt;/td&gt;
&lt;td&gt;redacted tool-response reference&lt;/td&gt;
&lt;td&gt;unrestricted customer transcript&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Run a redactor before persistence, and version it. A later investigation needs to know which rule set removed or transformed a field. Do not solve this with a vague instruction like “avoid PII.” Make it a schema rule: only allow-listed fields can enter the decision table.&lt;/p&gt;

&lt;p&gt;Also separate read permissions. An on-call engineer may need policy codes and status, while a privacy officer may be the only person allowed to open the underlying request reference. The decision log should remain useful even when its linked artifacts are unavailable to the reader.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make records replayable without replaying production
&lt;/h2&gt;

&lt;p&gt;Replay means testing whether the same typed facts and policy would produce the same verdict—not firing the live tool again.&lt;/p&gt;

&lt;p&gt;Create a replay fixture with:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;sanitized input facts;&lt;/li&gt;
&lt;li&gt;the referenced release and policy versions;&lt;/li&gt;
&lt;li&gt;expected decision, reason codes, and action contract;&lt;/li&gt;
&lt;li&gt;a fake tool response or recorded result shape.
&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="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;holds a renewal change when ownership evidence is missing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;accountOwnerVerified&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;requestedChange&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;renew&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;riskAmount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nf"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;renewal-policy-v4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hold&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toContain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;account_owner_not_verified&lt;/span&gt;&lt;span class="dl"&gt;"&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;p&gt;This is more stable than asking a model to narrate its prior thinking. Keep LLM interpretation upstream, constrained by a typed output schema. Make the consequential verdict deterministic where possible. If the model must classify, capture the allowed label, evidence references, confidence as a diagnostic signal, and the policy that converted the label into an action.&lt;/p&gt;

&lt;h2&gt;
  
  
  Query records like an incident responder
&lt;/h2&gt;

&lt;p&gt;Good records turn vague questions into bounded queries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“Which policy version allowed this action?”&lt;/li&gt;
&lt;li&gt;“Did we verify the external change or merely receive a response?”&lt;/li&gt;
&lt;li&gt;“Which release caused the first &lt;code&gt;hold&lt;/code&gt; spike?”&lt;/li&gt;
&lt;li&gt;“Did a retry reuse the same idempotency key?”&lt;/li&gt;
&lt;li&gt;“Which evidence reference was unavailable during the decision?”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Build a small operational view around those questions. Show the decision timeline, state transitions, policy reason codes, release ID, and safe links. Do not start with a polished chat summary; start with data that makes a summary checkable.&lt;/p&gt;

&lt;p&gt;An especially useful alert is a rising count of &lt;code&gt;unknown&lt;/code&gt; outcomes. It catches the uncomfortable middle state: a tool call may have reached the destination, but your service cannot yet prove what happened. That deserves reconciliation, not an automatic retry.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common failure modes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Logging an unbounded prompt blob
&lt;/h3&gt;

&lt;p&gt;It feels thorough until a customer asks for deletion, a secret appears in context, or an operator needs one fact quickly. Store a constrained summary and references; keep original data in the system responsible for it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Treating a model confidence score as authority
&lt;/h3&gt;

&lt;p&gt;Confidence is not permission. A high score does not replace ownership checks, spend limits, or a reviewer. Policy should decide whether a classification may become an action.&lt;/p&gt;

&lt;h3&gt;
  
  
  Calling an HTTP response “verified”
&lt;/h3&gt;

&lt;p&gt;An accepted request can still be rejected later, applied twice, or applied to the wrong version. Prefer a returned version, downstream event ID, read-after-write check, or reconciliation job.&lt;/p&gt;

&lt;h3&gt;
  
  
  Making the log editable
&lt;/h3&gt;

&lt;p&gt;Corrections are valuable; overwriting history is not. Append a correction event that names the original record, the actor, and the reason. That preserves the investigation trail.&lt;/p&gt;

&lt;h2&gt;
  
  
  A rollout plan that will not stall the team
&lt;/h2&gt;

&lt;p&gt;Start with one high-value tool action, such as changing a ticket state or creating an outbound message. Define its input facts, risk tier, policy reasons, idempotency key, and verification method. Put the log schema under test before adding a dashboard.&lt;/p&gt;

&lt;p&gt;Next, add &lt;code&gt;hold&lt;/code&gt; and &lt;code&gt;unknown&lt;/code&gt; states. Teams often log only success and failure, but these two states capture the real operational ambiguity. Finally, link the record to your existing trace and release manifest. You get an explanation layer without replacing observability, deployments, or access control.&lt;/p&gt;

&lt;p&gt;The result is less glamorous than an autonomous demo, but more useful in production: an agent can act quickly and still leave behind a clear, privacy-aware explanation of what it was allowed to do and what the system proved afterward.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the contract in CI
&lt;/h2&gt;

&lt;p&gt;Treat a decision log as an API contract. If a release removes the policy version, changes a reason code, or starts serializing a raw email address, the build should fail before that change reaches production.&lt;/p&gt;

&lt;p&gt;Start with three small tests:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Schema test&lt;/strong&gt;: every required field is present and every reference uses an allowed prefix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redaction test&lt;/strong&gt;: realistic prompts and tool responses cannot introduce secrets, direct identifiers, or unapproved fields.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transition test&lt;/strong&gt;: &lt;code&gt;unknown&lt;/code&gt; may become &lt;code&gt;verified&lt;/code&gt; or &lt;code&gt;rejected&lt;/code&gt;, but a terminal outcome cannot be silently overwritten.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;does not persist raw customer text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&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;record&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;toDecisionLog&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Email ava@example.com about invoice 8841&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hold&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reasons&lt;/span&gt;&lt;span class="p"&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;human_review_required&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;not&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toContain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ava@example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;requestRef&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toMatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^request:/&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;p&gt;Use fixtures from incidents and near misses after redacting them. A fixture that proves a duplicated retry preserves its idempotency key is more valuable than a generic happy-path example. When a policy changes intentionally, review the fixture diff as carefully as an application permission change.&lt;/p&gt;

&lt;p&gt;One final rule helps with multi-agent systems: assign a &lt;code&gt;parentDecisionId&lt;/code&gt; when one agent delegates to another. The child still logs its own policy and result. This lets an investigator follow the tree from a user request to a tool action without merging all agents into one unsearchable transcript.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What should an AI agent decision log contain?
&lt;/h3&gt;

&lt;p&gt;Include a stable decision ID, time, task reference, release and policy versions, safe evidence references, policy verdict and reason codes, the planned action, idempotency key, and a verified or unknown outcome. Avoid raw prompts and hidden reasoning.&lt;/p&gt;

&lt;h3&gt;
  
  
  Are AI agent decision logs the same as observability traces?
&lt;/h3&gt;

&lt;p&gt;No. Traces diagnose execution performance and call paths. Decision logs explain a consequential branch or action in business and policy terms. Link the two rather than forcing either one to do both jobs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should an AI agent decision log store chain of thought?
&lt;/h3&gt;

&lt;p&gt;No. Store explicit, reviewable policy reasons and safe evidence references instead. They are more reliable for operations and avoid creating an unnecessary store of sensitive, unbounded model text.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I verify an AI agent action actually happened?
&lt;/h3&gt;

&lt;p&gt;Use an idempotency key plus a domain-level confirmation: a returned version, durable downstream event, read-after-write check, or reconciliation process. Record &lt;code&gt;unknown&lt;/code&gt; until that confirmation exists.&lt;/p&gt;

&lt;h3&gt;
  
  
  How long should decision logs be retained?
&lt;/h3&gt;

&lt;p&gt;Match retention to the underlying business process, contractual needs, and privacy policy. Keep the smallest useful record, retain linked sensitive artifacts under their own controls, and test deletion and access rules.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can small teams implement this without a new platform?
&lt;/h3&gt;

&lt;p&gt;Yes. Start with an append-only database table or event stream, typed schema validation, policy reason codes, and links to existing traces and records. Add specialized tooling only after the basic contract is working.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Prompt Release Manifests: Ship AI Changes Without Breaking Production</title>
      <dc:creator>Jack M</dc:creator>
      <pubDate>Sun, 27 Sep 2026 10:38:47 +0000</pubDate>
      <link>https://dev.to/jackm-singularity/prompt-release-manifests-ship-ai-changes-without-breaking-production-3g7b</link>
      <guid>https://dev.to/jackm-singularity/prompt-release-manifests-ship-ai-changes-without-breaking-production-3g7b</guid>
      <description>&lt;p&gt;A one-line prompt edit can change what your application does. It can choose a different tool, omit a warning, produce invalid JSON, or spend twice as many tokens. Yet after a bad response reaches a customer, many teams still cannot answer the first incident question: &lt;strong&gt;what exact behavior was running?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Git history is necessary, but it is not enough. A production response depends on more than text in a prompt file. It also depends on the model and settings, output schema, retrieval index, tool descriptions, policy rules, and feature flags resolved at request time.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;prompt release manifest&lt;/strong&gt; packages those dependencies into one immutable, testable unit. This guide shows a vendor-neutral way to create one, promote it safely, and trace a response back to the release that caused it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a prompt is not the unit you deploy
&lt;/h2&gt;

&lt;p&gt;It is tempting to treat a prompt as a string:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;instructions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Answer from the account record. Return JSON.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That works until a behavior change spans two systems. Imagine that you change the instruction to ask for a &lt;code&gt;reason&lt;/code&gt; field, upgrade the model, and add a new billing tool. The application code still deploys successfully. But the old JSON validator rejects the new field, the model calls the tool for questions it previously answered directly, and traces only record the model name.&lt;/p&gt;

&lt;p&gt;No individual change looks dramatic. The combined release is the behavior users experience.&lt;/p&gt;

&lt;p&gt;Treat this tuple as the release unit:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Why it belongs in the release&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Prompt template and examples&lt;/td&gt;
&lt;td&gt;Changes language, priorities, and tool choice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model and generation settings&lt;/td&gt;
&lt;td&gt;Changes reasoning, format reliability, latency, and cost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output schema&lt;/td&gt;
&lt;td&gt;Defines what downstream code may safely consume&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retrieval configuration&lt;/td&gt;
&lt;td&gt;Changes the evidence available to the model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool contracts and permissions&lt;/td&gt;
&lt;td&gt;Changes what the workflow can do&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Policy version&lt;/td&gt;
&lt;td&gt;Changes allowed actions and escalation rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Evaluation dataset and thresholds&lt;/td&gt;
&lt;td&gt;Proves why the candidate may be promoted&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The goal is not to invent paperwork. It is to make a response reproducible when something goes wrong.&lt;/p&gt;

&lt;h3&gt;
  
  
  Separate artifact identity from deployment state
&lt;/h3&gt;

&lt;p&gt;This distinction prevents a subtle class of production bugs. An artifact answers, “what did we test?” A deployment label answers, “what should eligible traffic use right now?” If a dashboard lets someone edit prompt text behind a &lt;code&gt;production&lt;/code&gt; label, it has mixed those two jobs. You can no longer compare an evaluation result with the exact behavior served later.&lt;/p&gt;

&lt;p&gt;Keep the release artifact append-only. Keep deployment state small and auditable: label, previous label target, actor, time, cohort rule, and change reason. That leaves a compact trail even when several people work on the same feature. It also lets a developer reproduce a customer issue locally by fetching one ID instead of guessing which combination of configuration happened to be live.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define a small, immutable manifest
&lt;/h2&gt;

&lt;p&gt;Start with plain YAML or JSON in the repository. A database-backed registry is useful later, but a reviewable file is a good first control surface.&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;# releases/support-answer/2026-09-27.3.yaml&lt;/span&gt;
&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;support-answer-2026-09-27.3&lt;/span&gt;
&lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;support-platform&lt;/span&gt;
&lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;support-answer&lt;/span&gt;
  &lt;span class="na"&gt;sha256&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8b4a…d91e"&lt;/span&gt;
  &lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;customer_message&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;account_snapshot&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;policy_excerpt&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;openai&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gpt-6-luna&lt;/span&gt;
  &lt;span class="na"&gt;temperature&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.1&lt;/span&gt;
  &lt;span class="na"&gt;max_output_tokens&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;500&lt;/span&gt;
&lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schema_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;support-answer-v4&lt;/span&gt;
  &lt;span class="na"&gt;schema_sha256&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;f03c…7ab2"&lt;/span&gt;
&lt;span class="na"&gt;retrieval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;index&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;billing-kb&lt;/span&gt;
  &lt;span class="na"&gt;index_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;2026-09-26.2"&lt;/span&gt;
  &lt;span class="na"&gt;filters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;tenant_id&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;published&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="pi"&gt;:&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;get_invoice&lt;/span&gt;
    &lt;span class="na"&gt;contract_version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v2&lt;/span&gt;
    &lt;span class="na"&gt;permission_policy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read-billing-v3&lt;/span&gt;
&lt;span class="na"&gt;policy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;support-escalation-v5&lt;/span&gt;
&lt;span class="na"&gt;evaluation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;dataset&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;support-golden-v12&lt;/span&gt;
  &lt;span class="na"&gt;dataset_sha256&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a10e…4d77"&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;eval-1842&lt;/span&gt;
  &lt;span class="na"&gt;gates&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;schema_valid_rate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;0.995&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="na"&gt;grounded_answer_rate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;min&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;0.98&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="na"&gt;unsafe_action_rate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;max&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;0&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use an immutable &lt;code&gt;id&lt;/code&gt;, not an editable label such as &lt;code&gt;latest&lt;/code&gt;. A mutable label has a place—&lt;code&gt;staging&lt;/code&gt; or &lt;code&gt;production&lt;/code&gt;—but it should point to a release ID. The manifest itself should never change after promotion. A correction creates a new release.&lt;/p&gt;

&lt;p&gt;Hashes matter when artifacts live outside Git. They let an incident responder distinguish “the prompt with this name” from the exact prompt, schema, or dataset used in a run.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keep secrets and customer data out
&lt;/h3&gt;

&lt;p&gt;Do not place API keys, raw customer examples, or complete production documents in the manifest. Store references, version IDs, and redacted test fixtures instead. A release file often becomes visible to every engineer who can review a change; make it safe to review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make compatibility explicit before an evaluation
&lt;/h2&gt;

&lt;p&gt;An evaluation can tell you a candidate is worse. It will not automatically tell you whether your application can parse its output or whether a tool call violates a new policy. Add fast compatibility checks first.&lt;/p&gt;

&lt;p&gt;For each change, ask four questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Can all declared template variables be rendered?&lt;/li&gt;
&lt;li&gt;Does the output validate against the versioned schema?&lt;/li&gt;
&lt;li&gt;Can the declared tool contracts be invoked with the release's permissions?&lt;/li&gt;
&lt;li&gt;Does the retrieval index support the required tenant and publication filters?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The following TypeScript sketch fails a build before a candidate reaches an expensive model evaluation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Manifest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nl"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;schema_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nl"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;contract_version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;validateManifest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Manifest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Registry&lt;/span&gt;&lt;span class="p"&gt;)&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;missing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;variables&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;variableTypes&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;v&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="nx"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Unknown prompt variables: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&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;registry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;schemas&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;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;schema_id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Unknown output schema: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;schema_id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&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;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tools&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;registry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toolContracts&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="s2"&gt;`&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="s2"&gt;@&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;contract_version&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Unknown tool contract: &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="s2"&gt;@&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;contract_version&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&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;p&gt;This is deliberately boring. Boring checks prevent incidents that no prompt wording can fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evaluate the candidate against the current release
&lt;/h2&gt;

&lt;p&gt;Run the same representative cases against the current production release and the candidate. Do not judge a candidate only against a hand-picked example that inspired the change.&lt;/p&gt;

&lt;p&gt;Your golden set should include normal requests, short and ambiguous messages, missing records, stale retrieval results, permission boundaries, tool failures, and requests that require escalation. Keep the expected answer narrow where possible: schema validity, grounded claims, correct route, allowed tool use, and a human-review requirement can often be checked without asking another model for a vague quality score.&lt;/p&gt;

&lt;p&gt;Store the comparison with the manifest rather than in a dashboard comment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"candidate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"support-answer-2026-09-27.3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"baseline"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"support-answer-2026-09-18.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cases"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;184&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"schema_valid_rate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"baseline"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"candidate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"grounded_answer_rate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"baseline"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.989&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"candidate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.994&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"unsafe_action_rate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"baseline"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"candidate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"review"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"approved"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The numbers above are an example format, not a universal pass threshold. Define thresholds per workflow. A support-draft feature may tolerate a lower stylistic score than a workflow that changes a subscription or sends an email.&lt;/p&gt;

&lt;h2&gt;
  
  
  Promote with a pointer, not a code redeploy
&lt;/h2&gt;

&lt;p&gt;The serving application should resolve one release ID at the start of a request and attach it to the request context. It should never resolve &lt;code&gt;production&lt;/code&gt; again halfway through a tool-using workflow.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;beginAiRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;)&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;releaseId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;releases&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolveLabel&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;production&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;,&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;release&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;releases&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getImmutable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;releaseId&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;trace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tracer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startSpan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ai.request&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;attributes&lt;/span&gt;&lt;span class="p"&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;ai.release_id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;release&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;runWorkflow&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;release&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;trace&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&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;p&gt;Promotion becomes a controlled pointer change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;production -&amp;gt; support-answer-2026-09-18.1
production -&amp;gt; support-answer-2026-09-27.3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That separation matters. A rollback is then a pointer change to a known-good release, not a frantic attempt to reconstruct an earlier deployment from commits, caches, and environment variables.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use cohorts before full promotion
&lt;/h3&gt;

&lt;p&gt;Start a candidate with a low-risk cohort: internal users, a test tenant, or a small percentage of eligible read-only requests. Preserve the resolved release ID on every trace, output validation failure, tool call, and user feedback event.&lt;/p&gt;

&lt;p&gt;Compare candidate and baseline by release ID. Watch the measures that matter to the workflow: invalid responses, escalation rate, task completion, tool errors, latency, and token use. Do not promote because a general “thumbs up” average looks good when policy violations or parse failures are hiding underneath it.&lt;/p&gt;

&lt;p&gt;For write-capable workflows, shadow evaluation is often safer than a traffic split. Let the candidate produce a proposed action, compare it with the baseline or a reviewed expectation, then discard it. Never let a shadow run repeat an external side effect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Plan rollback as a test, not a hope
&lt;/h2&gt;

&lt;p&gt;Rolling back the prompt alone may not restore behavior. The candidate might depend on a schema, tool contract, or retrieval index that changed at the same time. That is why the manifest contains the whole compatible set.&lt;/p&gt;

&lt;p&gt;Rehearse this runbook:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Detect a release-scoped regression from traces or a gate alert.&lt;/li&gt;
&lt;li&gt;Freeze promotion and identify the last healthy release ID.&lt;/li&gt;
&lt;li&gt;Move the production label back to that ID.&lt;/li&gt;
&lt;li&gt;Confirm new requests resolve the old ID in each region and cache layer.&lt;/li&gt;
&lt;li&gt;Stop or safely drain in-flight work that has not resolved a release yet.&lt;/li&gt;
&lt;li&gt;Compare errors and outcomes by release ID, then create a new candidate instead of editing history.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Cache behavior deserves special attention. If one process caches the production label for ten minutes and another for thirty seconds, you do not have an instant rollback. Include label-cache TTL and region in trace attributes, and test a rollback during a calm weekday—not during an incident.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make traces useful to the person on call
&lt;/h2&gt;

&lt;p&gt;At minimum, record this data on every AI request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;release_id
prompt_hash
model_id and provider response version when available
schema_id
retrieval_index_version
tool contract versions
policy version
tenant-safe evaluation cohort
label-cache state
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not log the entire prompt or user content by default. Store redacted identifiers and use restricted debugging access for sensitive payloads. Traceability should improve incident response without turning observability into a second data leak.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical adoption path
&lt;/h2&gt;

&lt;p&gt;You do not need a prompt platform to adopt this pattern.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Week one:&lt;/strong&gt; move production prompts into reviewed files, add an immutable release ID, and log it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Week two:&lt;/strong&gt; add schema and tool-contract validation plus a small golden set of cases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Week three:&lt;/strong&gt; promote through a staging label and gate production on a recorded comparison.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Week four:&lt;/strong&gt; add cohort rollout, release-scoped dashboards, and a rollback rehearsal.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The smallest useful rule is simple: every production response must name the release that produced it. Once that is true, quality work becomes much less mysterious.&lt;/p&gt;

&lt;h2&gt;
  
  
  Release checklist
&lt;/h2&gt;

&lt;p&gt;Before moving a prompt release to production, confirm:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] The manifest is immutable and reviewed.&lt;/li&gt;
&lt;li&gt;[ ] Prompt, model, schema, retrieval, tools, and policy versions are pinned.&lt;/li&gt;
&lt;li&gt;[ ] Template variables and tool contracts passed compatibility checks.&lt;/li&gt;
&lt;li&gt;[ ] Candidate and baseline ran against the same relevant test set.&lt;/li&gt;
&lt;li&gt;[ ] Gates match the risk of the workflow.&lt;/li&gt;
&lt;li&gt;[ ] The rollout cohort and stop conditions are defined.&lt;/li&gt;
&lt;li&gt;[ ] Every request records the resolved release ID.&lt;/li&gt;
&lt;li&gt;[ ] Rollback points to a known compatible release and has been rehearsed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is a prompt release manifest?
&lt;/h3&gt;

&lt;p&gt;It is an immutable record of the full AI behavior you are deploying: prompt, model settings, schema, retrieval version, tool contracts, policy, and evaluation evidence. A label such as &lt;code&gt;production&lt;/code&gt; points to one manifest at a time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is Git enough for prompt versioning?
&lt;/h3&gt;

&lt;p&gt;Git is an excellent authoring and review surface, but it does not by itself show which prompt, model, index, and feature configuration a live request resolved. Pair Git history with an immutable runtime release ID and trace it on every request.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should every prompt edit get a new release ID?
&lt;/h3&gt;

&lt;p&gt;Any edit that can affect production behavior should. Drafts can change freely, but once a candidate is evaluated or exposed to users, give it a new immutable ID so results and incidents remain reproducible.&lt;/p&gt;

&lt;h3&gt;
  
  
  How is a prompt release manifest different from an evaluation harness?
&lt;/h3&gt;

&lt;p&gt;An evaluation harness measures behavior. A release manifest identifies exactly what behavior was measured and later served. Use both: the manifest links a candidate to the dataset, thresholds, and evaluation run that justified promotion.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I roll back a prompt without redeploying the application?
&lt;/h3&gt;

&lt;p&gt;Yes, if the application resolves a production label to an immutable manifest at request start. Move the label to a previously compatible release, then verify cache and region propagation. Do not rely on editing a live prompt in place.&lt;/p&gt;

&lt;h3&gt;
  
  
  What should a team log for AI release debugging?
&lt;/h3&gt;

&lt;p&gt;Log the resolved release ID plus prompt hash, model identifier, output schema, retrieval version, tool contracts, policy version, and safe rollout cohort. Avoid logging raw sensitive content unless access controls and retention rules justify it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Combine Rules and LLMs for Reliable AI Workflows</title>
      <dc:creator>Jack M</dc:creator>
      <pubDate>Wed, 23 Sep 2026 11:39:17 +0000</pubDate>
      <link>https://dev.to/jackm-singularity/how-to-combine-rules-and-llms-for-reliable-ai-workflows-32gd</link>
      <guid>https://dev.to/jackm-singularity/how-to-combine-rules-and-llms-for-reliable-ai-workflows-32gd</guid>
      <description>&lt;p&gt;An LLM can write a convincing answer while making the wrong decision. That is the uncomfortable gap behind many production AI failures: the system treats a probabilistic model as the final authority on facts that should be fixed, authorized, or calculated exactly.&lt;/p&gt;

&lt;p&gt;The answer is not to remove the LLM. It is to give it the right job. Use deterministic code for boundaries and decisions that must be repeatable; use the LLM for interpretation, extraction, explanation, and ambiguity. This article shows how to build that split, test it, and keep it useful as the workflow grows.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core pattern: rules decide, LLMs interpret
&lt;/h2&gt;

&lt;p&gt;Think of an AI workflow as four separate layers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Best job&lt;/th&gt;
&lt;th&gt;Should it be deterministic?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Input boundary&lt;/td&gt;
&lt;td&gt;validate identity, schema, size, and consent&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Decision policy&lt;/td&gt;
&lt;td&gt;permissions, pricing, eligibility, risk, routing&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language layer&lt;/td&gt;
&lt;td&gt;classify messy text, extract fields, summarize, explain&lt;/td&gt;
&lt;td&gt;No, but constrained&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Action boundary&lt;/td&gt;
&lt;td&gt;execute approved operations and record evidence&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is not a claim that models cannot reason. It is a design choice: do not ask a model to enforce a rule your application can enforce exactly. Recent &lt;a href="https://aws.amazon.com/blogs/security/preparing-for-agentic-ai-a-financial-services-approach/" rel="noopener noreferrer"&gt;AWS guidance for agentic systems&lt;/a&gt; similarly recommends server-side checks at the point of tool use, least-privilege boundaries, tracing, and canary testing for behavior changes.&lt;/p&gt;

&lt;p&gt;For example, a support workflow may ask an LLM to read a customer message and return a structured intent. The application, not the model, decides whether a refund is allowed and whether a ticket can be changed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;customer message
  -&amp;gt; LLM extracts intent + evidence
  -&amp;gt; schema validation
  -&amp;gt; deterministic policy evaluates eligibility
  -&amp;gt; approved action or human review
  -&amp;gt; LLM explains the outcome in plain language
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key benefit is debuggability. When a refund is blocked, you can tell whether extraction failed, a required fact was missing, or a rule intentionally denied the action. “The model decided” is not an operational explanation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Find the decisions that should never depend on a prompt
&lt;/h2&gt;

&lt;p&gt;Start by listing every workflow decision and placing it in one of three buckets.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hard rules
&lt;/h3&gt;

&lt;p&gt;Hard rules have a single correct answer from trusted inputs. Put them in code, a policy engine, or a database constraint.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is the caller authenticated for this tenant?&lt;/li&gt;
&lt;li&gt;Is the requested action within their role and spend limit?&lt;/li&gt;
&lt;li&gt;Is an invoice overdue according to the ledger?&lt;/li&gt;
&lt;li&gt;Does a request satisfy a required JSON schema?&lt;/li&gt;
&lt;li&gt;Is a tool call idempotent, rate-limited, and within its allowed scope?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An LLM can help explain a denial, but it should not be the source of truth for it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Soft judgments
&lt;/h3&gt;

&lt;p&gt;Soft judgments need context, language understanding, or a trade-off. These suit an LLM, ideally with an explicit output contract.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which issue category best fits this free-form report?&lt;/li&gt;
&lt;li&gt;Which facts in a document answer the user’s question?&lt;/li&gt;
&lt;li&gt;Is this explanation clear enough for a nontechnical reader?&lt;/li&gt;
&lt;li&gt;Which knowledge-base article is most relevant after retrieval?&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Escalation cases
&lt;/h3&gt;

&lt;p&gt;Some tasks are neither safe to automate nor simple enough for a static rule. Route them to a reviewer with the facts the reviewer needs. Do not hide uncertainty behind confident prose.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Disposition&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;allow&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;policyId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;deny&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;policyId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;review&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;policyId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;questions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;decideRefund&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;actorRole&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;agent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;viewer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;amountCents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;daysSincePurchase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}):&lt;/span&gt; &lt;span class="nx"&gt;Disposition&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="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;actorRole&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;viewer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;deny&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;policyId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refund-role-v1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;role cannot issue refunds&lt;/span&gt;&lt;span class="dl"&gt;"&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="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amountCents&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;review&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;policyId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refund-limit-v1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;questions&lt;/span&gt;&lt;span class="p"&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;Is manager approval attached?&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&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="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;daysSincePurchase&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;deny&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;policyId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refund-window-v1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;outside refund window&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;allow&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;policyId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refund-standard-v1&lt;/span&gt;&lt;span class="dl"&gt;"&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;p&gt;Notice what is missing: no model call is needed to determine the limit. That makes the result stable, unit-testable, and easy to audit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give the model a narrow, typed contract
&lt;/h2&gt;

&lt;p&gt;The LLM should return observations, not permissions. A good contract names the facts it may infer, records uncertainty, and leaves sensitive decisions to code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zod&lt;/span&gt;&lt;span class="dl"&gt;"&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;SupportSignal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refund_request&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;billing_question&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;technical_issue&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;other&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
  &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^ord_&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;a-z0-9&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+$/&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;requestedAmountCents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;nonnegative&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&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;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&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="na"&gt;needsHumanReview&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;SupportSignal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;infer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;SupportSignal&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the prompt, ask for only this JSON shape. After receiving it, parse it with the schema. If parsing fails, retry once with a repair instruction or route the task to review. Do not silently coerce a malformed amount, unknown intent, or invented order ID into an action.&lt;/p&gt;

&lt;p&gt;This division also protects against an easy mistake: treating text returned by a document, browser page, or tool as authority. Retrieved content can influence the LLM’s interpretation; it must not be allowed to override authorization or policy. Fetch authoritative facts from the systems that own them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build a decision pipeline, not a giant prompt
&lt;/h2&gt;

&lt;p&gt;An implementation becomes easier to reason about when each stage has one responsibility.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleSupportMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&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;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;authenticateAndLoadTenant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&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;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;validateIncomingMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&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;signal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;SupportSignal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;extractSignal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&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;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orderId&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;loadOrderForTenant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&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;disposition&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;needsHumanReview&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;confidence&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;review&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;policyId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;missing-or-uncertain-facts-v1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;questions&lt;/span&gt;&lt;span class="p"&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;Verify order and request details&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;decideRefund&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;actorRole&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;actorRole&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;amountCents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;requestedAmountCents&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;daysSincePurchase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;daysSince&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;purchasedAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;writeAuditRecord&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;disposition&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;id&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="nx"&gt;disposition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;allow&lt;/span&gt;&lt;span class="dl"&gt;"&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;renderSafeResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;disposition&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signal&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;executeIdempotentRefund&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;disposition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;policyId&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;p&gt;This pattern makes threat boundaries visible:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;authenticateAndLoadTenant&lt;/code&gt; gets identity from a trusted session, not model output.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;loadOrderForTenant&lt;/code&gt; scopes the lookup before it reaches a business rule.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;decideRefund&lt;/code&gt; is pure, so it can be tested with a table of cases.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;executeIdempotentRefund&lt;/code&gt; owns the side effect and records the policy that allowed it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The LLM can still make the experience feel natural. It can extract fields from a rambling request, draft a kind response, and suggest what information is missing. It cannot manufacture the right to change a record.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handle disagreement without pretending the model is a judge
&lt;/h2&gt;

&lt;p&gt;Many teams add a second model when the first output looks uncertain. That can be useful for language tasks, but it is not a substitute for policy. Two models agreeing that an action is allowed does not make it authorized.&lt;/p&gt;

&lt;p&gt;Use disagreement as a routing signal instead:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Compare model extraction with trusted records and deterministic checks.&lt;/li&gt;
&lt;li&gt;If the facts conflict, stop the automated path.&lt;/li&gt;
&lt;li&gt;Preserve the candidate output, relevant evidence, and policy result for review.&lt;/li&gt;
&lt;li&gt;Turn the resolved case into a regression fixture.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For a classification task, you might use a small model for a first pass and a stronger model only when confidence is low. For a payment, permission, deletion, or external message, the final authority should still be an application policy plus any required human approval.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measure the workflow at three levels
&lt;/h2&gt;

&lt;p&gt;HTTP success tells you very little. A 200 response can conceal a wrong classification, a denied action that should have been allowed, or a costly retry loop.&lt;/p&gt;

&lt;p&gt;Track three metric layers for each version of the workflow:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Examples&lt;/th&gt;
&lt;th&gt;Promotion question&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Contract&lt;/td&gt;
&lt;td&gt;schema-valid output, required facts present, tool argument checks&lt;/td&gt;
&lt;td&gt;Did the interface remain intact?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Outcome&lt;/td&gt;
&lt;td&gt;accepted resolution, correction rate, reviewer reversal, safety violation&lt;/td&gt;
&lt;td&gt;Did it help the user without breaking policy?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operations&lt;/td&gt;
&lt;td&gt;p95 latency, error rate, cost per completed task, queue age&lt;/td&gt;
&lt;td&gt;Can the system sustain it?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This mirrors a useful observation from a &lt;a href="https://tianpan.co/blog/2026/04/09/feature-flags-progressive-delivery-llm-features" rel="noopener noreferrer"&gt;feature-flag guide for LLM features&lt;/a&gt;: computational metrics are easy, deterministic behavioral checks need more setup, and semantic quality needs a rubric or human labeling. Do not let a latency improvement hide a semantic regression.&lt;/p&gt;

&lt;p&gt;Segment metrics by tenant plan, language, workflow type, and risk tier. A global average can be healthy while a low-volume but important category fails badly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test rules and model behavior differently
&lt;/h2&gt;

&lt;p&gt;Rules deserve ordinary unit tests because they should behave the same way every time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vitest&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;large refunds always require review&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;decideRefund&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;t_1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;actorRole&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;amountCents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="nx"&gt;_001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;daysSincePurchase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
  &lt;span class="p"&gt;})).&lt;/span&gt;&lt;span class="nf"&gt;toMatchObject&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;review&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;policyId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refund-limit-v1&lt;/span&gt;&lt;span class="dl"&gt;"&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;p&gt;Model behavior needs a different test set: representative messages, malformed inputs, adversarial instructions inside quoted text, sparse evidence, multilingual requests, and cases that must be escalated. Assert hard properties exactly—schema validity, no unapproved tool call, correct tenant scope. Score soft properties with a rubric and sampled human review.&lt;/p&gt;

&lt;p&gt;Keep a fixture whenever a production case is corrected. Over time, the suite becomes a map of real customer ambiguity, not a collection of polished demos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Roll out a policy-model change safely
&lt;/h2&gt;

&lt;p&gt;Treat a prompt, retrieval setting, tool description, model, and policy rule as a release bundle. Record version IDs for each piece. If two components change at once, you cannot tell which one improved or harmed the result.&lt;/p&gt;

&lt;p&gt;Use a small progression:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Offline replay:&lt;/strong&gt; run candidate and incumbent against saved, de-identified fixtures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shadow path:&lt;/strong&gt; run the candidate beside production, but keep it away from side effects and user-visible results.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Limited exposure:&lt;/strong&gt; give a stable, low-risk cohort the candidate path. Keep an immediate switch back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Promotion:&lt;/strong&gt; expand only when contract, outcome, and operational thresholds pass with enough representative traffic.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Before the first user sees a change, write veto conditions. Examples include any cross-tenant lookup, any unauthorized action, schema failure above a small tolerance, or a correction rate above the incumbent. These are not averages to trade away for a prettier answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical starting checklist
&lt;/h2&gt;

&lt;p&gt;If your current workflow is one large prompt, do not rewrite everything. Pick one irreversible or high-volume path and work through this list.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;List every proposed action and identify its authoritative data source.&lt;/li&gt;
&lt;li&gt;Move permission, eligibility, spend, and tenant-scope checks outside the prompt.&lt;/li&gt;
&lt;li&gt;Make the LLM output observations in a validated schema.&lt;/li&gt;
&lt;li&gt;Use trusted records to confirm identifiers before an action.&lt;/li&gt;
&lt;li&gt;Add an explicit &lt;code&gt;review&lt;/code&gt; disposition; uncertainty should have a safe destination.&lt;/li&gt;
&lt;li&gt;Make writes idempotent and log the policy version, input references, and outcome.&lt;/li&gt;
&lt;li&gt;Test rules with tables and model behavior with representative fixtures.&lt;/li&gt;
&lt;li&gt;Define release thresholds and vetoes before a new version receives traffic.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is not a less capable AI feature. It is an AI feature whose strengths are useful and whose boundaries are inspectable. Let the model handle language and ambiguity. Let the system enforce the facts, permissions, and actions that customers depend on.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  When should I use rules instead of an LLM?
&lt;/h3&gt;

&lt;p&gt;Use rules when a decision has a trusted input and a repeatable answer: authorization, pricing, eligibility, tenant scope, rate limits, schemas, and side-effect permissions. Use an LLM when the input is ambiguous language or the output is explanatory.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can an LLM call a rule engine?
&lt;/h3&gt;

&lt;p&gt;Yes. The safe pattern is for the LLM to request a narrow, validated operation, while the application or policy service evaluates the rule and returns the result. The model should not be able to bypass that service or supply its own authorization facts.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I prevent an AI workflow from acting on hallucinated IDs?
&lt;/h3&gt;

&lt;p&gt;Validate the model’s output against a strict schema, then retrieve the record from an authoritative system using the authenticated tenant scope. Never use a model-generated identifier as proof that a record exists or belongs to the caller.&lt;/p&gt;

&lt;h3&gt;
  
  
  What should happen when the model is uncertain?
&lt;/h3&gt;

&lt;p&gt;Make uncertainty explicit in the output contract and route it to a safe fallback: ask for missing information, use a deterministic alternate path, or create a review task. Do not lower a confidence threshold merely to increase automation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do deterministic rules make an AI feature less flexible?
&lt;/h3&gt;

&lt;p&gt;They make high-stakes edges less flexible on purpose. The LLM remains free to understand varied language, summarize, and explain. Rules simply reserve fixed decisions and external actions for components that can be tested and audited exactly.&lt;/p&gt;

&lt;h3&gt;
  
  
  How should I test hybrid AI workflows?
&lt;/h3&gt;

&lt;p&gt;Unit-test policies and side-effect guards with exact cases. Test the model layer with representative and adversarial fixtures, then assert strict properties such as schema validity, tenant scope, and forbidden-action avoidance. Evaluate softer qualities with rubrics and sampled review.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Conversation Regression Testing for AI Agents: Catch Multi-Turn Failures Before Production</title>
      <dc:creator>Jack M</dc:creator>
      <pubDate>Sat, 19 Sep 2026 15:16:25 +0000</pubDate>
      <link>https://dev.to/jackm-singularity/conversation-regression-testing-for-ai-agents-catch-multi-turn-failures-before-production-emg</link>
      <guid>https://dev.to/jackm-singularity/conversation-regression-testing-for-ai-agents-catch-multi-turn-failures-before-production-emg</guid>
      <description>&lt;p&gt;An agent can give a convincing final answer and still fail the user three turns earlier. It may forget an account constraint, call the wrong tool, accept a correction it should reject, or carry a stale fact into every later decision. A one-prompt test will happily pass.&lt;/p&gt;

&lt;p&gt;That is why production agents need &lt;strong&gt;conversation regression testing&lt;/strong&gt;: replaying a complete, realistic interaction after a change and checking properties that span the whole trajectory. This guide shows how to build a small, useful suite without pretending model output will be byte-for-byte deterministic.&lt;/p&gt;

&lt;p&gt;The payoff is concrete: when you change a model, prompt, tool, retrieval source, or memory policy, CI can tell you whether a familiar customer journey still completes safely—and where it first went wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why single-turn tests miss the expensive failures
&lt;/h2&gt;

&lt;p&gt;Single-turn tests are still valuable. They catch malformed structured output, unsafe tool arguments, and obvious retrieval errors quickly. But a customer-facing agent is stateful. Each turn changes what the next turn sees.&lt;/p&gt;

&lt;p&gt;Consider a support agent helping a customer change a subscription:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The customer identifies their workspace.&lt;/li&gt;
&lt;li&gt;They ask about plan options.&lt;/li&gt;
&lt;li&gt;They clarify that an annual invoice is already paid.&lt;/li&gt;
&lt;li&gt;The agent proposes a change.&lt;/li&gt;
&lt;li&gt;The customer approves it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;An agent can answer turn 5 politely while violating the constraint introduced at turn 3. A final-answer judge may call the response helpful; your billing system will call it a defect.&lt;/p&gt;

&lt;p&gt;Conversation tests reveal four failures that prompt tests routinely hide:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failure&lt;/th&gt;
&lt;th&gt;What a single prompt misses&lt;/th&gt;
&lt;th&gt;Conversation-level assertion&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Context loss&lt;/td&gt;
&lt;td&gt;The final answer looks plausible&lt;/td&gt;
&lt;td&gt;A previously confirmed constraint remains active&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bad trajectory&lt;/td&gt;
&lt;td&gt;The answer is right for the wrong reason&lt;/td&gt;
&lt;td&gt;Required tool calls happen in the approved order&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cascading error&lt;/td&gt;
&lt;td&gt;Later turns inherit an early mistake&lt;/td&gt;
&lt;td&gt;The first failing turn is recorded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unsafe recovery&lt;/td&gt;
&lt;td&gt;The agent retries an action after a correction&lt;/td&gt;
&lt;td&gt;A correction invalidates pending state and side effects&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The key idea is simple: test a conversation as a sequence of &lt;strong&gt;state transitions&lt;/strong&gt;, not as a bag of independent answers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with journeys, not a giant benchmark
&lt;/h2&gt;

&lt;p&gt;Do not begin by generating thousands of synthetic chats. Start with 10–20 journeys that represent expensive, frequent, or risky work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a customer changes a requirement halfway through;&lt;/li&gt;
&lt;li&gt;an API returns incomplete data and the agent must ask a question;&lt;/li&gt;
&lt;li&gt;a user asks for a write action, then withdraws approval;&lt;/li&gt;
&lt;li&gt;a retrieved policy conflicts with a remembered preference;&lt;/li&gt;
&lt;li&gt;a tool times out and the agent must not duplicate the side effect.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Good fixtures come from anonymized support transcripts, bug reports, sales-engineering handoffs, and incidents. Remove personal data, replace identifiers with stable test values, and record the business rule the journey protects. A fixture is not a transcript archive; it is an executable statement of what must remain true.&lt;/p&gt;

&lt;p&gt;Here is a compact TypeScript shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Turn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;?:&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;mustInclude&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="nl"&gt;mustNotInclude&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ConversationFixture&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;risk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;low&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;medium&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;high&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;initialState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;turns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Turn&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nl"&gt;invariants&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&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;cancelBeforeWrite&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ConversationFixture&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cancel-before-plan-change&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;risk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;high&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;initialState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;workspaceId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ws_test&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;annual&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;turns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Change us to the monthly plan.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;quote_plan_change&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Wait, do not make any changes yet.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;What would the prorated amount be?&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;quote_plan_change&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;invariants&lt;/span&gt;&lt;span class="p"&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;no plan_change write occurs&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;withdrawal is acknowledged&lt;/span&gt;&lt;span class="dl"&gt;"&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;p&gt;This fixture is deliberately small. It tests a real product promise: a quote is not authorization, and a withdrawn request cannot leak into a later tool call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Record enough evidence to replay the path
&lt;/h2&gt;

&lt;p&gt;You cannot debug a failed conversation with only the final text. Store a trace for each turn with the input, selected tool, sanitized arguments, tool result, state revision, prompt revision, model ID, and an immutable run ID.&lt;/p&gt;

&lt;p&gt;Keep secrets and raw customer data out of fixtures. Store references or redacted values instead. If a tool result changes outside your control, use a test double for CI and separately run a small nightly suite against a safe staging environment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;TraceStep&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;turn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;stateVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;assistantText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;toolCalls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;resultCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;promptVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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;p&gt;The distinction matters. A replay fixture supplies stable inputs. A trace explains the observed trajectory. Together they let a reviewer answer, “Did the model change, did our prompt change, or did a tool contract change?”&lt;/p&gt;

&lt;h2&gt;
  
  
  Assert invariants, not exact prose
&lt;/h2&gt;

&lt;p&gt;Exact-string assertions are brittle because legitimate wording changes. Purely subjective judging is brittle because it can miss deterministic safety failures. Use both, in layers.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Deterministic checks first
&lt;/h3&gt;

&lt;p&gt;Make product and security rules ordinary code. Examples:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;assertNoWriteAfterWithdrawal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TraceStep&lt;/span&gt;&lt;span class="p"&gt;[])&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;withdrewAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findIndex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="sr"&gt;/do not make|cancel|withdraw/i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;assistantText&lt;/span&gt;&lt;span class="p"&gt;)&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;writes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;withdrewAt&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flatMap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toolCalls&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;change_plan&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;writes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;plan changed after withdrawal&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;assertWorkspaceScope&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TraceStep&lt;/span&gt;&lt;span class="p"&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;call&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flatMap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toolCalls&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="nx"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;get_invoice&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ws_test&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tool call escaped fixture workspace&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&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;p&gt;Prefer assertions on permissions, schema validity, tool order, idempotency keys, source citations, and state transitions. They are fast, explainable, and cheap enough for every pull request.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Use rubric judges for meaning
&lt;/h3&gt;

&lt;p&gt;Then use a separate model or human review for properties that cannot be reduced to a rule: did the agent acknowledge the correction, ask a necessary clarifying question, or preserve the user’s goal?&lt;/p&gt;

&lt;p&gt;Give the judge a narrow rubric and the relevant evidence. Do not ask, “Is this good?” Ask, “After the user withdrew authorization, did the agent promise or initiate a plan change? Return pass, fail, or uncertain with the first supporting turn.” Treat &lt;code&gt;uncertain&lt;/code&gt; as review-needed, not as a pass.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Compare distributions when the model is variable
&lt;/h3&gt;

&lt;p&gt;Run high-risk fixtures several times. A pass rate of 10/10 is more meaningful than a fortunate 1/1, but do not turn every CI job into an expensive Monte Carlo experiment. A practical split is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PR checks: deterministic mocks plus one seeded or low-temperature run;&lt;/li&gt;
&lt;li&gt;nightly: 3–10 runs for high-risk journeys and broader user simulations;&lt;/li&gt;
&lt;li&gt;release candidate: full suite, with failures triaged before promotion.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Set a budget per suite. Regression testing should prevent surprise spend, not create it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attribute the first failure, not just the unhappy ending
&lt;/h2&gt;

&lt;p&gt;The most useful output is not “conversation failed.” It is “turn 2 selected an unscoped invoice lookup; turns 3–5 inherited the wrong workspace.” That transforms an opaque judge score into an engineering task.&lt;/p&gt;

&lt;p&gt;For each test, record:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;first_failure_turn&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;violated invariant or rubric item;&lt;/li&gt;
&lt;li&gt;causal tool call, retrieval item, or state transition;&lt;/li&gt;
&lt;li&gt;downstream turns affected;&lt;/li&gt;
&lt;li&gt;prompt, model, tool, and knowledge-base versions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This separation prevents a common mistake: fixing the last bad answer instead of the earlier decision that made it inevitable. It also makes failures comparable across releases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add forks for the moments where users change direction
&lt;/h2&gt;

&lt;p&gt;Linear transcripts cover the happy path. Real conversations fork at moments of uncertainty: a user corrects an assumption, refuses an action, or reveals a constraint. You do not need to regenerate the identical first four turns for every branch.&lt;/p&gt;

&lt;p&gt;Snapshot the safe state at a branch point and run alternatives from there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;branches&lt;/span&gt; &lt;span class="o"&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;Yes, apply the change.&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;No, cancel that request.&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;Use a different workspace instead.&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;nextUserMessage&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;branches&lt;/span&gt;&lt;span class="p"&gt;)&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;runFromSnapshot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;quoteState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;nextUserMessage&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;scoreConversation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&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;p&gt;Forking improves coverage while controlling token cost. It is especially useful for consent, payments, account access, and any agent that can trigger an external write.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put the suite in the delivery path
&lt;/h2&gt;

&lt;p&gt;Run conversation regression tests whenever any behavior-bearing input changes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;system prompt or workflow graph;&lt;/li&gt;
&lt;li&gt;model, model parameters, or provider route;&lt;/li&gt;
&lt;li&gt;tool schema, permission policy, or API version;&lt;/li&gt;
&lt;li&gt;retrieval corpus, ranking rule, or memory summarizer;&lt;/li&gt;
&lt;li&gt;frontend action that changes what users can approve.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tag fixtures by risk. A low-risk content-answering journey may warn on a judge regression. A high-risk write journey should block a release when a deterministic invariant fails.&lt;/p&gt;

&lt;p&gt;Keep a short review packet with the baseline trace, candidate trace, score changes, and first failure. This is far better than burying a 30-turn transcript in CI logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turn incidents into durable tests
&lt;/h2&gt;

&lt;p&gt;Every production failure should become one of three things: a fixed invariant, a replay fixture, or a reason not to add a test. The last category is legitimate when the incident was infrastructure-only and already covered elsewhere—but make that decision explicit.&lt;/p&gt;

&lt;p&gt;Use this loop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Redact and reconstruct the customer journey.&lt;/li&gt;
&lt;li&gt;Capture the smallest sequence that reproduces the failure.&lt;/li&gt;
&lt;li&gt;Add the missing invariant or rubric criterion.&lt;/li&gt;
&lt;li&gt;Verify the fix against the old fixture and a nearby branch.&lt;/li&gt;
&lt;li&gt;Keep the fixture permanently unless the product rule changes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Over time, the suite becomes an operational memory of how your agent has actually failed. That is more valuable than a generic leaderboard score because it reflects the work your users ask it to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical rollout checklist
&lt;/h2&gt;

&lt;p&gt;Before calling conversation regression testing complete, confirm:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] You have 10–20 risk-ranked journeys from real work.&lt;/li&gt;
&lt;li&gt;[ ] Fixtures use redacted, stable data and safe tool doubles.&lt;/li&gt;
&lt;li&gt;[ ] Each high-risk journey has at least one deterministic invariant.&lt;/li&gt;
&lt;li&gt;[ ] Traces identify prompt, model, tool, and state revisions.&lt;/li&gt;
&lt;li&gt;[ ] Failures report the first failing turn and causal evidence.&lt;/li&gt;
&lt;li&gt;[ ] Branches cover approval, cancellation, correction, and timeout paths.&lt;/li&gt;
&lt;li&gt;[ ] CI runs a bounded suite; deeper sampling happens nightly.&lt;/li&gt;
&lt;li&gt;[ ] Incidents feed new fixtures into the suite.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is not to prove an agent will never fail. It is to make familiar failures difficult to reintroduce—and to detect a new class of failure before it reaches a customer.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is conversation regression testing for AI agents?
&lt;/h3&gt;

&lt;p&gt;It is a repeatable test of a whole agent interaction, rather than one prompt and one reply. The test replays a realistic sequence of user turns and tool responses, then checks cross-turn rules such as context retention, permission boundaries, and task completion.&lt;/p&gt;

&lt;h3&gt;
  
  
  How is it different from an AI agent evaluation harness?
&lt;/h3&gt;

&lt;p&gt;An evaluation harness can cover many workflow-level checks. Conversation regression testing is a focused layer within that practice: it preserves known multi-turn journeys and compares behavior after a change. Its central outputs are cross-turn invariants, replay evidence, and first-failure attribution.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should conversation tests compare exact model responses?
&lt;/h3&gt;

&lt;p&gt;Usually no. Exact prose makes tests fragile. Assert deterministic product rules first, then use narrow rubrics for meaning. Exact matches are appropriate for structured output, tool arguments, required disclaimers, and other stable contracts.&lt;/p&gt;

&lt;h3&gt;
  
  
  How many conversation fixtures should a small team maintain?
&lt;/h3&gt;

&lt;p&gt;Start with 10–20. Prioritize journeys with writes, money, access, regulated information, high volume, or prior incidents. Add a fixture whenever a production failure reveals a behavior you do not want to see twice.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I run multi-turn agent tests in CI without spending too much?
&lt;/h3&gt;

&lt;p&gt;Yes. Mock external tools, run deterministic checks on every pull request, keep the PR suite small, and reserve repeated simulations for nightly or release-candidate runs. Fork from saved state instead of regenerating shared conversation prefixes.&lt;/p&gt;

&lt;h3&gt;
  
  
  What should make a conversation test block a deployment?
&lt;/h3&gt;

&lt;p&gt;Block on deterministic failures involving permissions, tenant boundaries, unintended writes, unsafe tool calls, invalid structured output, or required policy behavior. Treat subjective quality-score changes as warnings unless they cross a deliberate, reviewed threshold.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>testing</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>AI Agent Idempotency: Prevent Duplicate Charges, Emails, and Records</title>
      <dc:creator>Jack M</dc:creator>
      <pubDate>Wed, 16 Sep 2026 09:34:47 +0000</pubDate>
      <link>https://dev.to/jackm-singularity/ai-agent-idempotency-prevent-duplicate-charges-emails-and-records-2emk</link>
      <guid>https://dev.to/jackm-singularity/ai-agent-idempotency-prevent-duplicate-charges-emails-and-records-2emk</guid>
      <description>&lt;p&gt;A timeout is not a failed action. It is an unknown action.&lt;/p&gt;

&lt;p&gt;That distinction matters the first time an agent calls a payment API, sends a customer email, or creates a CRM record—and loses the response. If the agent retries blindly, it may double-charge a card or send the same message twice. If it does nothing, the user’s request may never finish. A prompt telling the model to “avoid duplicates” cannot settle that ambiguity.&lt;/p&gt;

&lt;p&gt;The fix is &lt;strong&gt;idempotency&lt;/strong&gt;: make each logical write happen once, even when workers crash, queues redeliver, SDKs retry, or the model asks to try again. This guide shows a practical design for putting that guarantee in the tool layer, where it can be tested and enforced.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why agents make ordinary retry bugs worse
&lt;/h2&gt;

&lt;p&gt;Every distributed system has ambiguous failures. A request may reach the target, commit its side effect, and then lose its response. Traditional backends solve this with idempotency keys, unique constraints, durable jobs, and reconciliation.&lt;/p&gt;

&lt;p&gt;Agents amplify the problem because they are built to keep trying. A model sees &lt;code&gt;timeout&lt;/code&gt;, changes its plan, and asks the same tool to run again. Meanwhile, a queue may redeliver the job after a lease expires, and an HTTP client may retry underneath both of them. One user intent can become four writes.&lt;/p&gt;

&lt;p&gt;Treat these as distinct outcomes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Outcome&lt;/th&gt;
&lt;th&gt;What the caller knows&lt;/th&gt;
&lt;th&gt;Safe next step&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Rejected before send&lt;/td&gt;
&lt;td&gt;The target did not receive it&lt;/td&gt;
&lt;td&gt;Correct input, then retry if appropriate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Explicit failure&lt;/td&gt;
&lt;td&gt;The target responded with a permanent error&lt;/td&gt;
&lt;td&gt;Stop or request correction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Explicit success&lt;/td&gt;
&lt;td&gt;The target returned a durable receipt&lt;/td&gt;
&lt;td&gt;Store and return the receipt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unknown commit state&lt;/td&gt;
&lt;td&gt;The request may have succeeded&lt;/td&gt;
&lt;td&gt;Reconcile before retrying&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The last row is the important one. A timeout after send must never be silently mapped to “failed.”&lt;/p&gt;

&lt;h2&gt;
  
  
  The design rule: one key per logical action
&lt;/h2&gt;

&lt;p&gt;An idempotency key identifies the business action, not a network attempt.&lt;/p&gt;

&lt;p&gt;For example, “send the approved invoice email for invoice &lt;code&gt;inv_123&lt;/code&gt; revision 4” is one action. It should keep the same key if a worker restarts five times. A later resend after a user edits the invoice is a new action and needs a new key.&lt;/p&gt;

&lt;p&gt;Good keys are stable, scoped, and inspectable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tenant:acme | run:run_8f2 | step:email_invoice | invoice:inv_123 | revision:4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hash that canonical representation if it contains sensitive identifiers. Do not generate a new UUID on every retry; that turns a dedupe mechanism into a duplicate generator. Also do not use only the user prompt. “Email this invoice” can be a valid request more than once.&lt;/p&gt;

&lt;p&gt;An action should usually include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;tenant and actor identity&lt;/li&gt;
&lt;li&gt;workflow run and step name&lt;/li&gt;
&lt;li&gt;target resource identity and version&lt;/li&gt;
&lt;li&gt;normalized arguments or an argument hash&lt;/li&gt;
&lt;li&gt;expiry policy and final result&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Put a write firewall in front of every agent tool
&lt;/h2&gt;

&lt;p&gt;The agent should request a business action, not manipulate retry behavior directly. Place a deterministic action runner between the model and every state-changing integration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;agent plan
   -&amp;gt; tool request (business intent)
   -&amp;gt; action runner (policy + operation ledger)
   -&amp;gt; external API / database
   -&amp;gt; receipt + reconciliation result
   -&amp;gt; agent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This boundary is useful even if your tools are ordinary functions. It gives the system one place to validate tenant access, lock the operation, pass provider keys, classify failures, and hide unsafe retry choices from the model.&lt;/p&gt;

&lt;p&gt;Reads can often be retried. Writes need an operation record first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with a small operation ledger
&lt;/h2&gt;

&lt;p&gt;Postgres is enough for many teams. Create the operation before calling the external service; keep it until the action’s replay window is over.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;agent_operations&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;tenant_id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;idempotency_key&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;action_type&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;args_hash&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="k"&gt;check&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'running'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'succeeded'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'unknown'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'failed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'needs_review'&lt;/span&gt;
  &lt;span class="p"&gt;)),&lt;/span&gt;
  &lt;span class="n"&gt;provider_reference&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;result_json&lt;/span&gt; &lt;span class="n"&gt;jsonb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;error_json&lt;/span&gt; &lt;span class="n"&gt;jsonb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;lease_expires_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="k"&gt;unique&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idempotency_key&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;p&gt;The unique constraint is a hard concurrency boundary. Two workers can receive the same job, but only one can own the logical action. Store an &lt;code&gt;args_hash&lt;/code&gt; too: if the same key arrives with different arguments, fail closed. Reusing a key with changed data is almost always a caller bug.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use status as evidence, not a guess
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;pending&lt;/code&gt; means no worker has started the side effect. &lt;code&gt;running&lt;/code&gt; means a worker holds a short lease. &lt;code&gt;succeeded&lt;/code&gt; stores the exact receipt to return on a duplicate call. &lt;code&gt;unknown&lt;/code&gt; means the downstream result is ambiguous, so a reconciliation job—not an agent—must decide what happened.&lt;/p&gt;

&lt;p&gt;Avoid marking an operation &lt;code&gt;failed&lt;/code&gt; simply because the HTTP request timed out. That destroys the evidence needed to prevent a duplicate.&lt;/p&gt;

&lt;h2&gt;
  
  
  A TypeScript action wrapper
&lt;/h2&gt;

&lt;p&gt;Here is a simplified pattern. In production, the &lt;code&gt;claim&lt;/code&gt; query should use a transaction and a lease so a crashed worker can be recovered safely.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ActionResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;receiptId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;already_sent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sendInvoiceEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;invoiceId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;revision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ActionResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&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;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`invoice-email:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;invoiceId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;revision&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&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;argsHash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;revision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;revision&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;operation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;operations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;claim&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;actionType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;invoice_email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;argsHash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;leaseSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&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="nx"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;succeeded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resultJson&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="nx"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;"&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;reconcileInvoiceEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;input&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="nx"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;running&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Action is not safe to execute&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;try&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;emailProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;invoice&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;operationId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;operations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;succeed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;receiptId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messageId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&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="nf"&gt;isAmbiguousTransportError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;operations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;markUnknown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;serialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&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;reconcileInvoiceEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;operations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;serialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&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;p&gt;Notice what is missing: no model instruction decides whether an unknown write gets replayed. The wrapper returns a receipt, a verified “already completed” result, or an explicit escalation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reconcile when the provider lacks idempotency keys
&lt;/h2&gt;

&lt;p&gt;Many third-party APIs do not support idempotency keys—or claim to accept them without making them searchable. Your internal ledger still helps, but it cannot prove the external effect occurred.&lt;/p&gt;

&lt;p&gt;Make the action observable at the target. Depending on the system, reconciliation can query:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a provider object by a stored request or message ID&lt;/li&gt;
&lt;li&gt;a database row with a unique operation ID&lt;/li&gt;
&lt;li&gt;a webhook event carrying correlation metadata&lt;/li&gt;
&lt;li&gt;a target record by a natural business key and version&lt;/li&gt;
&lt;li&gt;an outbox entry that is atomically written with your local state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For an email, attach &lt;code&gt;operationId&lt;/code&gt; as provider metadata and persist the provider message ID. For a CRM create, send an external ID derived from the operation key. For an internal database mutation, use a unique &lt;code&gt;operation_id&lt;/code&gt; column or an &lt;code&gt;INSERT ... ON CONFLICT&lt;/code&gt; pattern.&lt;/p&gt;

&lt;p&gt;If reconciliation cannot prove success or failure, keep the operation in &lt;code&gt;needs_review&lt;/code&gt;. This is safer than inventing an answer. The agent can tell the user that the action is pending verification rather than claiming it completed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate retries by failure class
&lt;/h2&gt;

&lt;p&gt;“Retry three times” is too blunt. A safe policy distinguishes failure modes.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failure class&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;th&gt;Retry policy&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Safe read&lt;/td&gt;
&lt;td&gt;Search endpoint returns 503&lt;/td&gt;
&lt;td&gt;Exponential backoff with a limit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Validation error&lt;/td&gt;
&lt;td&gt;Missing required field&lt;/td&gt;
&lt;td&gt;Do not retry; return correction needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rate limit&lt;/td&gt;
&lt;td&gt;HTTP 429&lt;/td&gt;
&lt;td&gt;Wait for the provider signal, then retry same operation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ambiguous write&lt;/td&gt;
&lt;td&gt;Socket closes after request body&lt;/td&gt;
&lt;td&gt;Mark unknown and reconcile&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Known transient write failure&lt;/td&gt;
&lt;td&gt;Provider confirms no commit&lt;/td&gt;
&lt;td&gt;Retry same operation key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth or policy denial&lt;/td&gt;
&lt;td&gt;Scope removed&lt;/td&gt;
&lt;td&gt;Stop and escalate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Retry budgets should also be outside the model. Put limits on attempts, elapsed time, spend, and allowed action types. This makes a bad downstream day finite instead of turning it into an overnight retry loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the ugly paths before customers find them
&lt;/h2&gt;

&lt;p&gt;Unit tests that expect a 200 response are not enough. Build a small fault-injection suite around every write tool.&lt;/p&gt;

&lt;p&gt;Test at least these cases:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Two workers claim the same operation at once.&lt;/li&gt;
&lt;li&gt;The provider succeeds but the response is dropped.&lt;/li&gt;
&lt;li&gt;The process dies after the provider call and before &lt;code&gt;succeeded&lt;/code&gt; is stored.&lt;/li&gt;
&lt;li&gt;A queue redelivers a completed job.&lt;/li&gt;
&lt;li&gt;The same key arrives with different arguments.&lt;/li&gt;
&lt;li&gt;Reconciliation finds a target effect after a timeout.&lt;/li&gt;
&lt;li&gt;Reconciliation finds no effect and allows a same-key retry.&lt;/li&gt;
&lt;li&gt;The provider returns a permanent validation or authorization error.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;An effective assertion is simple: after any number of retries, the target contains exactly one effect for the operation key. Also test the user-visible result: a duplicate request should return the original receipt, not an opaque error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make idempotency visible in operations
&lt;/h2&gt;

&lt;p&gt;Track these counters by tenant, tool, and provider:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;duplicate calls served from the ledger&lt;/li&gt;
&lt;li&gt;operations entering &lt;code&gt;unknown&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;reconciliation success and failure rate&lt;/li&gt;
&lt;li&gt;lease expirations and takeovers&lt;/li&gt;
&lt;li&gt;key/argument mismatches&lt;/li&gt;
&lt;li&gt;age of operations waiting for review&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are more useful than raw timeout counts. A rise in unknown commits may reveal a provider regression, an overly short client timeout, or a worker shutdown problem. A rise in duplicate calls can mean your queue is redelivering as designed—or that an upstream client is misbehaving.&lt;/p&gt;

&lt;p&gt;Add an audit event each time an operation moves state. Include the actor, agent run, tool version, key hash, arguments hash, provider reference, and decision made by reconciliation. Do not store sensitive prompt or customer data just for convenience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Roll this out without freezing your product
&lt;/h2&gt;

&lt;p&gt;Start with the highest-impact tools: payments, email sends, record creation, access changes, and anything that triggers work outside your system. Inventory every write-side tool and give it an action type, stable key recipe, reconciliation strategy, and owner.&lt;/p&gt;

&lt;p&gt;Then ship in stages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Observe:&lt;/strong&gt; log proposed keys and duplicate attempts without changing behavior.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Protect internal writes:&lt;/strong&gt; add unique operation IDs and return stored receipts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Protect provider writes:&lt;/strong&gt; pass stable provider idempotency keys and record references.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reconcile ambiguity:&lt;/strong&gt; route timeout-after-send cases through target checks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enforce:&lt;/strong&gt; reject direct write tools that bypass the action runner.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is a better investment than ever more prompt rules. Prompts can help an agent choose a valid action; they cannot offer exactly-once delivery across a network.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is AI agent idempotency?
&lt;/h3&gt;

&lt;p&gt;AI agent idempotency means repeating the same logical agent action produces one durable effect, not multiple ones. It protects state-changing tools from retries, worker crashes, duplicate queue messages, and repeated model tool calls.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should every agent tool use an idempotency key?
&lt;/h3&gt;

&lt;p&gt;Every state-changing tool should have an idempotency strategy. Read-only tools can normally use conventional retry policies. For writes, use provider keys, internal unique constraints, or an operation ledger plus reconciliation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can a system prompt prevent duplicate agent actions?
&lt;/h3&gt;

&lt;p&gt;No. A prompt influences model behavior but cannot coordinate concurrent workers, recover a lost response, or prove whether a remote API committed an action. Enforce deduplication in the action runner and target system.&lt;/p&gt;

&lt;h3&gt;
  
  
  What should happen after a timeout on a write tool?
&lt;/h3&gt;

&lt;p&gt;Treat it as an unknown commit state. Record the ambiguity, query the system of record using a correlation ID or business key, and retry only if you can establish that no effect occurred.&lt;/p&gt;

&lt;h3&gt;
  
  
  How long should idempotency records be retained?
&lt;/h3&gt;

&lt;p&gt;Keep them at least as long as every possible replay window: queue retention, client retries, scheduled job retries, and provider webhook delays. High-risk actions such as payments often need longer retention and durable audit evidence.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is idempotency the same as exactly-once delivery?
&lt;/h3&gt;

&lt;p&gt;No. Most infrastructure offers at-least-once delivery. Idempotency makes repeated delivery safe by ensuring the receiver commits a logical action once and returns the original result to later attempts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make unknown explicit
&lt;/h2&gt;

&lt;p&gt;Reliable agent systems do not pretend a timeout means failure. They preserve the operation, reconcile the target, and only then decide whether a retry is safe. Give every write tool a stable identity and a durable receipt, and an agent can be persistent without becoming destructive.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>saas</category>
      <category>agents</category>
      <category>backend</category>
    </item>
  </channel>
</rss>
