<?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>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>
    <item>
      <title>AI-Adjusted Gross Margin: The Metric That Exposes Your AI Feature’s Real Cost</title>
      <dc:creator>Jack M</dc:creator>
      <pubDate>Fri, 11 Sep 2026 03:26:01 +0000</pubDate>
      <link>https://dev.to/jackm-singularity/ai-adjusted-gross-margin-the-metric-that-exposes-your-ai-features-real-cost-dei</link>
      <guid>https://dev.to/jackm-singularity/ai-adjusted-gross-margin-the-metric-that-exposes-your-ai-features-real-cost-dei</guid>
      <description>&lt;p&gt;A feature can look healthy in a product dashboard while quietly becoming more expensive every time a customer uses it.&lt;/p&gt;

&lt;p&gt;That is the trap with AI features. A team sees rising usage, a good demo, and a familiar subscription price. Meanwhile, inference, retries, vector retrieval, tool calls, and human review pile up beneath the surface. The feature is popular—but its economics may be moving in the wrong direction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI-adjusted gross margin&lt;/strong&gt; makes that visible. It treats customer-facing AI costs as part of the cost of delivering the product, then connects those costs to the tenant, feature, and outcome that created them. This guide shows how to calculate it, instrument it, and use it without turning a small engineering team into a finance department.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The practical payoff: you will know which AI workflows are worth scaling, which need a cheaper architecture, and which need a different pricing or usage boundary before margin erosion becomes a surprise.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why normal gross margin misses the important part
&lt;/h2&gt;

&lt;p&gt;Traditional software margins are attractive partly because an additional customer often adds little delivery cost. AI changes that shape. Each successful user action may trigger variable work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;input and output tokens&lt;/li&gt;
&lt;li&gt;model hosting or inference requests&lt;/li&gt;
&lt;li&gt;embeddings and vector retrieval&lt;/li&gt;
&lt;li&gt;reranking, OCR, speech, image, or browser actions&lt;/li&gt;
&lt;li&gt;workflow orchestration, tracing, and evaluation&lt;/li&gt;
&lt;li&gt;human review or support caused by weak outputs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A model bill is not the whole story. A low-cost call that causes two retries and a support ticket is not cheap. Conversely, a higher-cost workflow can be excellent economics if it completes a valuable job on the first pass.&lt;/p&gt;

&lt;p&gt;The core question is therefore not, “What did we spend on models?” It is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“After every direct AI delivery cost, does this feature still produce the margin we expect?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That question matters now because agentic workflows expand the number of billable steps. Current developer tooling emphasizes orchestration, web context, agents, and observability; each can be useful, but each can also create more variable cost. Recent AI finance guidance has also started separating AI-specific COGS from general operating spend. Builders need the same discipline at implementation level.&lt;/p&gt;

&lt;h2&gt;
  
  
  The AI-adjusted gross margin formula
&lt;/h2&gt;

&lt;p&gt;Use a consistent definition before collecting data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AI-adjusted gross margin =
  (AI feature revenue − traditional delivery COGS − AI delivery COGS)
  ÷ AI feature revenue × 100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AI feature revenue&lt;/strong&gt; is revenue you can reasonably attribute to the feature, plan, or usage tier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traditional delivery COGS&lt;/strong&gt; includes direct hosting, payment processing, and support costs already recognized as delivery costs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI delivery COGS&lt;/strong&gt; includes direct customer-facing inference and AI infrastructure costs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is a small monthly example:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Amount&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Attributed AI feature revenue&lt;/td&gt;
&lt;td&gt;$20,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Traditional delivery COGS&lt;/td&gt;
&lt;td&gt;$2,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model inference&lt;/td&gt;
&lt;td&gt;$4,800&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Embeddings and vector search&lt;/td&gt;
&lt;td&gt;$550&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool and workflow execution&lt;/td&gt;
&lt;td&gt;$900&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI observability and evaluation&lt;/td&gt;
&lt;td&gt;$450&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Human review directly required by the feature&lt;/td&gt;
&lt;td&gt;$1,300&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AI-adjusted gross margin&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;50%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Calculation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;($20,000 − $2,000 − $4,800 − $550 − $900 − $450 − $1,300) / $20,000
= 50%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not use this number to judge an entire company on day one. Start with one feature or workflow. A document extraction queue, research assistant, support copilot, or AI-generated report is a workable unit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decide what belongs in AI COGS
&lt;/h2&gt;

&lt;p&gt;Consistency matters more than a perfect universal rule. Work with whoever owns finance and keep a short written policy.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Include as direct AI COGS&lt;/th&gt;
&lt;th&gt;Usually exclude or allocate separately&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Production inference used for a customer request&lt;/td&gt;
&lt;td&gt;Experimental model research&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-request embeddings, reranking, OCR, speech, or image generation&lt;/td&gt;
&lt;td&gt;General engineering salaries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Customer-facing vector database reads and storage when material&lt;/td&gt;
&lt;td&gt;Internal developer AI subscriptions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-run browser, data, or tool API fees&lt;/td&gt;
&lt;td&gt;Broad brand marketing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Production workflow and observability charges tied to use&lt;/td&gt;
&lt;td&gt;One-time architecture work&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Required human review per delivered result&lt;/td&gt;
&lt;td&gt;Unrelated support work&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two judgment calls deserve care.&lt;/p&gt;

&lt;p&gt;First, do not put every engineering expense into a per-feature margin calculation. That makes the metric noisy and hides the variable levers engineers can actually change. Second, do not pretend AI observability is free if it scales directly with production workload. If traces are required to operate the service safely, they are a delivery cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with an event ledger, not invoices
&lt;/h2&gt;

&lt;p&gt;Vendor invoices arrive late and rarely explain why a tenant became expensive. Build an event ledger at request time instead. Each AI workflow should emit a normalized cost event after every metered step.&lt;/p&gt;

&lt;p&gt;A useful minimal record 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;AiCostEvent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&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;workflowId&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;feature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;report&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;assistant&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;extractor&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;provider&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;costType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;inference&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;embedding&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&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;vector&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;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;quantity&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;unitCostUsd&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;estimatedCostUsd&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;outcome&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;accepted&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&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="o"&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="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;recordCost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AiCostEvent&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aiCostEvents&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;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;estimatedCostUsd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;estimatedCostUsd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&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 &lt;code&gt;runId&lt;/code&gt; is vital. It joins a model call to tool use, retries, a user acceptance signal, and any review task. Without it, teams only see a provider total and start guessing.&lt;/p&gt;

&lt;p&gt;Use an estimated rate during the request, then reconcile it against provider invoices later. Keep both values. Estimate is for fast controls; billed cost is for accounting accuracy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attribute cost to the right customer and feature
&lt;/h2&gt;

&lt;p&gt;A single model gateway makes attribution easier, but it is not required. The rule is simple: every customer-facing AI call receives a tenant ID, feature ID, run ID, and cost center before it is dispatched.&lt;/p&gt;

&lt;p&gt;Avoid these common shortcuts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One shared “AI” bucket.&lt;/strong&gt; It cannot tell you whether search, chat, or extraction is causing erosion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only logging tokens.&lt;/strong&gt; Tool APIs, retrieval, retries, and review can matter as much as tokens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Using user ID alone.&lt;/strong&gt; A user may belong to multiple workspaces; billable ownership is often the tenant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treating a retry as a separate success.&lt;/strong&gt; Retries are part of the cost of the original job.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For shared platform costs, pick a transparent allocation key. Examples include successful workflows, retrieval requests, stored vectors, or active tenants. Document the key and avoid changing it every week; otherwise trend comparisons become meaningless.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add outcome quality to the margin view
&lt;/h2&gt;

&lt;p&gt;Margin without outcome quality creates a perverse incentive: make the answer cheaper even if customers have to repair it.&lt;/p&gt;

&lt;p&gt;Track at least these four companion metrics:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;What it reveals&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cost per successful outcome&lt;/td&gt;
&lt;td&gt;The true delivery cost of accepted work&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retry rate&lt;/td&gt;
&lt;td&gt;Whether cheap first attempts create expensive loops&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Escalation rate&lt;/td&gt;
&lt;td&gt;Human labor hidden behind automation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI-adjusted gross margin by tenant cohort&lt;/td&gt;
&lt;td&gt;Whether heavy users improve or damage unit economics&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For example, Workflow A may cost $0.08 per request and Workflow B $0.18. If A succeeds 40% of the time and B succeeds 90% of the time, their cost per accepted outcome is $0.20 and $0.20 respectively—before support effort. The “cheaper” option is not automatically better.&lt;/p&gt;

&lt;p&gt;This is the practical gap in many margin explainers: they describe the formula but stop before showing engineers how to connect cost to a real accepted outcome.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build a margin dashboard engineers can act on
&lt;/h2&gt;

&lt;p&gt;A useful dashboard should answer a decision, not decorate a board slide. Start with five cuts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Feature:&lt;/strong&gt; Which workflow produces or loses margin?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tenant cohort:&lt;/strong&gt; Are trials, paid plans, or power users behaving differently?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model and route:&lt;/strong&gt; Is a fallback or reasoning route driving spend?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Outcome:&lt;/strong&gt; Are retries and escalations concentrating in one step?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time:&lt;/strong&gt; Did a prompt, model, retrieval, or pricing release move the trend?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A compact query could look 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;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;feature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;date_trunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'week'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;occurred_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;week&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;estimated_cost_usd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;ai_cogs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;runs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&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="n"&gt;FILTER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;outcome&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'accepted'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;accepted_events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;estimated_cost_usd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;
    &lt;span class="k"&gt;NULLIF&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;COUNT&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="n"&gt;FILTER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;outcome&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'accepted'&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="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;cost_per_accepted_event&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;ai_cost_events&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;week&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ai_cogs&lt;/span&gt; &lt;span class="k"&gt;DESC&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 not the final finance report. It is an engineering control surface. Pair it with a revenue table and your stated allocation policy to calculate the full AI-adjusted gross margin by feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use margin controls before usage becomes a bill shock
&lt;/h2&gt;

&lt;p&gt;Once cost is observable, put guardrails near the workflow—not only at the invoice stage.&lt;/p&gt;

&lt;h3&gt;
  
  
  Set a budget per run
&lt;/h3&gt;

&lt;p&gt;Give each run a maximum cost based on task value and plan. The budget should include the expected fallback and tool path, not just the first model call.&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ledger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;runCost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;runId&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;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxRunCostUsd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&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;pause&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;runId&lt;/span&gt;&lt;span class="p"&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;budget_exceeded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;nextAction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;return_partial_result_or_request_approval&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;A pause is often better than a hard error. It lets a customer choose a lower-cost answer, wait for approval, or narrow the request.&lt;/p&gt;

&lt;h3&gt;
  
  
  Route by task, not by habit
&lt;/h3&gt;

&lt;p&gt;Use the smallest reliable model and retrieval path for each task class. A classification, extraction, long-form reasoning task, and tool-driven workflow do not deserve the same default. Evaluate quality before changing routes, then watch cost per successful outcome—not raw token cost—after rollout.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cache stable work carefully
&lt;/h3&gt;

&lt;p&gt;Prompt caching, embedding reuse, deterministic extraction results, and tool-result caching can reduce direct AI COGS. Cache only when tenant permissions, source freshness, and personalization are preserved. A cheap stale answer can create more expensive correction work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Put limits on fan-out
&lt;/h3&gt;

&lt;p&gt;Agent workflows can multiply cost through parallel searches, repeated tools, and self-repair loops. Set explicit caps for tool calls, retrieved chunks, retries, and model turns. Log the cap that stopped a run so you can distinguish a true product limit from a model failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Review the metric on a useful cadence
&lt;/h2&gt;

&lt;p&gt;Daily alerts are for fast failures: a route regression, runaway tenant, or tool loop. Weekly reviews are for engineering choices. Monthly reviews are for pricing, packaging, and capacity decisions.&lt;/p&gt;

&lt;p&gt;A strong weekly review asks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which feature had the largest AI COGS increase, and why?&lt;/li&gt;
&lt;li&gt;Did success rate change with cost?&lt;/li&gt;
&lt;li&gt;Which tenants exceeded their expected usage envelope?&lt;/li&gt;
&lt;li&gt;Did a model, prompt, or retrieval change improve cost per successful outcome?&lt;/li&gt;
&lt;li&gt;Is a temporary promotion, trial, or free tier masking the steady-state margin?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not automatically restrict the highest-cost tenant. First see whether the tenant is also the most valuable, whether their workflow is unusually successful, and whether the problem is a product design issue such as unlimited retries.&lt;/p&gt;

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

&lt;p&gt;Start small and improve the data in layers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week one: define.&lt;/strong&gt; Pick one paid AI workflow, define revenue attribution, write the COGS policy, and name an accepted outcome.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week two: instrument.&lt;/strong&gt; Add the cost event ledger at the model gateway and major tools. Capture tenant, feature, run, route, quantity, and estimated cost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week three: reconcile.&lt;/strong&gt; Compare estimated provider totals with billed totals. Fix rate cards, rounding, and missing events.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week four: control.&lt;/strong&gt; Add per-run budgets, retry caps, and a feature-level margin dashboard. Test limits with representative customer flows.&lt;/p&gt;

&lt;p&gt;The goal is not perfect accounting. It is a trustworthy feedback loop that informs product and architecture decisions while there is still time to make them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this fits in a production AI architecture
&lt;/h2&gt;

&lt;p&gt;AI-adjusted gross margin belongs in the &lt;strong&gt;production AI architecture&lt;/strong&gt; pillar, alongside model routing, observability, evaluation, and outcome measurement. It is a middle-funnel implementation topic: readers are past the prototype and need an operating model.&lt;/p&gt;

&lt;p&gt;Useful companion topics include an LLM gateway control plane, an AI outcome conversion metric, agent observability, and agent cost forecasting. Together they form a content cluster around reliable AI economics: route work well, measure the full cost, verify the outcome, and act on the signal.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  What is AI-adjusted gross margin?
&lt;/h3&gt;

&lt;p&gt;AI-adjusted gross margin is gross margin after subtracting direct AI delivery costs such as inference, embeddings, retrieval, tools, evaluation, and required review from attributed feature revenue.&lt;/p&gt;

&lt;h3&gt;
  
  
  How is AI-adjusted gross margin different from cost per request?
&lt;/h3&gt;

&lt;p&gt;Cost per request measures one technical event. AI-adjusted gross margin connects all direct delivery costs to revenue. It can reveal that a cheap request is unprofitable after retries, support, and related AI infrastructure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should AI observability be included in AI COGS?
&lt;/h3&gt;

&lt;p&gt;Include it when the cost scales directly with production AI usage and is necessary to operate the customer-facing feature. Keep the policy consistent and separate broad, fixed engineering tooling where appropriate.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I calculate AI revenue for a bundled feature?
&lt;/h3&gt;

&lt;p&gt;Use a documented attribution rule, such as a separate add-on price, usage revenue, a plan allocation, or a controlled cohort comparison. The goal is a stable decision metric, not false precision.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is a good AI-adjusted gross margin?
&lt;/h3&gt;

&lt;p&gt;There is no universal target. Compare the metric with your product’s required margin, the value created for customers, and the trend over time. A lower margin can be acceptable for a high-value workflow; an unexplained downward trend is not.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can small teams measure this without a finance system?
&lt;/h3&gt;

&lt;p&gt;Yes. Start with an event ledger, a simple revenue export, and monthly invoice reconciliation. One well-instrumented workflow is more useful than a broad dashboard built on guessed allocations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why track cost per successful outcome too?
&lt;/h3&gt;

&lt;p&gt;It protects quality. Without it, teams can optimize for cheaper model calls that generate more retries, edits, and human escalations. Successful outcomes connect cost to the work customers actually value.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>saas</category>
      <category>analytics</category>
      <category>devops</category>
    </item>
    <item>
      <title>AI Outcome Conversion Metric: Measure Work That Actually Gets Finished</title>
      <dc:creator>Jack M</dc:creator>
      <pubDate>Wed, 09 Sep 2026 09:10:22 +0000</pubDate>
      <link>https://dev.to/jackm-singularity/ai-outcome-conversion-metric-measure-work-that-actually-gets-finished-2jl4</link>
      <guid>https://dev.to/jackm-singularity/ai-outcome-conversion-metric-measure-work-that-actually-gets-finished-2jl4</guid>
      <description>&lt;p&gt;A low token bill can hide an expensive AI workflow.&lt;/p&gt;

&lt;p&gt;If an agent makes 1,000 cheap attempts but only 200 create a result a user accepts, the number that matters is not cost per request. It is &lt;strong&gt;cost per successful outcome&lt;/strong&gt;. That difference is where many AI products lose trust: a dashboard says the model is fast and affordable while users keep retrying, escalating, or fixing its work.&lt;/p&gt;

&lt;p&gt;This guide shows how to build an &lt;strong&gt;AI outcome conversion metric&lt;/strong&gt;: a practical way to measure whether an AI workflow finishes useful work, why it fails when it does not, and what to change next. It is designed for teams building support agents, document workflows, research assistants, coding helpers, or internal automations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why request metrics are not enough
&lt;/h2&gt;

&lt;p&gt;Most AI telemetry starts with easy numbers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Requests&lt;/li&gt;
&lt;li&gt;Tokens&lt;/li&gt;
&lt;li&gt;Latency&lt;/li&gt;
&lt;li&gt;Model errors&lt;/li&gt;
&lt;li&gt;Tool-call count&lt;/li&gt;
&lt;li&gt;Cost per request&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keep those numbers. They are operationally useful. But they do not answer the question a user has after pressing Run:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Did the workflow complete the job well enough to use?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A low-latency answer can still be wrong. A completed tool call can still update the wrong record. A cheaper model can create more retries and more human cleanup. And a workflow that finishes without an explicit user rejection can still be abandoned.&lt;/p&gt;

&lt;p&gt;The missing link is an outcome definition that sits above the model call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define the AI outcome conversion metric
&lt;/h2&gt;

&lt;p&gt;At its simplest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AI outcome conversion rate = accepted successful outcomes / eligible AI attempts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two important words are &lt;strong&gt;accepted&lt;/strong&gt; and &lt;strong&gt;eligible&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;An &lt;em&gt;eligible attempt&lt;/em&gt; is a user- or system-started workflow that had a fair chance to finish. Exclude events such as a user immediately cancelling before input is captured, a planned maintenance window, or an upstream outage that prevented the job from starting.&lt;/p&gt;

&lt;p&gt;An &lt;em&gt;accepted successful outcome&lt;/em&gt; is a result that meets the workflow's agreed completion rule. That rule differs by job:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Workflow&lt;/th&gt;
&lt;th&gt;A useful outcome could be&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Support assistant&lt;/td&gt;
&lt;td&gt;A customer confirms the answer, or does not reopen the issue within a defined window&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Document extractor&lt;/td&gt;
&lt;td&gt;Required fields pass validation and are accepted downstream&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Research agent&lt;/td&gt;
&lt;td&gt;A brief includes verifiable sources and the user keeps it without a major rewrite&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coding helper&lt;/td&gt;
&lt;td&gt;A change passes tests, review, and deployment checks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Invoice matcher&lt;/td&gt;
&lt;td&gt;The match meets confidence and policy rules, then is posted or approved&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Do not let the model declare its own success. A model can report &lt;code&gt;done&lt;/code&gt; after producing fluent text, even when the work is incomplete. Success should come from a deterministic check, a human decision, a downstream event, or a carefully defined combination.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hook that makes this metric useful
&lt;/h2&gt;

&lt;p&gt;The practical trigger is a surprising contrast: &lt;strong&gt;AI usage can rise while useful work falls.&lt;/strong&gt; That gives this metric strong relevance for builders under pressure to reduce cost, increase automation, and protect quality at the same time.&lt;/p&gt;

&lt;p&gt;Recent AI operations discussion is converging on outcome-level measurement because inference creates cost on every attempt, while value only arrives on a completed and accepted result. The content gap is practical: many articles explain token observability, model benchmarks, or generic business conversion. Far fewer show developers how to define an outcome object, capture acceptance evidence, segment failures, and use the metric as a rollout gate.&lt;/p&gt;

&lt;p&gt;That is why this is an implementation guide, not a dashboard tour.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with one narrow workflow contract
&lt;/h2&gt;

&lt;p&gt;Do not begin with “measure all AI.” Choose one workflow that has a visible finish line. A narrow contract prevents vague reporting and lets you improve something real.&lt;/p&gt;

&lt;p&gt;For example, imagine a support workflow that answers billing-plan questions. Define it like 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;"workflow"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"billing_answer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"eligible_when"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"a signed-in user submits a question with a valid account context"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"success_when"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"the answer is policy-grounded and the issue is not reopened within 72 hours"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"failure_when"&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="s2"&gt;"no_answer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"unsupported_claim"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"human_escalation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"user_reopen"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"policy_block"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"timeout"&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;"owner"&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-platform"&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_window_hours"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;72&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;This contract does three jobs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It turns “good answer” into a testable definition.&lt;/li&gt;
&lt;li&gt;It makes failure categories visible before you have a graph.&lt;/li&gt;
&lt;li&gt;It gives product, support, and engineering one shared vocabulary.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Use a conservative first definition: undercount success rather than count polished failures as wins.&lt;/p&gt;

&lt;h2&gt;
  
  
  Record an outcome object, not only a trace
&lt;/h2&gt;

&lt;p&gt;A trace is excellent for debugging a model call. An outcome object connects several traces, tool calls, user actions, and downstream checks into one unit of work.&lt;/p&gt;

&lt;p&gt;Here is a compact schema:&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;Outcome&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;outcomeId&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;workflow&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_answer&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;document_extract&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;code_change&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;startedAt&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;completedAt&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;eligible&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;succeeded&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="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;abandoned&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;excluded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;successEvidence&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;user_accept&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;downstream_validation&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;human_approval&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;no_reopen&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;observedAt&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;reference&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;failureReason&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;modelRoute&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;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;retrievalVersion&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="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;inputTokens&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;outputTokens&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;toolCostCents&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;humanMinutes&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few design choices matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Store a tenant identifier so you can find a workflow that works overall but fails for one customer segment.&lt;/li&gt;
&lt;li&gt;Store versions. Without a prompt, retrieval, tool-schema, and model-route version, you cannot explain movement after a release.&lt;/li&gt;
&lt;li&gt;Store only evidence references where possible. Avoid copying sensitive prompts, source documents, or customer content into a broad analytics table.&lt;/li&gt;
&lt;li&gt;Keep &lt;code&gt;abandoned&lt;/code&gt; separate from &lt;code&gt;failed&lt;/code&gt;. It is a signal, but its cause may be unclear.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Count success with a state machine
&lt;/h2&gt;

&lt;p&gt;AI workflows are often asynchronous. A user may see a draft immediately, approve it later, and reopen the task tomorrow. Treat outcome status as a state machine rather than a single boolean.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;eligible -&amp;gt; running -&amp;gt; delivered -&amp;gt; pending_evidence
                                 |             |
                                 v             v
                              failed       succeeded
                                 |
                                 v
                              escalated
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a support workflow, delivery is not success. A record moves to &lt;code&gt;pending_evidence&lt;/code&gt; until the user accepts it, a policy validator approves it, or the no-reopen window ends. For a coding workflow, delivery may be a pull request, while success requires checks and a merge.&lt;/p&gt;

&lt;p&gt;This design stops an all-too-common reporting error: calling every generated response a conversion.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add failure reasons before optimizing models
&lt;/h2&gt;

&lt;p&gt;A single “failure” bucket creates busy work. Teams argue about which model is better while different problems are mixed together. Use a small, mutually understandable taxonomy.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failure reason&lt;/th&gt;
&lt;th&gt;What it usually means&lt;/th&gt;
&lt;th&gt;First place to inspect&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;insufficient_context&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The workflow lacked a needed fact or permission&lt;/td&gt;
&lt;td&gt;retrieval, source freshness, access rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;unsupported_claim&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The output could not prove an important statement&lt;/td&gt;
&lt;td&gt;grounding checks, citations, policy rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tool_failure&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;An integration failed or returned unusable data&lt;/td&gt;
&lt;td&gt;tool contract, retry design, provider health&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;policy_block&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A safe guardrail stopped the request&lt;/td&gt;
&lt;td&gt;scope, UX, approval path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;timeout&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The workflow ran out of time&lt;/td&gt;
&lt;td&gt;fan-out, queueing, model route, tool latency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;human_rewrite&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The result was usable only after material edits&lt;/td&gt;
&lt;td&gt;task spec, examples, evaluation set&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;user_reopen&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The result looked complete but did not solve the job&lt;/td&gt;
&lt;td&gt;outcome definition, quality evaluation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Do not use failure reasons to blame a model. Their purpose is to route work. If &lt;code&gt;insufficient_context&lt;/code&gt; dominates, changing models is probably not your first move. If &lt;code&gt;tool_failure&lt;/code&gt; dominates, improve the tool contract and retry policy before tuning prompts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Calculate cost per successful outcome
&lt;/h2&gt;

&lt;p&gt;Conversion alone can be misleading. A workflow can become more successful by using a slower, more expensive model for every task. Track cost beside outcome quality:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cost per successful outcome =
  (model cost + tool cost + review cost + retry cost) / accepted successful outcomes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you estimate human review cost, be honest about the assumptions. Start with minutes of human work per attempt, even if you do not convert it to currency.&lt;/p&gt;

&lt;p&gt;A simple query illustrates the shape:&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;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;date_trunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'week'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;started_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;week&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&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="n"&gt;FILTER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'eligible'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;eligible_attempts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&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="n"&gt;FILTER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'succeeded'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;successful_outcomes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;COUNT&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="n"&gt;FILTER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'succeeded'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;
    &lt;span class="k"&gt;NULLIF&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;COUNT&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="n"&gt;FILTER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'eligible'&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="mi"&gt;3&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;outcome_conversion_rate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;input_tokens&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;output_tokens&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;token_price_cents&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;tool_cost_cents&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;
    &lt;span class="k"&gt;NULLIF&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;COUNT&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="n"&gt;FILTER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'succeeded'&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="mi"&gt;2&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;cost_per_success_cents&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;ai_outcomes&lt;/span&gt;
&lt;span class="k"&gt;WHERE&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;'eligible'&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;'failed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'abandoned'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pricing varies; allocate retries and tool costs to the final outcome. A “cheap” first attempt is not cheap if it creates two more attempts and a handoff.&lt;/p&gt;

&lt;h2&gt;
  
  
  Segment before you celebrate an average
&lt;/h2&gt;

&lt;p&gt;An overall rate can hide the risk that matters. Slice results by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tenant or plan class, with privacy-safe minimum sample sizes&lt;/li&gt;
&lt;li&gt;Workflow version&lt;/li&gt;
&lt;li&gt;Model route and fallback route&lt;/li&gt;
&lt;li&gt;Language or document type&lt;/li&gt;
&lt;li&gt;Source connector&lt;/li&gt;
&lt;li&gt;Tool availability&lt;/li&gt;
&lt;li&gt;New versus repeat user&lt;/li&gt;
&lt;li&gt;Risk tier&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Suppose overall success rises from 72% to 78%. Good news—until you see that a high-value tenant segment fell from 80% to 58% after a retrieval change. Segmentation makes the metric actionable instead of decorative.&lt;/p&gt;

&lt;p&gt;Use a sample-size threshold before reacting. Ten attempts do not justify a large architecture change. For low-volume or high-risk workflows, pair the rate with qualitative review and a small golden set.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the metric a release gate
&lt;/h2&gt;

&lt;p&gt;The strongest use of an AI outcome conversion metric is not a monthly report. It is a rollout decision.&lt;/p&gt;

&lt;p&gt;Before changing a model route, prompt, retrieval index, or tool contract:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Freeze the current outcome definition.&lt;/li&gt;
&lt;li&gt;Route a controlled slice of eligible tasks to the new version.&lt;/li&gt;
&lt;li&gt;Compare conversion, cost per success, failure mix, latency, and safety blocks.&lt;/li&gt;
&lt;li&gt;Inspect a sample of successes and failures. Numbers alone can miss a serious quality regression.&lt;/li&gt;
&lt;li&gt;Roll forward only when the new version clears a pre-agreed threshold.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A lightweight gate might be:&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;release_gate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;minimum_eligible_attempts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;
  &lt;span class="na"&gt;outcome_conversion_change&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;=&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-0.02"&lt;/span&gt;
  &lt;span class="na"&gt;cost_per_success_change&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;=&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;0.10"&lt;/span&gt;
  &lt;span class="na"&gt;unsupported_claim_rate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;=&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;baseline"&lt;/span&gt;
  &lt;span class="na"&gt;critical_policy_incidents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
  &lt;span class="na"&gt;human_review&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;required&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not a universal threshold. A low-risk meeting-summary workflow can tolerate different outcomes than a workflow that changes account data. What matters is deciding the rule before seeing the result.&lt;/p&gt;

&lt;h2&gt;
  
  
  A worked example: document extraction
&lt;/h2&gt;

&lt;p&gt;A team processes customer onboarding forms. Initially, it measures extraction latency and field-level confidence. Both look healthy. Yet operations staff keep correcting records.&lt;/p&gt;

&lt;p&gt;The team adds an outcome contract:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Eligible attempt: a document with all required pages received.&lt;/li&gt;
&lt;li&gt;Success: every required field passes deterministic validation and an operator accepts the record without editing a required field.&lt;/li&gt;
&lt;li&gt;Failure: missing source, invalid field, low confidence, operator rewrite, timeout, or policy block.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After two weeks, the outcome conversion rate is 61%. The failure mix shows 18% &lt;code&gt;insufficient_context&lt;/code&gt; from poor scans, 12% &lt;code&gt;human_rewrite&lt;/code&gt; on addresses, and 5% tool timeouts.&lt;/p&gt;

&lt;p&gt;That result changes the roadmap. Instead of immediately switching models, the team adds image-quality checks, a document-type router, and address normalization. The next experiment may still use a different model, but now it has a clear job to beat: improve accepted records without raising cost per successful record.&lt;/p&gt;

&lt;h2&gt;
  
  
  Avoid four metric traps
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Treating thumbs-up as the only acceptance signal
&lt;/h3&gt;

&lt;p&gt;Feedback is useful but sparse and biased toward strong opinions. Combine explicit feedback with downstream validation, reopen events, approvals, and sampled review.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Counting an automated action as a successful outcome
&lt;/h3&gt;

&lt;p&gt;A tool may return HTTP 200 while creating a duplicate ticket, sending an incomplete report, or choosing the wrong account. Verify the business effect, not just the API result.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Hiding safety blocks in the denominator
&lt;/h3&gt;

&lt;p&gt;A policy block can be a healthy result when it prevents unsafe work. Report it separately so you can improve the user path without pressuring the system to take unsafe actions.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Optimizing for the rate alone
&lt;/h3&gt;

&lt;p&gt;A team can raise conversion by narrowing eligibility until only easy tasks remain. Publish eligibility volume beside the rate. Success at a tiny fraction of real work is not progress.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;[ ] Pick one workflow with a clear, user-visible finish line.&lt;/li&gt;
&lt;li&gt;[ ] Write an eligible-attempt rule and a success rule.&lt;/li&gt;
&lt;li&gt;[ ] Add an outcome object that links versions, cost, evidence, and failure reason.&lt;/li&gt;
&lt;li&gt;[ ] Keep abandoned, excluded, blocked, and failed states distinct.&lt;/li&gt;
&lt;li&gt;[ ] Define a short failure taxonomy people can act on.&lt;/li&gt;
&lt;li&gt;[ ] Measure cost per successful outcome alongside conversion.&lt;/li&gt;
&lt;li&gt;[ ] Segment by version, tenant-safe cohort, route, source, and risk tier.&lt;/li&gt;
&lt;li&gt;[ ] Inspect samples before calling an experiment a win.&lt;/li&gt;
&lt;li&gt;[ ] Use a pre-agreed release gate for material changes.&lt;/li&gt;
&lt;li&gt;[ ] Review outcome definitions as the workflow and customer expectations change.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The next metric to build
&lt;/h2&gt;

&lt;p&gt;Once you can count accepted outcomes, add &lt;strong&gt;time to successful outcome&lt;/strong&gt;. This tells you whether a workflow becomes slower through retries, queues, or human handoff even when it eventually succeeds.&lt;/p&gt;

&lt;p&gt;Then connect it to your existing reliability work: traces explain a failed attempt, source health explains broken context, and approval records explain a blocked action. The outcome metric tells you whether those systems are creating useful work for the person who started the job.&lt;/p&gt;

&lt;p&gt;That is the durable goal: not more AI activity, but more completed work that users can trust.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  What is an AI outcome conversion metric?
&lt;/h3&gt;

&lt;p&gt;It is the percentage of eligible AI workflow attempts that create a defined, accepted successful result. It measures useful completed work rather than only requests, tokens, or model latency.&lt;/p&gt;

&lt;h3&gt;
  
  
  How is outcome conversion different from model accuracy?
&lt;/h3&gt;

&lt;p&gt;Model accuracy evaluates a model response against an expected answer. Outcome conversion measures whether the full workflow solved the real task, including retrieval, tools, validation, approval, retries, and user acceptance.&lt;/p&gt;

&lt;h3&gt;
  
  
  What counts as a successful AI outcome?
&lt;/h3&gt;

&lt;p&gt;Use an observable rule tied to the workflow: a validated extracted record, an accepted support answer, a merged code change, or an approved action. Do not rely only on the model saying it completed the task.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should policy-blocked AI requests count as failures?
&lt;/h3&gt;

&lt;p&gt;Report them separately. A policy block may be a correct safety outcome, though a growing block rate can reveal unclear UX, insufficient permissions, or a workflow that needs an approval route.&lt;/p&gt;

&lt;h3&gt;
  
  
  How many attempts do I need before trusting the metric?
&lt;/h3&gt;

&lt;p&gt;There is no universal number. Use a minimum sample threshold suited to the workflow's risk and volume, and inspect representative cases. For rare or high-risk work, combine the metric with manual review and evaluation tests.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I improve a low AI outcome conversion rate?
&lt;/h3&gt;

&lt;p&gt;Start with the failure mix. Fix missing context, source quality, permissions, tool reliability, task specification, or validation rules before assuming the model is the only problem. Then test changes against cost per successful outcome and safety checks.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>saas</category>
      <category>analytics</category>
      <category>devops</category>
    </item>
    <item>
      <title>AI Incident Handoff: Keep Engineers Ready When Agents Fix Production</title>
      <dc:creator>Jack M</dc:creator>
      <pubDate>Sat, 05 Sep 2026 08:24:23 +0000</pubDate>
      <link>https://dev.to/jackm-singularity/ai-incident-handoff-keep-engineers-ready-when-agents-fix-production-37g5</link>
      <guid>https://dev.to/jackm-singularity/ai-incident-handoff-keep-engineers-ready-when-agents-fix-production-37g5</guid>
      <description>&lt;p&gt;AI incident response can look magical right up to the moment it hands you the weirdest outage your team has seen all quarter.&lt;/p&gt;

&lt;p&gt;That is the trap. If agents fix every routine alert, engineers may lose the daily reps that teach them how the system actually fails. The goal is not to reject automation. The goal is to design an &lt;strong&gt;AI incident response handoff&lt;/strong&gt; that cuts noise, preserves human judgment, and makes the next hard incident easier to solve.&lt;/p&gt;

&lt;p&gt;This guide shows a practical pattern for builders adding AI triage, remediation, or on-call copilots to production systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters Now
&lt;/h2&gt;

&lt;p&gt;AI operations tools are moving from chat summaries to active responders. A modern incident agent can read alerts, inspect logs, query traces, compare a fresh deploy, suggest a root cause, write a status update, and sometimes run a low-risk fix.&lt;/p&gt;

&lt;p&gt;That is useful. It is also risky.&lt;/p&gt;

&lt;p&gt;Recent developer conversations and AI operations articles show a clear pattern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Teams want lower MTTR, fewer false alarms, and better incident summaries.&lt;/li&gt;
&lt;li&gt;Builders are experimenting with autonomous remediation for routine failures.&lt;/li&gt;
&lt;li&gt;Security teams worry about agents taking unsafe actions during high-pressure events.&lt;/li&gt;
&lt;li&gt;SREs are asking where human approval belongs in the loop.&lt;/li&gt;
&lt;li&gt;A growing concern is skill decay: if automation handles easy incidents, humans get less practice before the rare hard one arrives.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A lot of top-ranking content focuses on tool lists, broad AI incident response benefits, or big MTTR promises. The missing practical layer is the &lt;strong&gt;handoff contract&lt;/strong&gt;: what the agent must collect, when it must stop, how it briefs a human, and how the team keeps responders sharp.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Rule: Agents Investigate, Humans Own Risk
&lt;/h2&gt;

&lt;p&gt;For production systems, treat your AI responder like a fast junior engineer with perfect stamina and imperfect judgment.&lt;/p&gt;

&lt;p&gt;Good jobs for the agent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Collect logs, metrics, traces, deploy diffs, and recent alerts&lt;/li&gt;
&lt;li&gt;Cluster duplicate incidents&lt;/li&gt;
&lt;li&gt;Find likely blast radius&lt;/li&gt;
&lt;li&gt;Suggest known runbook steps&lt;/li&gt;
&lt;li&gt;Draft status updates&lt;/li&gt;
&lt;li&gt;Execute pre-approved low-risk actions&lt;/li&gt;
&lt;li&gt;Prepare a human handoff packet&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bad jobs for the agent without controls:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deleting data&lt;/li&gt;
&lt;li&gt;Rolling back large deployments blindly&lt;/li&gt;
&lt;li&gt;Changing permissions&lt;/li&gt;
&lt;li&gt;Disabling security controls&lt;/li&gt;
&lt;li&gt;Modifying billing, quota, or tenant state&lt;/li&gt;
&lt;li&gt;Suppressing alerts without evidence&lt;/li&gt;
&lt;li&gt;Calling an incident resolved only because symptoms went quiet&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The handoff should make this boundary visible in code, not just in a prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Simple AI Incident Handoff Architecture
&lt;/h2&gt;

&lt;p&gt;Here is a practical architecture for small teams building AI operations into an app or platform.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Alert -&amp;gt; Incident Intake -&amp;gt; Evidence Collector -&amp;gt; Agent Triage
                                      |              |
                                      v              v
                                Evidence Store   Risk Scorer
                                                     |
                          +--------------------------+-------------------+
                          |                                              |
                    Low-risk action                              Human handoff
                          |                                              |
                    Verify + log                           On-call review packet
                          |                                              |
                    Close or escalate                         Approve / reject / guide
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key is that the agent does not just produce a confident sentence. It produces a structured package that another person can inspect quickly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build the Handoff Packet First
&lt;/h2&gt;

&lt;p&gt;Before you automate remediation, define the handoff packet. This becomes the shared format between the agent, the on-call engineer, your UI, and your audit log.&lt;/p&gt;

&lt;p&gt;A useful packet includes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Incident ID&lt;/td&gt;
&lt;td&gt;Links every action, note, and trace to one event&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trigger&lt;/td&gt;
&lt;td&gt;Alert name, threshold, source, and first detected time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Customer impact&lt;/td&gt;
&lt;td&gt;Tenants, regions, endpoints, jobs, or features affected&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timeline&lt;/td&gt;
&lt;td&gt;What changed before and during the incident&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Evidence&lt;/td&gt;
&lt;td&gt;Logs, metrics, traces, deploys, feature flags, queue stats&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hypotheses&lt;/td&gt;
&lt;td&gt;Ranked possible causes with supporting and opposing evidence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Confidence&lt;/td&gt;
&lt;td&gt;Why the agent thinks this is safe or uncertain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recommended action&lt;/td&gt;
&lt;td&gt;Proposed next step, not a hidden action&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Risk tier&lt;/td&gt;
&lt;td&gt;Read-only, reversible, customer-impacting, or destructive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Required approval&lt;/td&gt;
&lt;td&gt;Who must approve and why&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verification plan&lt;/td&gt;
&lt;td&gt;How success or failure will be measured&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rollback plan&lt;/td&gt;
&lt;td&gt;How to undo the action if it makes things worse&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Example Handoff Packet Schema
&lt;/h2&gt;

&lt;p&gt;You can start with JSON. Keep it strict enough for validation and flexible enough for real incidents.&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;"incident_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;"inc_2026_09_05_001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"trigger"&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;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"metrics"&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;"api_error_rate_high"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"started_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-09-05T08:05: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;"severity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sev2"&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;"impact"&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;"regions"&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;"us-east-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;"tenants_affected"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"user_visible"&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;"symptoms"&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;"checkout retries"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"slow API responses"&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;"timeline"&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="nl"&gt;"time"&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-09-05T07:52: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;"event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"deployment api-7f42 started"&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;"time"&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-09-05T08:03: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;"event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"p95 latency crossed 2s"&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;"hypotheses"&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="nl"&gt;"cause"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"new database query path from latest deployment"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"confidence"&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.72&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"supporting_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="s2"&gt;"error spike began after api-7f42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"trace span db.lookup increased"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"opposing_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="s2"&gt;"one worker pool without api-7f42 also shows minor latency"&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="nl"&gt;"recommended_action"&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;"rollback_deployment"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"api-7f42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"risk_tier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"reversible_customer_impacting"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"requires_approval"&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="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;"verification_plan"&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="s2"&gt;"watch p95 latency for 10 minutes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"confirm checkout retry rate drops below baseline + 10%"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"sample 20 traces after rollback"&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;This schema prevents the worst incident-response anti-pattern: a fluent summary with no evidence trail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Score Incident Risk Before Any Action
&lt;/h2&gt;

&lt;p&gt;The agent should not decide risk with vague labels. Use a small scoring model that combines blast radius, reversibility, confidence, and permission scope.&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;RiskTier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;read_only&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;low_reversible&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;customer_impacting&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;destructive&lt;/span&gt;&lt;span class="dl"&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;IncidentAction&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;touchesCustomers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;changesData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;reversible&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;confidence&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;// 0 to 1&lt;/span&gt;
  &lt;span class="nl"&gt;tenantsAffected&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;classifyAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;IncidentAction&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;RiskTier&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;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;changesData&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;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reversible&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;destructive&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;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;touchesCustomers&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tenantsAffected&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;5&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;customer_impacting&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;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reversible&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;action&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;gt;=&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="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;low_reversible&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;read_only&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;needsHumanApproval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;IncidentAction&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&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;tier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;classifyAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;action&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;tier&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;customer_impacting&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;tier&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;destructive&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;action&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.75&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 intentionally simple. In production, you can add tenant plan, compliance zone, time of day, on-call coverage, and recent failure history.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Four Handoff Modes
&lt;/h2&gt;

&lt;p&gt;Do not use one automation level for every incident. Use modes.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Read-Only Investigator
&lt;/h3&gt;

&lt;p&gt;The agent collects evidence and drafts hypotheses. It cannot change production.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;The system is new&lt;/li&gt;
&lt;li&gt;The runbook is untested&lt;/li&gt;
&lt;li&gt;The incident affects regulated data&lt;/li&gt;
&lt;li&gt;Confidence is low&lt;/li&gt;
&lt;li&gt;The alert is noisy or poorly understood&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the safest starting point for most AI builders.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Suggested Runbook Executor
&lt;/h3&gt;

&lt;p&gt;The agent recommends a known runbook step and prepares the command, but a human clicks approve.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;The action is familiar&lt;/li&gt;
&lt;li&gt;The rollback path is clear&lt;/li&gt;
&lt;li&gt;The command needs parameters from live evidence&lt;/li&gt;
&lt;li&gt;You want speed without silent execution&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The UI should show the exact action, expected effect, evidence, verification plan, and rollback step.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Bounded Autopilot
&lt;/h3&gt;

&lt;p&gt;The agent can run low-risk reversible actions from an allowlist.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Restart one unhealthy worker&lt;/li&gt;
&lt;li&gt;Clear a stuck job lease&lt;/li&gt;
&lt;li&gt;Scale a queue consumer within a narrow range&lt;/li&gt;
&lt;li&gt;Re-enable a known safe feature flag after health checks pass&lt;/li&gt;
&lt;li&gt;Open a pre-filled incident channel&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every action should still produce an audit log and verification receipt.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Human Command Mode
&lt;/h3&gt;

&lt;p&gt;The engineer takes control, and the agent becomes a fast assistant.&lt;/p&gt;

&lt;p&gt;Use this for ambiguous, severe, or novel incidents. The agent can answer questions, fetch evidence, compare traces, and draft notes, but it does not lead.&lt;/p&gt;

&lt;p&gt;This mode matters because the rare incident is exactly where human judgment is most valuable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design the On-Call Review Screen
&lt;/h2&gt;

&lt;p&gt;If the handoff lives only in Slack text, it will be hard to trust under pressure. Give responders a compact review screen.&lt;/p&gt;

&lt;p&gt;Show these sections first:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;What is broken?&lt;/strong&gt; Affected users, systems, regions, and severity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why does the agent think so?&lt;/strong&gt; Three strongest evidence items.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What changed recently?&lt;/strong&gt; Deploys, config, data jobs, vendor events.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What is it asking to do?&lt;/strong&gt; Exact action and risk tier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How will we know it worked?&lt;/strong&gt; Verification checks and rollback path.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Keep the first screen short. Let engineers expand raw logs and traces only when needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep Engineers Sharp With Practice Loops
&lt;/h2&gt;

&lt;p&gt;The hardest part of AI incident response is not technical. It is organizational memory.&lt;/p&gt;

&lt;p&gt;If agents close easy incidents, engineers lose chances to build intuition. Solve that with deliberate practice.&lt;/p&gt;

&lt;p&gt;Add these loops:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Shadow mode reviews:&lt;/strong&gt; The agent handles a routine incident, but the on-call engineer later reviews the packet and marks whether they agree.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weekly incident replay:&lt;/strong&gt; Pick one closed alert and ask an engineer to diagnose it from the evidence packet before seeing the agent answer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No-agent drills:&lt;/strong&gt; Run one simulated incident where responders cannot ask the agent for the first 10 minutes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hypothesis scoring:&lt;/strong&gt; Track whether the agent's top cause was correct, partially correct, or wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runbook decay checks:&lt;/strong&gt; If a runbook has not been used by a human in months, test it in staging.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Automation should remove toil, not remove learning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measure More Than MTTR
&lt;/h2&gt;

&lt;p&gt;MTTR matters, but it is not enough. If an agent closes incidents faster by hiding uncertainty, your dashboard will look better while risk grows.&lt;/p&gt;

&lt;p&gt;Track these metrics:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;What it tells you&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Time to evidence packet&lt;/td&gt;
&lt;td&gt;How fast the agent gives useful context&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Human approval rate&lt;/td&gt;
&lt;td&gt;Whether risk tiers are calibrated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rejected recommendation rate&lt;/td&gt;
&lt;td&gt;Whether the agent is overconfident&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Evidence completeness&lt;/td&gt;
&lt;td&gt;Whether packets include enough data to review&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wrong top hypothesis rate&lt;/td&gt;
&lt;td&gt;Whether triage quality is improving&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rollback success rate&lt;/td&gt;
&lt;td&gt;Whether actions are truly reversible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Practice coverage&lt;/td&gt;
&lt;td&gt;Whether humans still rehearse critical systems&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Silent recurrence rate&lt;/td&gt;
&lt;td&gt;Whether incidents return after automated closure&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Add one metric I like: &lt;strong&gt;handoff usefulness score&lt;/strong&gt;. After an incident, ask the responder to rate the packet from 1 to 5.&lt;/p&gt;

&lt;p&gt;A fast packet that engineers ignore is not useful automation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Workflow for a Small Team
&lt;/h2&gt;

&lt;p&gt;Here is a lean implementation path.&lt;/p&gt;

&lt;h3&gt;
  
  
  Week 1: Evidence-Only Packets
&lt;/h3&gt;

&lt;p&gt;Start with alerts and read-only evidence collection.&lt;/p&gt;

&lt;p&gt;Connect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Metrics provider&lt;/li&gt;
&lt;li&gt;Logs&lt;/li&gt;
&lt;li&gt;Traces&lt;/li&gt;
&lt;li&gt;Deploy history&lt;/li&gt;
&lt;li&gt;Feature flag changes&lt;/li&gt;
&lt;li&gt;Error tracking&lt;/li&gt;
&lt;li&gt;Queue or job status&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The agent should summarize, cite sources, and produce a packet. It should not recommend production changes yet.&lt;/p&gt;

&lt;h3&gt;
  
  
  Week 2: Runbook Matching
&lt;/h3&gt;

&lt;p&gt;Map alerts to runbooks.&lt;/p&gt;

&lt;p&gt;Example:&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;api_error_rate_high&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;allowed_modes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;read_only&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;suggested_runbook&lt;/span&gt;
  &lt;span class="na"&gt;evidence_required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;recent_deploys&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;error_rate_by_endpoint&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;top_exception_groups&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;trace_latency_breakdown&lt;/span&gt;
  &lt;span class="na"&gt;candidate_runbooks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;rollback_recent_api_deploy&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;disable_experimental_checkout_flag&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;scale_api_workers&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives the agent a controlled menu instead of an open-ended command line.&lt;/p&gt;

&lt;h3&gt;
  
  
  Week 3: Approval Gates
&lt;/h3&gt;

&lt;p&gt;Add human approval for customer-impacting actions. Store the reviewer, timestamp, action payload, evidence hash, and result.&lt;/p&gt;

&lt;p&gt;Do not bury approval in chat reactions. Make the approved payload explicit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Week 4: Bounded Autopilot
&lt;/h3&gt;

&lt;p&gt;Only after you have review data, allow low-risk actions. Keep limits narrow.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Maximum one worker restart per 15 minutes&lt;/li&gt;
&lt;li&gt;No database writes&lt;/li&gt;
&lt;li&gt;No tenant-wide config changes&lt;/li&gt;
&lt;li&gt;No action if evidence is older than five minutes&lt;/li&gt;
&lt;li&gt;No action if the same incident recurred twice after automation&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common Mistakes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Mistake 1: Letting the Agent Close Incidents Alone
&lt;/h3&gt;

&lt;p&gt;Closing an incident is a judgment call. The agent can suggest closure, but it should prove recovery with metrics, traces, and user-impact checks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 2: Treating Confidence as Truth
&lt;/h3&gt;

&lt;p&gt;A confidence score is a signal, not a fact. Require supporting and opposing evidence for each hypothesis.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 3: Automating Before Runbooks Are Clean
&lt;/h3&gt;

&lt;p&gt;If your runbooks are stale, the agent will automate confusion. Clean the runbook first.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 4: Hiding Raw Evidence
&lt;/h3&gt;

&lt;p&gt;Summaries are useful, but responders need links to raw logs, traces, dashboards, deploys, and commands.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 5: Ignoring Skill Decay
&lt;/h3&gt;

&lt;p&gt;If humans only appear for unusual incidents, they need more training, not less.&lt;/p&gt;

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

&lt;p&gt;Before you let an AI responder touch production, confirm:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Every incident creates a structured handoff packet&lt;/li&gt;
&lt;li&gt;[ ] Evidence links are stored and reviewable&lt;/li&gt;
&lt;li&gt;[ ] Actions are classified by risk tier&lt;/li&gt;
&lt;li&gt;[ ] Customer-impacting actions require approval&lt;/li&gt;
&lt;li&gt;[ ] Low-risk autopilot actions are allowlisted&lt;/li&gt;
&lt;li&gt;[ ] Every action has a verification plan&lt;/li&gt;
&lt;li&gt;[ ] Every action has a rollback path&lt;/li&gt;
&lt;li&gt;[ ] Humans rehearse incidents regularly&lt;/li&gt;
&lt;li&gt;[ ] Metrics include quality, not only speed&lt;/li&gt;
&lt;li&gt;[ ] The agent can say “I do not know” and escalate&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;AI incident response is valuable because it can gather context faster than a tired human at 3 a.m. But production reliability still depends on judgment, ownership, and practice.&lt;/p&gt;

&lt;p&gt;A strong &lt;strong&gt;AI incident response handoff&lt;/strong&gt; gives you the best of both sides: automation handles the repetitive evidence work, while engineers stay responsible for risky decisions. Start with read-only packets. Add runbook suggestions. Gate customer-impacting actions. Practice the incidents your agent usually solves.&lt;/p&gt;

&lt;p&gt;The goal is not an on-call team that never touches incidents. The goal is an on-call team that gets better evidence, faster decisions, and enough practice to handle the outage automation cannot.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  What is an AI incident response handoff?
&lt;/h3&gt;

&lt;p&gt;An AI incident response handoff is the structured packet and workflow an AI responder uses when escalating an incident to a human. It should include impact, timeline, evidence, hypotheses, recommended action, risk tier, approval requirement, verification plan, and rollback path.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should AI agents automatically fix production incidents?
&lt;/h3&gt;

&lt;p&gt;Only for narrow, low-risk, reversible actions with strong evidence and clear limits. Customer-impacting, destructive, permission-changing, or low-confidence actions should require human approval.&lt;/p&gt;

&lt;h3&gt;
  
  
  How is this different from an AI incident summary?
&lt;/h3&gt;

&lt;p&gt;A summary explains what happened. A handoff packet supports a decision. It includes evidence links, opposing signals, risk classification, exact action payloads, and verification steps.&lt;/p&gt;

&lt;h3&gt;
  
  
  What metrics should teams track for AI incident response?
&lt;/h3&gt;

&lt;p&gt;Track time to evidence packet, approval rate, rejected recommendation rate, wrong hypothesis rate, evidence completeness, rollback success, handoff usefulness, and silent recurrence. MTTR alone is not enough.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do you prevent engineers from losing incident response skill?
&lt;/h3&gt;

&lt;p&gt;Use shadow reviews, incident replay, no-agent drills, hypothesis scoring, and runbook decay checks. Automation should reduce toil while preserving practice on the systems humans still own.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the safest first step for a small team?
&lt;/h3&gt;

&lt;p&gt;Start with read-only evidence packets. Let the agent collect logs, traces, metrics, deploy history, and likely hypotheses without changing production. Add approval-gated runbook suggestions only after responders trust the packets.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>saas</category>
      <category>sre</category>
      <category>devops</category>
    </item>
    <item>
      <title>AI Source Connector Health Check: Stop Agents From Trusting Broken Data</title>
      <dc:creator>Jack M</dc:creator>
      <pubDate>Thu, 03 Sep 2026 04:26:42 +0000</pubDate>
      <link>https://dev.to/jackm-singularity/ai-source-connector-health-check-stop-agents-from-trusting-broken-data-4jd0</link>
      <guid>https://dev.to/jackm-singularity/ai-source-connector-health-check-stop-agents-from-trusting-broken-data-4jd0</guid>
      <description>&lt;p&gt;An AI agent can sound confident while reading yesterday's permissions, a half-synced CRM record, or a document connector that silently stopped crawling. That is worse than a normal outage because the UI still works, the model still answers, and users may not notice the data is wrong until trust is already damaged.&lt;/p&gt;

&lt;p&gt;If you are building AI features on top of customer data, your connectors are now part of the answer quality system. A Slack import, Google Drive sync, database replica, support-ticket feed, analytics warehouse, or MCP data tool is not just plumbing. It is the evidence layer your agent uses to decide what is true.&lt;/p&gt;

&lt;p&gt;This guide shows how to build an &lt;strong&gt;AI source connector health check&lt;/strong&gt;: a practical set of tests, scores, alerts, and fallback rules that stop agents from trusting broken data.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;No product pitch here. The pattern works whether you use managed connectors, open-source ingestion, custom sync jobs, MCP tools, RAG pipelines, or direct database access.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why connector health suddenly matters more
&lt;/h2&gt;

&lt;p&gt;Recent AI platform activity points in the same direction: more agentic workflows, more data-source connectors, more plugin-style integrations, and more pressure to measure cost per task. Product launches around AI sources and agent plugins show that builders want agents to work across real systems, not toy prompts. Developer discussions around federated query layers, integrations, MCP, and agent databases show the same demand from the technical side.&lt;/p&gt;

&lt;p&gt;That creates a quiet failure mode.&lt;/p&gt;

&lt;p&gt;Traditional software usually fails loudly when a dependency breaks. A 500 error, empty response, expired token, or failed cron job is visible. AI systems can fail softly. They retrieve fewer documents, use stale facts, skip restricted rows, quote an old policy, or answer from cached context.&lt;/p&gt;

&lt;p&gt;The model may still produce a polished response.&lt;/p&gt;

&lt;p&gt;For AI app builders, connector health affects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;answer accuracy&lt;/li&gt;
&lt;li&gt;tenant isolation&lt;/li&gt;
&lt;li&gt;retrieval quality&lt;/li&gt;
&lt;li&gt;cost per task&lt;/li&gt;
&lt;li&gt;support escalations&lt;/li&gt;
&lt;li&gt;user trust&lt;/li&gt;
&lt;li&gt;compliance evidence&lt;/li&gt;
&lt;li&gt;agent action safety&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If an agent drafts emails, updates tickets, analyzes revenue, answers policy questions, or triggers workflow actions, bad source data is not a minor bug. It becomes bad judgment at machine speed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core idea: every source needs a health contract
&lt;/h2&gt;

&lt;p&gt;A connector health check is not one ping endpoint. It is a contract that says, "This source is fresh, complete enough, permission-safe, schema-compatible, and usable for this AI task."&lt;/p&gt;

&lt;p&gt;A useful health contract has five layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Connection health&lt;/strong&gt;: Can we reach the source and authenticate?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sync health&lt;/strong&gt;: Are records arriving on time and without large gaps?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schema health&lt;/strong&gt;: Do fields still match what retrieval, prompts, and tools expect?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permission health&lt;/strong&gt;: Are tenant, user, and role filters still enforced?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Answer health&lt;/strong&gt;: Can the AI workflow answer known questions using this source?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Most teams monitor the first layer. Production AI features need all five.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical source health score
&lt;/h2&gt;

&lt;p&gt;Use a score that is simple enough for alerts and strict enough to protect users. Here is a starting model:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;Weight&lt;/th&gt;
&lt;th&gt;Failure example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Auth and API reachability&lt;/td&gt;
&lt;td&gt;15%&lt;/td&gt;
&lt;td&gt;expired OAuth token&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Freshness lag&lt;/td&gt;
&lt;td&gt;20%&lt;/td&gt;
&lt;td&gt;latest synced ticket is 9 hours old&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sync completeness&lt;/td&gt;
&lt;td&gt;15%&lt;/td&gt;
&lt;td&gt;import skipped 18% of documents&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schema compatibility&lt;/td&gt;
&lt;td&gt;15%&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;customer_id&lt;/code&gt; renamed to &lt;code&gt;account_id&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Permission filters&lt;/td&gt;
&lt;td&gt;20%&lt;/td&gt;
&lt;td&gt;user can retrieve another tenant's row&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sample answer tests&lt;/td&gt;
&lt;td&gt;15%&lt;/td&gt;
&lt;td&gt;agent cannot answer a known policy question&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A source below 90 is usable with caution. Below 80 should degrade the AI feature. Below 70 should block high-risk answers or actions.&lt;/p&gt;

&lt;p&gt;The exact numbers matter less than the behavior: the agent should know when evidence is unhealthy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Health check data model
&lt;/h2&gt;

&lt;p&gt;Start with a small table. You can expand later.&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;ai_source_health_checks&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;source_id&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;source_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;checked_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="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="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;-- healthy, degraded, blocked&lt;/span&gt;
  &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="nb"&gt;INT&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;freshness_lag_seconds&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;schema_version&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;permission_test_passed&lt;/span&gt; &lt;span class="nb"&gt;BOOLEAN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;sample_answer_passed&lt;/span&gt; &lt;span class="nb"&gt;BOOLEAN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;failure_reasons&lt;/span&gt; &lt;span class="n"&gt;JSONB&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="s1"&gt;'[]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;evidence&lt;/span&gt; &lt;span class="n"&gt;JSONB&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="s1"&gt;'{}'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_ai_source_health_latest&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;ai_source_health_checks&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;source_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;checked_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is &lt;code&gt;evidence&lt;/code&gt;. Do not store only a green or red status. Store what was checked, which sample records were used, what changed, and which workflow should degrade.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check 1: freshness lag
&lt;/h2&gt;

&lt;p&gt;Freshness is the easiest failure to miss. A connector can look healthy while serving old data.&lt;/p&gt;

&lt;p&gt;Track the source's latest update time, the latest synced record time, and the latest indexed or embedded time. Those are different clocks.&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;FreshnessResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;sourceId&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;latestSourceUpdate&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;latestSyncedRecord&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;latestIndexedRecord&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;lagSeconds&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;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;healthy&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;degraded&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;blocked&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;scoreFreshness&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lagSeconds&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="nx"&gt;maxLagSeconds&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lagSeconds&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;maxLagSeconds&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;score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&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;healthy&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;lagSeconds&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;maxLagSeconds&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3&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;score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;70&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;degraded&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;score&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="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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set freshness targets by workflow, not globally.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Support responses may need ticket data within minutes.&lt;/li&gt;
&lt;li&gt;Contract search may tolerate a few hours.&lt;/li&gt;
&lt;li&gt;Quarterly analytics summaries may tolerate a daily warehouse sync.&lt;/li&gt;
&lt;li&gt;Agent actions against production records should require current permissions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A single freshness threshold creates false confidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check 2: sync completeness
&lt;/h2&gt;

&lt;p&gt;Fresh data is not enough if the connector skipped half the source.&lt;/p&gt;

&lt;p&gt;Measure expected versus observed records:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;records seen in the source API&lt;/li&gt;
&lt;li&gt;records accepted by ingestion&lt;/li&gt;
&lt;li&gt;records rejected by validation&lt;/li&gt;
&lt;li&gt;records indexed for retrieval&lt;/li&gt;
&lt;li&gt;records removed due to permissions&lt;/li&gt;
&lt;li&gt;records too large or malformed to process&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A practical completeness check can be simple:&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;completenessRatio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;:&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="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;ingested&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;indexed&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="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;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;expected&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="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&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="nx"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ingested&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;indexed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;expected&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;scoreCompleteness&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ratio&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ratio&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.98&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;100&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;ratio&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.90&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;70&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;20&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;Also track the reason records were skipped. "Ten documents failed because they were corrupt" is different from "all private documents disappeared after a permission bug."&lt;/p&gt;

&lt;h2&gt;
  
  
  Check 3: schema compatibility
&lt;/h2&gt;

&lt;p&gt;AI features often depend on fields that are not obvious in the UI: owner IDs, timestamps, product area, customer tier, source URL, embedding text, permission tags, or lifecycle status.&lt;/p&gt;

&lt;p&gt;If a connector changes a field name, enum value, null behavior, or nested JSON shape, the model may still receive text, but the workflow logic can break.&lt;/p&gt;

&lt;p&gt;Create a schema manifest per source:&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;"source_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;"support_tickets"&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_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;"tickets.v3"&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_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="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="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="s2"&gt;"requester_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;"status"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"updated_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;"body_text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"permission_scope"&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;"enum_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="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"status"&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;"open"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pending"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"solved"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"closed"&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;Then validate samples on every sync and before major agent runs. If a required field disappears, do not let the agent guess.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check 4: permission probes
&lt;/h2&gt;

&lt;p&gt;Permission bugs are the most dangerous connector failures because retrieval can look accurate while leaking the wrong data.&lt;/p&gt;

&lt;p&gt;Run permission probes for each tenant and role pattern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A user should retrieve their own documents.&lt;/li&gt;
&lt;li&gt;A user should not retrieve another tenant's documents.&lt;/li&gt;
&lt;li&gt;A restricted user should not retrieve admin-only records.&lt;/li&gt;
&lt;li&gt;A revoked user should retrieve nothing after revocation.&lt;/li&gt;
&lt;li&gt;A service agent should only retrieve the scopes granted to that workflow.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example probe:&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;PermissionProbe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;actorId&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;query&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;mustExclude&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;runPermissionProbe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;probe&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PermissionProbe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;retrieve&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Function&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;results&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;retrieve&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;probe&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;actorId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;probe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;actorId&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="nx"&gt;probe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&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;ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;results&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;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&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;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recordId&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;leaked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;probe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mustExclude&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;id&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;ids&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;id&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;passed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;leaked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&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="nx"&gt;leaked&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;resultCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;results&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run these probes against the same retrieval path your AI feature uses. Testing only the database policy is not enough if embeddings, caches, search indexes, or tool responses bypass it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check 5: sample answer tests
&lt;/h2&gt;

&lt;p&gt;Connector health should end with a question: can the AI workflow still answer known tasks from this source?&lt;/p&gt;

&lt;p&gt;Build a tiny golden set per source:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Test question&lt;/th&gt;
&lt;th&gt;Expected evidence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Docs&lt;/td&gt;
&lt;td&gt;"What is the refund window for annual plans?"&lt;/td&gt;
&lt;td&gt;policy page URL + current section&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tickets&lt;/td&gt;
&lt;td&gt;"What are the top three billing complaints this week?"&lt;/td&gt;
&lt;td&gt;recent tagged tickets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CRM&lt;/td&gt;
&lt;td&gt;"Which renewal accounts are blocked by security review?"&lt;/td&gt;
&lt;td&gt;account records with status&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Analytics&lt;/td&gt;
&lt;td&gt;"Did activation improve after onboarding change?"&lt;/td&gt;
&lt;td&gt;metric definition + date range&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The model does not need to match exact wording. It does need to retrieve the right evidence and avoid unsupported claims.&lt;/p&gt;

&lt;p&gt;A simple judge rubric:&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;"retrieved_required_evidence"&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;"used_current_records"&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;"respected_permissions"&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;"answer_contains_unsupported_claims"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pass"&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;This is where connector health joins RAG evaluation. Retrieval metrics tell you whether the system found relevant chunks. Source health tells you whether those chunks should be trusted in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  How agents should use health status
&lt;/h2&gt;

&lt;p&gt;Do not hide source health inside dashboards only. Pass a compact health summary into the agent workflow.&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;"source_health"&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;"support_tickets"&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;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"degraded"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;76&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"freshness lag is 4h 12m; target is 30m"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"allowed_actions"&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;"draft"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"summarize_with_warning"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"blocked_actions"&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;"send_reply"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"update_ticket_status"&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="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;This lets the agent adapt:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;answer with a freshness warning&lt;/li&gt;
&lt;li&gt;ask for confirmation before acting&lt;/li&gt;
&lt;li&gt;use a fallback source&lt;/li&gt;
&lt;li&gt;switch to draft-only mode&lt;/li&gt;
&lt;li&gt;refuse high-risk actions&lt;/li&gt;
&lt;li&gt;create an internal incident note&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key rule: unhealthy evidence should reduce autonomy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Degraded UX beats silent confidence
&lt;/h2&gt;

&lt;p&gt;A good degraded state is honest and useful. Avoid vague banners like "Something went wrong." Tell the user what is safe.&lt;/p&gt;

&lt;p&gt;Better examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"I can draft a reply, but I will not send it because ticket data is 4 hours out of date."&lt;/li&gt;
&lt;li&gt;"Analytics are available through yesterday. I cannot answer questions about today's usage yet."&lt;/li&gt;
&lt;li&gt;"This answer excludes private Drive documents because the permission sync is being repaired."&lt;/li&gt;
&lt;li&gt;"I found matching records, but source health is degraded, so please review before applying changes."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Users forgive temporary limits. They do not forgive confident wrong answers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Alerting that avoids noise
&lt;/h2&gt;

&lt;p&gt;Do not page someone every time a connector has a small delay. Alert by risk and user impact.&lt;/p&gt;

&lt;p&gt;Useful alert dimensions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;affected tenants&lt;/li&gt;
&lt;li&gt;affected workflows&lt;/li&gt;
&lt;li&gt;source type&lt;/li&gt;
&lt;li&gt;action risk level&lt;/li&gt;
&lt;li&gt;freshness lag&lt;/li&gt;
&lt;li&gt;failed permission probes&lt;/li&gt;
&lt;li&gt;sample answer failures&lt;/li&gt;
&lt;li&gt;number of AI runs that used degraded data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A high-risk alert should say:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Connector health blocked: support_tickets
Tenant: acme
Reason: permission probe failed
Impact: send_reply and update_ticket_status disabled
Recent agent runs using this source: 12
Next step: rotate connector token, rebuild permission index, replay probes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is much better than "sync failed."&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this fits in your architecture
&lt;/h2&gt;

&lt;p&gt;A connector health service usually sits between ingestion and AI runtime.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Source API / DB / File Store
        ↓
Connector Sync Job
        ↓
Validation + Permission Index + Embeddings
        ↓
Source Health Checks
        ↓
AI Runtime / Agent / RAG / MCP Tool
        ↓
Answer Receipt + Logs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For small teams, this can be a scheduled job and one database table. You do not need a separate platform on day one.&lt;/p&gt;

&lt;p&gt;Start with:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;freshness lag&lt;/li&gt;
&lt;li&gt;schema checks&lt;/li&gt;
&lt;li&gt;permission probes&lt;/li&gt;
&lt;li&gt;five sample-answer tests&lt;/li&gt;
&lt;li&gt;runtime degradation rules&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then add completeness scoring, trend reports, and per-workflow thresholds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Treating OAuth success as health. A valid token only proves access, not freshness, completeness, permissions, or answer quality.&lt;/li&gt;
&lt;li&gt;Testing ingestion but not retrieval. Test the path the agent actually uses.&lt;/li&gt;
&lt;li&gt;Using one global status. A source can be healthy for summaries and unsafe for actions.&lt;/li&gt;
&lt;li&gt;Ignoring deletes and revocations. Old data must disappear from search, caches, memory, and tool results.&lt;/li&gt;
&lt;li&gt;Letting the model decide trust from text alone. Enforce hard runtime policy outside the model.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Implementation checklist
&lt;/h2&gt;

&lt;p&gt;Use this as a first sprint plan:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] List every source your AI feature can read.&lt;/li&gt;
&lt;li&gt;[ ] Define freshness targets per workflow.&lt;/li&gt;
&lt;li&gt;[ ] Store latest source, sync, and index timestamps.&lt;/li&gt;
&lt;li&gt;[ ] Add schema manifests for required fields.&lt;/li&gt;
&lt;li&gt;[ ] Track expected, ingested, rejected, and indexed records.&lt;/li&gt;
&lt;li&gt;[ ] Create permission probes for normal, restricted, revoked, and cross-tenant users.&lt;/li&gt;
&lt;li&gt;[ ] Build five sample-answer tests per critical source.&lt;/li&gt;
&lt;li&gt;[ ] Store health scores with evidence, not just status.&lt;/li&gt;
&lt;li&gt;[ ] Pass compact source health into the AI runtime.&lt;/li&gt;
&lt;li&gt;[ ] Block or degrade high-risk actions when source health is low.&lt;/li&gt;
&lt;li&gt;[ ] Show honest user-facing degraded states.&lt;/li&gt;
&lt;li&gt;[ ] Attach source health to answer receipts and incident reviews.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;h3&gt;
  
  
  What is an AI source connector health check?
&lt;/h3&gt;

&lt;p&gt;It is a set of tests that verifies whether a data source is reachable, fresh, complete, schema-compatible, permission-safe, and usable by an AI workflow. It goes beyond checking whether the API is online.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is this different from RAG evaluation?
&lt;/h3&gt;

&lt;p&gt;Yes. RAG evaluation checks whether retrieval and answers are good. Source connector health checks whether the underlying data source should be trusted before retrieval or agent action uses it. They work best together.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do small AI products need connector health checks?
&lt;/h3&gt;

&lt;p&gt;Yes, but they can start small. Track freshness, schema compatibility, permission probes, and a few sample-answer tests. That is enough to catch many silent failures before users do.&lt;/p&gt;

&lt;h3&gt;
  
  
  How often should connector health checks run?
&lt;/h3&gt;

&lt;p&gt;Run lightweight checks after every sync and before high-risk agent workflows. Run deeper sample-answer tests on a schedule, after schema changes, and after permission or ingestion code changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should agents see source health status?
&lt;/h3&gt;

&lt;p&gt;Yes, but do not rely on the model alone. Pass a compact health summary to the agent for better responses, and enforce hard blocks in runtime policy for risky actions.&lt;/p&gt;

&lt;h3&gt;
  
  
  What should happen when a connector is unhealthy?
&lt;/h3&gt;

&lt;p&gt;The workflow should degrade based on risk. Low-risk summaries can show warnings. Drafting can continue with review. Writes, sends, billing actions, and cross-user updates should pause until health is restored.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;AI quality is not only a model problem. It is an evidence problem.&lt;/p&gt;

&lt;p&gt;If your agent reads broken data, stale permissions, or incomplete syncs, a better prompt will only make the wrong answer sound cleaner. Build connector health checks early, wire them into runtime behavior, and make unhealthy evidence impossible to ignore.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>saas</category>
      <category>agents</category>
      <category>architecture</category>
    </item>
    <item>
      <title>AI Code Review Packet: Make Agent-Written Pull Requests Easy to Trust</title>
      <dc:creator>Jack M</dc:creator>
      <pubDate>Wed, 02 Sep 2026 06:02:35 +0000</pubDate>
      <link>https://dev.to/jackm-singularity/ai-code-review-packet-make-agent-written-pull-requests-easy-to-trust-2c0g</link>
      <guid>https://dev.to/jackm-singularity/ai-code-review-packet-make-agent-written-pull-requests-easy-to-trust-2c0g</guid>
      <description>&lt;p&gt;AI can write a clean 900-line pull request before lunch. The hard part is not generating the code anymore; it is helping a tired reviewer understand what changed, what might break, and what evidence proves the work is safe.&lt;/p&gt;

&lt;p&gt;That is where an &lt;strong&gt;AI code review packet&lt;/strong&gt; helps. Instead of asking reviewers to reverse-engineer an agent's thinking from a diff, you attach a small, structured bundle of proof to every AI-assisted pull request.&lt;/p&gt;

&lt;p&gt;This guide shows how to design that packet for production teams building AI features, agent workflows, and developer tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why agent-written pull requests feel harder to review
&lt;/h2&gt;

&lt;p&gt;Traditional pull requests carry hidden context. A human developer usually spends hours exploring the problem before opening the PR. By the time reviewers see the code, the author can explain tradeoffs, weird edge cases, and what failed during testing.&lt;/p&gt;

&lt;p&gt;AI coding agents change that rhythm.&lt;/p&gt;

&lt;p&gt;They can generate code fast, but the review burden moves downstream:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The diff is larger than the human expected.&lt;/li&gt;
&lt;li&gt;The code looks polished even when the design is wrong.&lt;/li&gt;
&lt;li&gt;Tests may cover the happy path but miss tenant, billing, permission, or latency risks.&lt;/li&gt;
&lt;li&gt;The reviewer does not know which files the agent inspected before editing.&lt;/li&gt;
&lt;li&gt;The PR description sounds confident but does not prove anything.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Recent developer discussion keeps circling the same point: AI made writing code cheap, but it made reading code expensive. For AI SaaS builders and small product teams, that is dangerous. You do not have extra reviewers sitting around. If review cost grows faster than delivery speed, the agent becomes a bottleneck disguised as acceleration.&lt;/p&gt;

&lt;p&gt;An AI code review packet fixes the handoff.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is an AI code review packet?
&lt;/h2&gt;

&lt;p&gt;An &lt;strong&gt;AI code review packet&lt;/strong&gt; is a structured review artifact attached to a pull request. It summarizes the intent, changed surface area, risk level, tests, commands run, screenshots or traces, rollback plan, and open questions.&lt;/p&gt;

&lt;p&gt;Think of it as a receipt for the work.&lt;/p&gt;

&lt;p&gt;It does not replace code review. It makes code review cheaper and more focused.&lt;/p&gt;

&lt;p&gt;A good packet answers seven questions quickly:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What user or system problem changed?&lt;/li&gt;
&lt;li&gt;What files and behaviors were touched?&lt;/li&gt;
&lt;li&gt;What risks are introduced?&lt;/li&gt;
&lt;li&gt;What evidence proves the change works?&lt;/li&gt;
&lt;li&gt;What was not tested?&lt;/li&gt;
&lt;li&gt;How can we roll it back?&lt;/li&gt;
&lt;li&gt;What should a human inspect first?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For AI-assisted engineering, this is more useful than a long natural-language PR summary. Reviewers need routing information, not a novel.&lt;/p&gt;

&lt;h2&gt;
  
  
  The search gap: review proof, not generic AI code review
&lt;/h2&gt;

&lt;p&gt;There is plenty of content about AI code review tools, AI pair programming, and prompt tips. The underserved search gap is more practical: teams want to know how to review agent-written pull requests without trusting a black box.&lt;/p&gt;

&lt;p&gt;Useful long-tail keywords include &lt;strong&gt;AI code review packet&lt;/strong&gt;, &lt;strong&gt;agent-written pull request checklist&lt;/strong&gt;, &lt;strong&gt;AI-generated code review workflow&lt;/strong&gt;, &lt;strong&gt;pull request evidence template&lt;/strong&gt;, &lt;strong&gt;AI PR risk assessment&lt;/strong&gt;, and &lt;strong&gt;agentic coding quality gates&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The unique angle here is not "use AI to review code." The stronger angle is: &lt;strong&gt;make AI-written code easier for humans to verify.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The packet structure
&lt;/h2&gt;

&lt;p&gt;Here is a practical structure you can paste into a PR template or generate from your coding agent.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## AI Code Review Packet&lt;/span&gt;

&lt;span class="gu"&gt;### 1. Intent&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; User problem:
&lt;span class="p"&gt;-&lt;/span&gt; Expected behavior:
&lt;span class="p"&gt;-&lt;/span&gt; Non-goals:

&lt;span class="gu"&gt;### 2. Changed Surface Area&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Files changed:
&lt;span class="p"&gt;-&lt;/span&gt; APIs/routes changed:
&lt;span class="p"&gt;-&lt;/span&gt; Database/schema changes:
&lt;span class="p"&gt;-&lt;/span&gt; Background jobs or queues changed:
&lt;span class="p"&gt;-&lt;/span&gt; Permissions/billing/tenant logic touched:

&lt;span class="gu"&gt;### 3. Risk Rating&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Risk level: Low / Medium / High
&lt;span class="p"&gt;-&lt;/span&gt; Why:
&lt;span class="p"&gt;-&lt;/span&gt; Human review focus:

&lt;span class="gu"&gt;### 4. Evidence&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Tests added/updated:
&lt;span class="p"&gt;-&lt;/span&gt; Commands run:
&lt;span class="p"&gt;-&lt;/span&gt; Manual checks:
&lt;span class="p"&gt;-&lt;/span&gt; Screenshots/traces/logs:

&lt;span class="gu"&gt;### 5. Edge Cases&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Empty input:
&lt;span class="p"&gt;-&lt;/span&gt; Large input:
&lt;span class="p"&gt;-&lt;/span&gt; Permission denied:
&lt;span class="p"&gt;-&lt;/span&gt; Provider timeout:
&lt;span class="p"&gt;-&lt;/span&gt; Cross-tenant data:
&lt;span class="p"&gt;-&lt;/span&gt; Retry/idempotency:

&lt;span class="gu"&gt;### 6. Rollback Plan&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Safe rollback steps:
&lt;span class="p"&gt;-&lt;/span&gt; Feature flag:
&lt;span class="p"&gt;-&lt;/span&gt; Migration rollback:
&lt;span class="p"&gt;-&lt;/span&gt; Data repair needed:

&lt;span class="gu"&gt;### 7. Open Questions&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Known uncertainty:
&lt;span class="p"&gt;-&lt;/span&gt; Reviewer decision needed:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The value is not the template itself. The value is consistency. Every agent-written PR should carry the same shape of proof so reviewers know where to look.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add a risk score before review starts
&lt;/h2&gt;

&lt;p&gt;Not every AI-generated pull request deserves the same attention. A typo fix and a billing permissions change should not enter the same review lane.&lt;/p&gt;

&lt;p&gt;Use a simple risk score.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;Low risk&lt;/th&gt;
&lt;th&gt;Medium risk&lt;/th&gt;
&lt;th&gt;High risk&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;User impact&lt;/td&gt;
&lt;td&gt;Internal only&lt;/td&gt;
&lt;td&gt;User-visible UI&lt;/td&gt;
&lt;td&gt;Billing, auth, data access&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data touched&lt;/td&gt;
&lt;td&gt;No stored data&lt;/td&gt;
&lt;td&gt;Existing user data read&lt;/td&gt;
&lt;td&gt;Writes, deletes, exports&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime behavior&lt;/td&gt;
&lt;td&gt;Static change&lt;/td&gt;
&lt;td&gt;Request path change&lt;/td&gt;
&lt;td&gt;Background jobs, retries, agents&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reversibility&lt;/td&gt;
&lt;td&gt;Easy revert&lt;/td&gt;
&lt;td&gt;Feature flag&lt;/td&gt;
&lt;td&gt;Migration or data repair needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test evidence&lt;/td&gt;
&lt;td&gt;Strong&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;td&gt;Missing or unclear&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A basic scoring function can be enough:&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;ReviewRisk&lt;/span&gt; &lt;span class="o"&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="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ChangeSignal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;touchesAuth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;touchesBilling&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;touchesTenantData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;hasMigration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;changesBackgroundJob&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;lacksTests&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;behindFeatureFlag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&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;scoreReviewRisk&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;ChangeSignal&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;ReviewRisk&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;score&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="k"&gt;if &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;touchesAuth&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;3&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;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;touchesBilling&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;3&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;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;touchesTenantData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;3&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;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hasMigration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;score&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="k"&gt;if &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;changesBackgroundJob&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;score&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="k"&gt;if &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;lacksTests&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;score&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="k"&gt;if &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;behindFeatureFlag&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;score&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&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;medium&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;low&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;For solo SaaS developers, this can be a script that comments on a PR. For larger teams, it can route PRs into different review policies.&lt;/p&gt;

&lt;p&gt;High-risk PRs should require stronger evidence:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A human design note&lt;/li&gt;
&lt;li&gt;Passing tests plus targeted regression tests&lt;/li&gt;
&lt;li&gt;Tenant isolation checks&lt;/li&gt;
&lt;li&gt;Rollback steps&lt;/li&gt;
&lt;li&gt;Logs or traces for changed workflows&lt;/li&gt;
&lt;li&gt;Manual approval from the owner of the touched domain&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is not ceremony. The goal is to stop risky changes from looking routine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Capture what the agent actually inspected
&lt;/h2&gt;

&lt;p&gt;One hidden failure mode in agentic coding is shallow context. The agent edits the right file but never reads the nearby policy, schema, migration, test helper, or previous incident note.&lt;/p&gt;

&lt;p&gt;Your packet should include a &lt;strong&gt;context inspected&lt;/strong&gt; section.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;### Context Inspected&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Read before editing:
&lt;span class="p"&gt;  -&lt;/span&gt; app/api/billing/usage.ts
&lt;span class="p"&gt;  -&lt;/span&gt; app/services/tenant-policy.ts
&lt;span class="p"&gt;  -&lt;/span&gt; tests/billing/usage.test.ts
&lt;span class="p"&gt;  -&lt;/span&gt; docs/incidents/usage-metering-timeout.md
&lt;span class="p"&gt;-&lt;/span&gt; Not inspected:
&lt;span class="p"&gt;  -&lt;/span&gt; legacy billing worker
&lt;span class="p"&gt;  -&lt;/span&gt; enterprise plan overrides
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is simple, but it changes the review conversation. A reviewer can quickly spot missing context.&lt;/p&gt;

&lt;p&gt;For example, if the PR changes a RAG ingestion job but the agent did not inspect tenant permission rules, that is a review blocker. If it changes a model routing function but did not inspect cost limits, that is a review blocker too.&lt;/p&gt;

&lt;p&gt;You can generate this from file-read logs if your agent framework records them. If not, ask the agent to maintain a short list while working.&lt;/p&gt;

&lt;h2&gt;
  
  
  Require evidence, not confidence
&lt;/h2&gt;

&lt;p&gt;AI-generated PR descriptions often sound complete. That is not the same as being complete.&lt;/p&gt;

&lt;p&gt;Replace confident summaries with concrete evidence.&lt;/p&gt;

&lt;p&gt;Weak:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Updated the billing logic and added tests. This should handle edge cases.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Stronger:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Added usage aggregation for streamed model calls. Tested empty usage, retry dedupe, tenant isolation, and provider timeout paths. Did not test enterprise override plans because the fixture does not exist yet.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Your review packet should separate &lt;strong&gt;claims&lt;/strong&gt; from &lt;strong&gt;proof&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;### Evidence Table&lt;/span&gt;

| Claim | Evidence | Reviewer note |
| --- | --- | --- |
| Retry dedupe works | &lt;span class="sb"&gt;`usage-retry.test.ts`&lt;/span&gt; covers duplicate event IDs | Check idempotency key source |
| Tenant data stays isolated | Added test with two tenant IDs | Verify query includes tenant scope |
| Timeout returns safe error | Manual trace attached | Confirm frontend copy is acceptable |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern is especially useful for AI SaaS workflows because many bugs hide in edges: retries, partial streams, background jobs, stale context, provider failures, and cross-tenant reads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add reviewer-first navigation
&lt;/h2&gt;

&lt;p&gt;A reviewer should not have to read every changed line in order. The packet should tell them where risk lives.&lt;/p&gt;

&lt;p&gt;Add a &lt;strong&gt;review focus&lt;/strong&gt; section:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;### Human Review Focus&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; &lt;span class="sb"&gt;`tenant-policy.ts`&lt;/span&gt; — confirms every usage query is scoped by tenant ID.
&lt;span class="p"&gt;2.&lt;/span&gt; &lt;span class="sb"&gt;`usage-worker.ts`&lt;/span&gt; — retry dedupe logic changed.
&lt;span class="p"&gt;3.&lt;/span&gt; &lt;span class="sb"&gt;`usage-retry.test.ts`&lt;/span&gt; — new tests may miss concurrent retry behavior.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This saves time and improves quality. It tells the reviewer, "Start here. These lines matter most."&lt;/p&gt;

&lt;p&gt;For large AI-written PRs, this is the difference between useful review and approval theater.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use packets to prevent oversized AI pull requests
&lt;/h2&gt;

&lt;p&gt;AI agents are good at continuing. That is also the problem.&lt;/p&gt;

&lt;p&gt;A small request can become a sweeping refactor unless the workflow sets boundaries. The review packet should expose scope creep.&lt;/p&gt;

&lt;p&gt;Add a changed-surface budget:&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;"max_files_changed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"max_lines_changed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"allowed_directories"&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;"app/billing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tests/billing"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"blocked_directories"&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;"app/auth"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"db/migrations"&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;If the agent crosses the budget, it must explain why or split the work.&lt;/p&gt;

&lt;p&gt;This pairs well with coding agents in tools like Claude Code, Cursor, Codex-style CLIs, or internal agent runners. The agent can draft the packet, but CI should verify the facts where possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automate the boring checks
&lt;/h2&gt;

&lt;p&gt;A review packet gets stronger when machines fill in the objective parts.&lt;/p&gt;

&lt;p&gt;Your CI can add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Files changed&lt;/li&gt;
&lt;li&gt;Test commands run&lt;/li&gt;
&lt;li&gt;Coverage delta&lt;/li&gt;
&lt;li&gt;Migration detection&lt;/li&gt;
&lt;li&gt;API route changes&lt;/li&gt;
&lt;li&gt;Dependency changes&lt;/li&gt;
&lt;li&gt;Secret scanning status&lt;/li&gt;
&lt;li&gt;Bundle size delta&lt;/li&gt;
&lt;li&gt;Lint/typecheck results&lt;/li&gt;
&lt;li&gt;Risk score hints&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is a tiny Node.js example that creates a changed-file summary:&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;execSync&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:child_process&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;diff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;execSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;git diff --name-only origin/main...HEAD&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;encoding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&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;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;diff&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&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="nb"&gt;Boolean&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;risky&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;files&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;file&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;file&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;auth&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="nx"&gt;file&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;billing&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="nx"&gt;file&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;tenant&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="nx"&gt;file&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;migration&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="nx"&gt;file&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;worker&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;## Changed Surface Area&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;files&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;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;file&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="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="se"&gt;\n&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;risky&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="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;## Risk Hints&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;risky&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;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`- Review carefully: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;file&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="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="se"&gt;\n&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;Do not ask the model to invent objective facts. Let scripts collect facts. Ask the model to explain them.&lt;/p&gt;

&lt;p&gt;That division matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example packet for an AI feature PR
&lt;/h2&gt;

&lt;p&gt;Imagine an agent adds fallback model routing when a provider times out.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## AI Code Review Packet&lt;/span&gt;

&lt;span class="gu"&gt;### Intent&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; User problem: AI answers fail when the primary model provider times out.
&lt;span class="p"&gt;-&lt;/span&gt; Expected behavior: retry once, then route to a cheaper fallback model for safe task types.
&lt;span class="p"&gt;-&lt;/span&gt; Non-goals: no change to premium reasoning tasks or billing plans.

&lt;span class="gu"&gt;### Changed Surface Area&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`app/ai/model-router.ts`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`app/ai/provider-client.ts`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`app/ai/task-policy.ts`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`tests/ai/model-router.test.ts`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; No database migration.
&lt;span class="p"&gt;-&lt;/span&gt; No auth changes.

&lt;span class="gu"&gt;### Risk Rating&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Medium.
&lt;span class="p"&gt;-&lt;/span&gt; Runtime model behavior changes, but only behind &lt;span class="sb"&gt;`ai_fallback_v2`&lt;/span&gt; flag.

&lt;span class="gu"&gt;### Evidence&lt;/span&gt;
| Claim | Evidence | Reviewer note |
| --- | --- | --- |
| Timeout falls back safely | &lt;span class="sb"&gt;`model-router.test.ts`&lt;/span&gt; timeout case | Check task allowlist |
| Premium tasks do not fallback | policy test added | Verify plan mapping |
| Cost ledger still records final model | unit test added | Confirm analytics event name |

&lt;span class="gu"&gt;### Human Review Focus&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; &lt;span class="sb"&gt;`task-policy.ts`&lt;/span&gt; — fallback allowlist.
&lt;span class="p"&gt;2.&lt;/span&gt; &lt;span class="sb"&gt;`model-router.ts`&lt;/span&gt; — retry and fallback ordering.
&lt;span class="p"&gt;3.&lt;/span&gt; &lt;span class="sb"&gt;`provider-client.ts`&lt;/span&gt; — timeout handling.

&lt;span class="gu"&gt;### Rollback Plan&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Disable &lt;span class="sb"&gt;`ai_fallback_v2`&lt;/span&gt; feature flag.
&lt;span class="p"&gt;-&lt;/span&gt; Revert PR if errors continue.
&lt;span class="p"&gt;-&lt;/span&gt; No data repair required.

&lt;span class="gu"&gt;### Open Questions&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Should fallback answers include a lower-confidence UI label?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice how the packet makes review faster without hiding uncertainty. The open question is visible. The risk is named. The rollback is clear.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where packets fit in the development workflow
&lt;/h2&gt;

&lt;p&gt;A practical AI-assisted workflow looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Human writes a short task contract.&lt;/li&gt;
&lt;li&gt;Agent inspects context before editing.&lt;/li&gt;
&lt;li&gt;Agent edits within a scope budget.&lt;/li&gt;
&lt;li&gt;CI collects objective facts.&lt;/li&gt;
&lt;li&gt;Agent drafts the review packet.&lt;/li&gt;
&lt;li&gt;CI checks that required packet sections exist.&lt;/li&gt;
&lt;li&gt;Human reviews the packet first, then the risky files.&lt;/li&gt;
&lt;li&gt;High-risk PRs require stronger approval.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This flow works for solo SaaS developers too. If you are the only reviewer, the packet protects future you. It leaves a trail of why the change looked safe at the time.&lt;/p&gt;

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

&lt;p&gt;Start small.&lt;/p&gt;

&lt;p&gt;For the next five AI-assisted PRs, require only these fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Intent&lt;/li&gt;
&lt;li&gt;Changed surface area&lt;/li&gt;
&lt;li&gt;Risk rating&lt;/li&gt;
&lt;li&gt;Evidence&lt;/li&gt;
&lt;li&gt;Human review focus&lt;/li&gt;
&lt;li&gt;Rollback plan&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After five PRs, review what helped and what people ignored. Then automate the objective parts.&lt;/p&gt;

&lt;p&gt;You do not need a new platform to begin. A PR template, a CI script, and a firm rule are enough:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;No agent-written pull request merges without a review packet.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That one rule can save hours of review time and catch the kind of subtle bugs that clean-looking AI code tends to hide.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final checklist
&lt;/h2&gt;

&lt;p&gt;Before merging an AI-assisted PR, ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does the packet explain the user-visible change?&lt;/li&gt;
&lt;li&gt;Does it list risky files first?&lt;/li&gt;
&lt;li&gt;Does evidence match the actual diff?&lt;/li&gt;
&lt;li&gt;Are missing tests named honestly?&lt;/li&gt;
&lt;li&gt;Is rollback safe and fast?&lt;/li&gt;
&lt;li&gt;Would a new teammate understand why this was merged?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the answer is no, the PR is not ready. AI coding agents are most useful when they increase delivery speed without making trust expensive. Review packets help keep that balance.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  What is an AI code review packet?
&lt;/h3&gt;

&lt;p&gt;An AI code review packet is a structured summary attached to an AI-assisted pull request. It lists intent, changed files, risk, test evidence, review focus, rollback steps, and open questions so humans can review faster and more safely.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is this different from a normal pull request template?
&lt;/h3&gt;

&lt;p&gt;Yes. A normal PR template often asks for a description and screenshots. An AI code review packet focuses on proof: what the agent inspected, what changed, what is risky, what was tested, and where reviewers should look first.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should the AI agent write the packet?
&lt;/h3&gt;

&lt;p&gt;The agent can draft it, but scripts and CI should fill objective facts such as changed files, commands run, migrations, dependency changes, and test status. The model should explain facts, not invent them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do solo developers need AI review packets?
&lt;/h3&gt;

&lt;p&gt;Yes. If you are a solo SaaS developer, the packet gives you a lightweight safety check before merge and a record you can inspect later when debugging incidents or customer reports.&lt;/p&gt;

&lt;h3&gt;
  
  
  What should make an AI-written pull request high risk?
&lt;/h3&gt;

&lt;p&gt;Treat a PR as high risk if it touches authentication, authorization, billing, tenant data, migrations, background jobs, model routing, secrets, deletion, exports, or user-visible automated actions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can review packets replace tests?
&lt;/h3&gt;

&lt;p&gt;No. They make test evidence easier to inspect, but they do not replace unit tests, integration tests, policy checks, manual verification, or human judgment.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>saas</category>
      <category>codequality</category>
      <category>agents</category>
    </item>
    <item>
      <title>AI Agent Communication Gateway: Let Users Reach Agents Without Fragile Webhooks</title>
      <dc:creator>Jack M</dc:creator>
      <pubDate>Tue, 01 Sep 2026 03:34:03 +0000</pubDate>
      <link>https://dev.to/jackm-singularity/ai-agent-communication-gateway-let-users-reach-agents-without-fragile-webhooks-f6m</link>
      <guid>https://dev.to/jackm-singularity/ai-agent-communication-gateway-let-users-reach-agents-without-fragile-webhooks-f6m</guid>
      <description>&lt;p&gt;An AI agent that nobody can reliably reach is not a product feature. It is a background worker with a chat box attached.&lt;/p&gt;

&lt;p&gt;That sounds harsh, but it is the problem many builders hit after the demo works. The model can reason. The tools are wired. The workflow can update records, draft replies, search documents, and call internal APIs. Then real users arrive through email, SMS, Slack, WhatsApp, in-app chat, voice calls, support forms, and webhooks from other systems. Suddenly the hard part is not “Can the model answer?” It is “Can the right user reach the right agent, through the right channel, with the right permissions, without losing state or trust?”&lt;/p&gt;

&lt;p&gt;This guide shows how to build an AI agent communication gateway: a small control layer between messy human channels and your agent runtime. It is not a vendor pitch. It is an implementation pattern for solo developers, Micro SaaS builders, and technical founders who need agents that can be contacted, resumed, governed, and audited in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an AI agent communication gateway does
&lt;/h2&gt;

&lt;p&gt;An AI agent communication gateway receives events from communication channels, normalizes them, checks policy, attaches identity and context, and routes work to the right agent workflow.&lt;/p&gt;

&lt;p&gt;Think of it as the front door for agent conversations.&lt;/p&gt;

&lt;p&gt;It should answer seven questions before your model sees anything:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Who is contacting the agent?&lt;/li&gt;
&lt;li&gt;Which tenant, workspace, or account does this belong to?&lt;/li&gt;
&lt;li&gt;What channel did the message come from?&lt;/li&gt;
&lt;li&gt;Is this message allowed under consent, rate limit, and security policy?&lt;/li&gt;
&lt;li&gt;Which conversation or workflow should resume?&lt;/li&gt;
&lt;li&gt;What tools can this agent use for this user?&lt;/li&gt;
&lt;li&gt;What evidence should be stored for debugging and audit?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Without this layer, each channel becomes a custom integration. Email has one identity model. SMS has another. Slack has another. Webhooks arrive with missing context. Voice calls produce partial transcripts. Support forms create tickets. Soon your agent runtime is full of channel-specific hacks.&lt;/p&gt;

&lt;p&gt;A gateway keeps the agent runtime boring.&lt;/p&gt;

&lt;h2&gt;
  
  
  The production problem: channels are not just text boxes
&lt;/h2&gt;

&lt;p&gt;A demo often starts like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User -&amp;gt; chat UI -&amp;gt; agent -&amp;gt; tool call -&amp;gt; response
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Production looks more like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SMS reply            -&amp;gt; webhook -&amp;gt; ?
Email thread         -&amp;gt; parser  -&amp;gt; ?
Slack mention        -&amp;gt; event   -&amp;gt; ?
Voice transcript     -&amp;gt; stream  -&amp;gt; ?
Support form         -&amp;gt; ticket  -&amp;gt; ?
Partner webhook      -&amp;gt; event   -&amp;gt; ?
In-app chat message  -&amp;gt; socket  -&amp;gt; ?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every channel brings different failure modes.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Channel&lt;/th&gt;
&lt;th&gt;Hidden production risk&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Email&lt;/td&gt;
&lt;td&gt;thread splitting, spoofing, quoted text, attachments, delayed delivery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SMS&lt;/td&gt;
&lt;td&gt;opt-outs, carrier delays, short replies, number recycling, compliance rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slack/Discord&lt;/td&gt;
&lt;td&gt;team identity, mentions, bot permissions, public/private context leakage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WhatsApp&lt;/td&gt;
&lt;td&gt;templates, consent, media, delivery states, business identity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Voice&lt;/td&gt;
&lt;td&gt;partial transcripts, interruption, latency, caller authentication&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Webhooks&lt;/td&gt;
&lt;td&gt;replay attacks, missing signatures, schema drift, duplicate events&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;In-app chat&lt;/td&gt;
&lt;td&gt;tenant context, session state, browser identity, auth expiry&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The model should not be responsible for fixing these. The gateway should.&lt;/p&gt;

&lt;h2&gt;
  
  
  A simple architecture
&lt;/h2&gt;

&lt;p&gt;Start with five parts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Channel Adapter
  -&amp;gt; Event Normalizer
  -&amp;gt; Identity Mapper
  -&amp;gt; Policy Gate
  -&amp;gt; Agent Router
  -&amp;gt; Event Queue
  -&amp;gt; Agent Runtime
  -&amp;gt; Response Dispatcher
  -&amp;gt; Audit Log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  1. Channel adapters
&lt;/h3&gt;

&lt;p&gt;A channel adapter converts provider-specific events into one internal shape.&lt;/p&gt;

&lt;p&gt;Example normalized event:&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;"event_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;"evt_01J..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"channel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sms"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"direction"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"inbound"&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="s2"&gt;"tenant_123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"external_user_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;"+15551234567"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"conversation_key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sms:+15551234567:tenant_123"&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="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;"text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Can you move my demo to Thursday?"&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;"provider"&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;"sms_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;"message_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;"abc123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"signature_verified"&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="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;"received_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-09-01T03:30:00Z"&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 agent should not care whether the text came from SMS, email, or chat. It should receive a trusted event with clear channel metadata.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Event normalizer
&lt;/h3&gt;

&lt;p&gt;Normalization prevents each workflow from re-solving the same problems.&lt;/p&gt;

&lt;p&gt;Normalize:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;message text&lt;/li&gt;
&lt;li&gt;attachments&lt;/li&gt;
&lt;li&gt;sender identifiers&lt;/li&gt;
&lt;li&gt;timestamps&lt;/li&gt;
&lt;li&gt;delivery status&lt;/li&gt;
&lt;li&gt;reply/thread IDs&lt;/li&gt;
&lt;li&gt;opt-out events&lt;/li&gt;
&lt;li&gt;retry counters&lt;/li&gt;
&lt;li&gt;provider error codes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not over-clean the message. Keep the raw event in cold storage or an audit table. Store the normalized event separately so agent code has a stable contract.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Identity mapper
&lt;/h3&gt;

&lt;p&gt;This is where many agent products become risky.&lt;/p&gt;

&lt;p&gt;A phone number, email address, Slack user ID, browser session, and API token may all represent the same person. Or they may not. The gateway should map external channel identity to an internal actor.&lt;/p&gt;

&lt;p&gt;Use a table 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_channel_identity&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;channel&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;external_subject&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;internal_user_id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;trust_level&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="c1"&gt;-- unknown, verified, owner, admin&lt;/span&gt;
  &lt;span class="n"&gt;consent_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="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;-- allowed, limited, revoked&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="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;last_seen_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="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;channel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;external_subject&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 lets you avoid dangerous shortcuts like, “If the email says it is from the founder, let the agent act as admin.”&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Policy gate
&lt;/h3&gt;

&lt;p&gt;The policy gate decides what the event is allowed to do before it reaches the agent.&lt;/p&gt;

&lt;p&gt;Minimum checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;webhook signature verification&lt;/li&gt;
&lt;li&gt;replay protection using event IDs and timestamps&lt;/li&gt;
&lt;li&gt;per-channel rate limits&lt;/li&gt;
&lt;li&gt;tenant-level spend limits&lt;/li&gt;
&lt;li&gt;consent and opt-out state&lt;/li&gt;
&lt;li&gt;allowed attachment types&lt;/li&gt;
&lt;li&gt;sensitive action restrictions&lt;/li&gt;
&lt;li&gt;unknown sender handling&lt;/li&gt;
&lt;li&gt;abuse and spam scoring&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A useful rule: unknown users can ask questions, but they cannot trigger writes.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Agent router
&lt;/h3&gt;

&lt;p&gt;The router chooses the 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="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;RouteDecision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;support&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;sales_ops&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;billing&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;internal_admin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;answer_only&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;draft&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;supervised_action&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;conversationId&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;toolScope&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;requiresApproval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&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;Keep routing boring and explicit. The model can help classify intent, but the final route should be constrained by deterministic rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design the gateway around events, not chats
&lt;/h2&gt;

&lt;p&gt;The most important shift is this: communication channels are event streams.&lt;/p&gt;

&lt;p&gt;A message is one event. A delivery receipt is another. A failed send is another. A user reply is another. An opt-out is another. A human takeover is another.&lt;/p&gt;

&lt;p&gt;Your gateway should store all of them.&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_comm_event&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;conversation_id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;channel&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;event_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;actor_id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;normalized_payload&lt;/span&gt; &lt;span class="n"&gt;jsonb&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;raw_payload_ref&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;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;policy_result&lt;/span&gt; &lt;span class="n"&gt;jsonb&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;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="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;This gives you idempotency, replay, debugging, and analytics. It also lets long-running agents resume from the event log instead of relying on a fragile in-memory chat session.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to process inbound messages safely
&lt;/h2&gt;

&lt;p&gt;Here is a practical inbound flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Receive the provider webhook.&lt;/li&gt;
&lt;li&gt;Verify the signature.&lt;/li&gt;
&lt;li&gt;Reject old timestamps and duplicate event IDs.&lt;/li&gt;
&lt;li&gt;Store the raw payload.&lt;/li&gt;
&lt;li&gt;Normalize the event.&lt;/li&gt;
&lt;li&gt;Map the sender to a tenant and user.&lt;/li&gt;
&lt;li&gt;Check consent, rate limits, and channel permissions.&lt;/li&gt;
&lt;li&gt;Create or resume the conversation.&lt;/li&gt;
&lt;li&gt;Push a job into a durable queue.&lt;/li&gt;
&lt;li&gt;Run the agent with scoped context and tools.&lt;/li&gt;
&lt;li&gt;Dispatch the response through the approved channel.&lt;/li&gt;
&lt;li&gt;Store the trace, result, and delivery state.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Example TypeScript sketch:&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;handleInbound&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&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;rawBody&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;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&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;headers&lt;/span&gt; &lt;span class="o"&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;fromEntries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&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;verified&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;verifyWebhookSignature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;headers&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;verified&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;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;invalid signature&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;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;401&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;providerEvent&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;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawBody&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;idempotencyKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;buildIdempotencyKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;providerEvent&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;duplicate&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;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;duplicate&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;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ok&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;normalized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalizeChannelEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;providerEvent&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;identity&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;mapIdentity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;normalized&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;policy&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;evaluateCommunicationPolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;normalized&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;identity&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;events&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="na"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;identity&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;channel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;normalized&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;eventType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;normalized&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&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="na"&gt;normalizedPayload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;normalized&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;policyResult&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="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;policy&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="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;dispatchSafeNotice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;normalized&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;reason&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;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ok&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;await&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;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;agent.inbound&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;eventId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;normalized&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="na"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;identity&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;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;internalUserId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;conversationKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;normalized&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;conversationKey&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;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ok&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: the webhook handler does not call the model directly. That is intentional. Webhook handlers should be fast, idempotent, and boring.&lt;/p&gt;

&lt;h2&gt;
  
  
  Channel adapters need different trust levels
&lt;/h2&gt;

&lt;p&gt;Do not treat every channel equally.&lt;/p&gt;

&lt;p&gt;An authenticated in-app message from a logged-in user has a different trust level than an inbound SMS from a phone number. A signed partner webhook has a different trust level than a public support form.&lt;/p&gt;

&lt;p&gt;A simple trust model helps:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Trust level&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;th&gt;Allowed behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;unknown&lt;/td&gt;
&lt;td&gt;new phone number, public form&lt;/td&gt;
&lt;td&gt;answer general questions, create intake record&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;known&lt;/td&gt;
&lt;td&gt;matched email or phone&lt;/td&gt;
&lt;td&gt;retrieve limited account context&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;verified&lt;/td&gt;
&lt;td&gt;logged-in session, signed link&lt;/td&gt;
&lt;td&gt;draft changes, access scoped records&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;privileged&lt;/td&gt;
&lt;td&gt;admin session with fresh auth&lt;/td&gt;
&lt;td&gt;request sensitive actions with approval&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This trust level should affect tool access, context retrieval, response content, and approval requirements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build a response dispatcher, not direct sends
&lt;/h2&gt;

&lt;p&gt;Agents should not directly send SMS, email, or chat messages. They should produce a response 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;"conversation_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;"conv_123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"channel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&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="nl"&gt;"response_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;"draft_or_send"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"I can help move the demo. Thursday has two open slots: 10:00 or 14: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;"requires_approval"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&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_labels"&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;"scheduling"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"low_risk"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"references"&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;"calendar_slot_check_456"&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 dispatcher applies channel rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SMS length and opt-out footer rules&lt;/li&gt;
&lt;li&gt;email subject and thread headers&lt;/li&gt;
&lt;li&gt;Slack mention formatting&lt;/li&gt;
&lt;li&gt;WhatsApp template constraints&lt;/li&gt;
&lt;li&gt;voice response length&lt;/li&gt;
&lt;li&gt;human approval before sensitive sends&lt;/li&gt;
&lt;li&gt;quiet hours&lt;/li&gt;
&lt;li&gt;delivery retry policy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This keeps model output separate from channel operations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where developers usually get burned
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Mistake 1: Calling the model inside the webhook
&lt;/h3&gt;

&lt;p&gt;This creates timeout failures, duplicate replies, and messy retries. Put the work on a queue.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 2: Using channel identity as app identity
&lt;/h3&gt;

&lt;p&gt;A phone number is not a permission model. Map it, verify it, and scope it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 3: Forgetting delivery and failure events
&lt;/h3&gt;

&lt;p&gt;If the agent sends a message but never records delivery status, it will act on assumptions. Store sent, delivered, failed, bounced, replied, and opted-out events.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 4: No human handoff state
&lt;/h3&gt;

&lt;p&gt;A handoff is not just “notify support.” It should pause the agent, attach context, show suggested next steps, and record who took over.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 5: Letting one conversation cross tenants
&lt;/h3&gt;

&lt;p&gt;Thread IDs, phone numbers, and emails can collide in surprising ways. Always include tenant ID in conversation keys and unique constraints.&lt;/p&gt;

&lt;h2&gt;
  
  
  A lightweight implementation plan
&lt;/h2&gt;

&lt;p&gt;If you are building alone, do not start with every channel. Start with one high-value channel and design the contract as if more are coming.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 1: One channel, strong contract
&lt;/h3&gt;

&lt;p&gt;Pick the channel your users already use. Implement:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;signature verification&lt;/li&gt;
&lt;li&gt;normalized event shape&lt;/li&gt;
&lt;li&gt;identity mapping&lt;/li&gt;
&lt;li&gt;durable event storage&lt;/li&gt;
&lt;li&gt;queue-based agent execution&lt;/li&gt;
&lt;li&gt;response dispatcher&lt;/li&gt;
&lt;li&gt;audit logs&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Phase 2: Conversation state
&lt;/h3&gt;

&lt;p&gt;Add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;conversation IDs&lt;/li&gt;
&lt;li&gt;thread mapping&lt;/li&gt;
&lt;li&gt;last-agent-run pointer&lt;/li&gt;
&lt;li&gt;human takeover state&lt;/li&gt;
&lt;li&gt;escalation reason&lt;/li&gt;
&lt;li&gt;summarized conversation memory&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Phase 3: Policy and permissions
&lt;/h3&gt;

&lt;p&gt;Add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;trust levels&lt;/li&gt;
&lt;li&gt;tool scopes&lt;/li&gt;
&lt;li&gt;rate limits&lt;/li&gt;
&lt;li&gt;spend limits&lt;/li&gt;
&lt;li&gt;consent states&lt;/li&gt;
&lt;li&gt;approval requirements&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Phase 4: More channels
&lt;/h3&gt;

&lt;p&gt;Only add a second channel after the first one has clean events. The second channel will test whether your gateway is real or just a renamed integration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Metrics worth tracking
&lt;/h2&gt;

&lt;p&gt;Track metrics that show whether users can actually reach the agent and get useful outcomes.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;inbound event acceptance rate&lt;/td&gt;
&lt;td&gt;catches signature, schema, and adapter failures&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;duplicate webhook rate&lt;/td&gt;
&lt;td&gt;shows replay/idempotency pressure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;time to first agent response&lt;/td&gt;
&lt;td&gt;measures practical responsiveness&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;channel delivery failure rate&lt;/td&gt;
&lt;td&gt;prevents silent broken conversations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;human handoff rate&lt;/td&gt;
&lt;td&gt;reveals unclear intents or risky workflows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;opt-out/revocation events&lt;/td&gt;
&lt;td&gt;protects trust and compliance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;unknown sender blocked actions&lt;/td&gt;
&lt;td&gt;proves policy is working&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cost per resolved conversation&lt;/td&gt;
&lt;td&gt;connects model spend to useful outcomes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Do not only measure model accuracy. A correct answer that never reaches the user is still a failed workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final checklist
&lt;/h2&gt;

&lt;p&gt;Before you ship an AI agent communication gateway, make sure you can say yes to these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can every inbound event be replayed?&lt;/li&gt;
&lt;li&gt;Can every event be tied to a tenant?&lt;/li&gt;
&lt;li&gt;Can every sender be mapped to a trust level?&lt;/li&gt;
&lt;li&gt;Can duplicate webhooks be ignored safely?&lt;/li&gt;
&lt;li&gt;Can failed deliveries change the workflow state?&lt;/li&gt;
&lt;li&gt;Can revoked consent stop future messages?&lt;/li&gt;
&lt;li&gt;Can a human take over without losing context?&lt;/li&gt;
&lt;li&gt;Can the agent run without raw provider payloads?&lt;/li&gt;
&lt;li&gt;Can risky actions require fresh verification?&lt;/li&gt;
&lt;li&gt;Can you explain why a message was sent?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If not, pause before adding more channels.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  What is an AI agent communication gateway?
&lt;/h3&gt;

&lt;p&gt;An AI agent communication gateway is a control layer that receives messages and events from channels like SMS, email, chat, voice, and webhooks, then normalizes them, maps identity, checks policy, routes work to an agent, and dispatches safe responses.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is this different from an LLM gateway?
&lt;/h3&gt;

&lt;p&gt;Yes. An LLM gateway controls model calls, routing, caching, and provider policy. A communication gateway controls user and system communication events before and after the agent runs. Many products need both.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do small teams need a communication gateway?
&lt;/h3&gt;

&lt;p&gt;Small teams do not need a large platform. They do need the core pattern: normalized events, identity mapping, policy checks, durable queues, and response dispatch. You can build this with a few tables and one worker.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should my webhook call the AI model directly?
&lt;/h3&gt;

&lt;p&gt;Usually no. Webhook handlers should verify, normalize, store, and enqueue. Model calls can be slow, expensive, and retry-prone. A queue gives you durability, idempotency, and safer retries.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I prevent cross-tenant message leaks?
&lt;/h3&gt;

&lt;p&gt;Include tenant ID in every identity mapping, conversation key, event row, queue payload, tool call, and audit log. Never route a message using only an email address, phone number, or external thread ID.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the safest first channel to support?
&lt;/h3&gt;

&lt;p&gt;The safest first channel is the one where you already have strong identity. For many products, that is authenticated in-app chat. SMS, email, and public forms can work well, but they need stricter identity and consent checks.&lt;/p&gt;

&lt;h3&gt;
  
  
  What should happen when the agent is unsure?
&lt;/h3&gt;

&lt;p&gt;The gateway should support a human handoff state. The agent can attach a summary, evidence, attempted actions, and a suggested reply, then pause until a person reviews or resumes the workflow.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>saas</category>
      <category>agents</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Incremental AI Agent Workflows: Run Agents Only on What Changed</title>
      <dc:creator>Jack M</dc:creator>
      <pubDate>Mon, 31 Aug 2026 13:54:04 +0000</pubDate>
      <link>https://dev.to/jackm-singularity/incremental-ai-agent-workflows-run-agents-only-on-what-changed-j7a</link>
      <guid>https://dev.to/jackm-singularity/incremental-ai-agent-workflows-run-agents-only-on-what-changed-j7a</guid>
      <description>&lt;p&gt;Your agent does not need to reread the whole repo, rescan every document, or reprocess every customer record each time it runs. That habit feels safe, but it quietly burns tokens, slows feedback loops, and increases the chance that the model gets distracted by stale context.&lt;/p&gt;

&lt;p&gt;The better pattern is simple: make AI workflows incremental. Give the agent a clean list of what changed, the previous baseline, the acceptance rules, and only the context needed to finish the next step.&lt;/p&gt;

&lt;p&gt;This guide shows how to design an incremental AI agent workflow for builders who need practical reliability without adding a giant orchestration platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Incremental Agents Matter Now
&lt;/h2&gt;

&lt;p&gt;Recent developer discussions and tool launches point in the same direction: agentic systems are getting more capable, but the bottleneck is shifting from “can the model act?” to “can the workflow stay scoped, cheap, and verifiable?”&lt;/p&gt;

&lt;p&gt;A few current signals stand out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Developers are experimenting with local-first agent IDEs, subagents, tool sandboxes, and token analytics.&lt;/li&gt;
&lt;li&gt;New change-tracking tools are emerging so AI skills can run only over files changed since the last pass.&lt;/li&gt;
&lt;li&gt;Tool-call rule engines are appearing because teams need to deny, rewrite, or annotate agent actions before they run.&lt;/li&gt;
&lt;li&gt;Maintainers are tired of reviewing low-quality AI-generated changes that add noise instead of useful fixes.&lt;/li&gt;
&lt;li&gt;AI infrastructure costs keep pushing teams to measure tokens, latency, retries, and failed work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The practical implication is clear: broad agent access is not the same as productive agent work. If your workflow gives the model everything every time, you are paying for confusion.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Idea: Treat Changed Work as the Input
&lt;/h2&gt;

&lt;p&gt;An incremental workflow starts with a change set. A change set is the smallest useful unit of work since the last successful baseline.&lt;/p&gt;

&lt;p&gt;That could be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Modified files in a repository&lt;/li&gt;
&lt;li&gt;New support tickets since the last triage run&lt;/li&gt;
&lt;li&gt;Updated documents in a knowledge base&lt;/li&gt;
&lt;li&gt;Newly failed conversations in an AI support tool&lt;/li&gt;
&lt;li&gt;Recent rows in an analytics table&lt;/li&gt;
&lt;li&gt;Changed API schema definitions&lt;/li&gt;
&lt;li&gt;New user feedback since the last product review&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of asking, “What should the agent inspect?” you ask, “What changed since this agent last completed its job?”&lt;/p&gt;

&lt;p&gt;That one question improves cost, quality, and safety.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Simple Architecture for Incremental AI Workflows
&lt;/h2&gt;

&lt;p&gt;You do not need much infrastructure to start. The pattern has six pieces.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. A Baseline Store
&lt;/h3&gt;

&lt;p&gt;The baseline store remembers what each workflow has already processed. Each agent or skill should have its own baseline because different workflows care about different changes.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;security-review&lt;/code&gt; tracks security-sensitive files.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;docs-update&lt;/code&gt; tracks public documentation changes.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;test-writer&lt;/code&gt; tracks source files without matching tests.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;support-triage&lt;/code&gt; tracks new or updated tickets.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A baseline can be a Git commit, content hash, timestamp cursor, event ID, database cursor, or message offset. Content hashes are often safer than timestamps because rebases, file moves, and clock drift can make timestamps misleading.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. A Change Detector
&lt;/h3&gt;

&lt;p&gt;The change detector compares the current state with the saved baseline and returns candidate items.&lt;/p&gt;

&lt;p&gt;For a repo, it may call Git and hash file contents. For a data product, it may query &lt;code&gt;updated_at &amp;gt; last_cursor&lt;/code&gt;. For a queue, it may read unacknowledged events.&lt;/p&gt;

&lt;p&gt;Keep this component boring. It should not rely on the model. The agent should receive the result, not decide the result.&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;ChangeItem&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;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;file&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;ticket&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;doc&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;event&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;path&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;hash&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;updatedAt&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="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. A Scope Filter
&lt;/h3&gt;

&lt;p&gt;Not every change belongs in every agent run. A docs agent should not inspect secrets. A test agent does not need marketing copy. A support classifier does not need full billing history.&lt;/p&gt;

&lt;p&gt;Use deterministic filters before the model sees anything.&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;filterForDocsAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ChangeItem&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;item&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;file&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;docs/&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="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;README.md&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;For production AI products, this is also where you apply tenant boundaries, PII redaction, role-based access, and token budgets.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. A Context Builder
&lt;/h3&gt;

&lt;p&gt;The context builder packages each change with just enough surrounding information.&lt;/p&gt;

&lt;p&gt;For code, that may include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The changed file&lt;/li&gt;
&lt;li&gt;Nearby imports&lt;/li&gt;
&lt;li&gt;Related tests&lt;/li&gt;
&lt;li&gt;A short dependency summary&lt;/li&gt;
&lt;li&gt;The project rules that apply to this path&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For support tickets, it may include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The latest message&lt;/li&gt;
&lt;li&gt;The customer plan tier&lt;/li&gt;
&lt;li&gt;Product area labels&lt;/li&gt;
&lt;li&gt;Relevant help center snippets&lt;/li&gt;
&lt;li&gt;Recent known incidents&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Avoid dumping the whole system prompt, whole repo, or whole user history into every run. Bigger context is not always better context.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. A Verification Gate
&lt;/h3&gt;

&lt;p&gt;An incremental agent should not mark work as complete just because it produced an answer. It should pass a verification gate.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tests pass for changed code.&lt;/li&gt;
&lt;li&gt;Markdown builds without broken links.&lt;/li&gt;
&lt;li&gt;Generated SQL is read-only unless approved.&lt;/li&gt;
&lt;li&gt;A support reply cites the right source.&lt;/li&gt;
&lt;li&gt;A classification matches a known schema.&lt;/li&gt;
&lt;li&gt;The output includes a handoff note with evidence.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The gate can be code, policy, LLM-as-judge, human review, or a mix. Use code for things code can prove.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. A Mark-on-Success Rule
&lt;/h3&gt;

&lt;p&gt;Only update the baseline after the workflow succeeds. This is the part teams often miss.&lt;/p&gt;

&lt;p&gt;If an agent fails halfway, do not mark the change as processed. If tests fail, do not advance the cursor. If human review rejects the output, keep it pending or move it to a repair queue.&lt;/p&gt;

&lt;p&gt;This gives you safe retries without losing work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example: Incremental Code Review Agent
&lt;/h2&gt;

&lt;p&gt;Imagine you run an AI code review helper before pull requests. The naive version reads the whole diff, project docs, test suite, and coding rules every time. It is slow and inconsistent.&lt;/p&gt;

&lt;p&gt;The incremental version works like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Detect files changed since the last successful review baseline.&lt;/li&gt;
&lt;li&gt;Exclude generated files, lockfiles, snapshots, and vendor folders.&lt;/li&gt;
&lt;li&gt;Group changes by risk: auth, billing, data access, UI, tests, docs.&lt;/li&gt;
&lt;li&gt;Build a context packet for each group.&lt;/li&gt;
&lt;li&gt;Ask the agent for review findings with severity and evidence.&lt;/li&gt;
&lt;li&gt;Run static checks and tests.&lt;/li&gt;
&lt;li&gt;Store findings and mark only successful groups as reviewed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A review prompt might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;You are reviewing only the files in CHANGE_SET.
Do not comment on unchanged code unless it directly affects the changed lines.
Return findings as JSON with: severity, file, line, issue, evidence, suggested_fix.
If there are no findings, return an empty findings array.

CHANGE_SET:
&lt;span class="p"&gt;-&lt;/span&gt; src/billing/usage-meter.ts changed because hash differs from baseline
&lt;span class="p"&gt;-&lt;/span&gt; src/billing/usage-meter.test.ts changed because hash differs from baseline

PROJECT_RULES:
&lt;span class="p"&gt;-&lt;/span&gt; Billing usage must be tenant-scoped.
&lt;span class="p"&gt;-&lt;/span&gt; Metering writes must be idempotent.
&lt;span class="p"&gt;-&lt;/span&gt; Never trust client-provided tenant IDs.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That instruction matters: “only the files in CHANGE_SET.” It prevents the agent from turning a focused review into a wandering architecture critique.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example: Incremental Knowledge Base Refresh
&lt;/h2&gt;

&lt;p&gt;Now consider a RAG product. Many teams rebuild or rescan too much of the knowledge base after every update. That wastes embedding cost and can introduce stale chunks.&lt;/p&gt;

&lt;p&gt;A better workflow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Track document hashes by tenant and source.&lt;/li&gt;
&lt;li&gt;When a document changes, parse only that document.&lt;/li&gt;
&lt;li&gt;Delete old chunks for that document version.&lt;/li&gt;
&lt;li&gt;Create new chunks with version IDs.&lt;/li&gt;
&lt;li&gt;Run citation and retrieval smoke tests.&lt;/li&gt;
&lt;li&gt;Promote the new chunks only if tests pass.&lt;/li&gt;
&lt;li&gt;Mark the document version as indexed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A minimal schema might look 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;document_baselines&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;tenant_id&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;source_id&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;document_id&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;content_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;indexed_version&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;indexed_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="p"&gt;,&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="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="n"&gt;document_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;This avoids a common failure: mixing new chunks with old chunks and letting the model cite whatever looks plausible.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to Log
&lt;/h2&gt;

&lt;p&gt;Incremental workflows need auditability. Log enough to answer three questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What changed?&lt;/li&gt;
&lt;li&gt;What did the agent see?&lt;/li&gt;
&lt;li&gt;Why was the baseline advanced?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A useful run log includes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;workflow_name&lt;/td&gt;
&lt;td&gt;Separates baselines per agent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;run_id&lt;/td&gt;
&lt;td&gt;Makes retries traceable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;baseline_before&lt;/td&gt;
&lt;td&gt;Shows what the agent had already processed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;change_set&lt;/td&gt;
&lt;td&gt;Lists the exact inputs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;context_packet_hash&lt;/td&gt;
&lt;td&gt;Proves what context was shown&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;token_cost&lt;/td&gt;
&lt;td&gt;Tracks waste and budget drift&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tool_calls&lt;/td&gt;
&lt;td&gt;Shows what the agent tried to do&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;verification_result&lt;/td&gt;
&lt;td&gt;Explains success or failure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;baseline_after&lt;/td&gt;
&lt;td&gt;Shows what was marked complete&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Do not store sensitive raw prompts forever by default. Store hashes, redacted packets, and retention rules where possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Mistakes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Mistake 1: One Baseline for Every Workflow
&lt;/h3&gt;

&lt;p&gt;A single global baseline sounds simple, but it creates blind spots. Your docs agent, test agent, security agent, and support agent process different things at different speeds. Give them separate cursors.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 2: Advancing the Cursor on Partial Success
&lt;/h3&gt;

&lt;p&gt;If three files pass and one file fails, mark only the successful unit if your system supports partial baselines. Otherwise, keep the whole batch pending. Never hide failed work behind a successful timestamp.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 3: Letting the Model Choose Its Own Scope
&lt;/h3&gt;

&lt;p&gt;Models are helpful, but scope detection should be deterministic. Let code decide what changed. Let policy decide what the agent can see. Let the model reason inside those boundaries.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 4: Ignoring Deleted Files
&lt;/h3&gt;

&lt;p&gt;Deleted files are changes too. If a source document is deleted, remove its chunks. If a test is deleted, ask why. If a policy file disappears, escalate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 5: Reprocessing After Harmless Formatting Changes
&lt;/h3&gt;

&lt;p&gt;Normalize where it makes sense. If whitespace-only changes should not trigger a costly review, detect that. If generated snapshots are noisy, exclude them or process them with cheaper checks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing the Right Unit of Work
&lt;/h2&gt;

&lt;p&gt;The hardest design choice is granularity.&lt;/p&gt;

&lt;p&gt;A unit that is too large wastes context. A unit that is too small loses meaning.&lt;/p&gt;

&lt;p&gt;Use this rule of thumb:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For code: group by feature area or risk boundary.&lt;/li&gt;
&lt;li&gt;For docs: process one source document at a time.&lt;/li&gt;
&lt;li&gt;For support: process one conversation thread at a time.&lt;/li&gt;
&lt;li&gt;For analytics: process one metric definition or dashboard change at a time.&lt;/li&gt;
&lt;li&gt;For agents with tools: process one planned action batch at a time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The unit should be small enough to retry and large enough to verify.&lt;/p&gt;

&lt;h2&gt;
  
  
  How This Fits Into a Larger AI Product Stack
&lt;/h2&gt;

&lt;p&gt;Incremental workflows are not a replacement for observability, approval gates, sandboxing, or evaluation. They make those systems cheaper and sharper.&lt;/p&gt;

&lt;p&gt;They connect naturally with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LLM gateways&lt;/strong&gt; for model routing and prompt caching&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent observability&lt;/strong&gt; for traces and cost monitoring&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool budgets&lt;/strong&gt; for limiting expensive actions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Approval gates&lt;/strong&gt; for high-risk writes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RAG evaluation&lt;/strong&gt; for changed source documents&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Output provenance&lt;/strong&gt; for answer receipts and audit trails&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Think of incremental processing as the front door. It decides what deserves attention before the rest of your AI stack spends money.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Practical Rollout Plan
&lt;/h2&gt;

&lt;p&gt;Start with one workflow that already hurts.&lt;/p&gt;

&lt;p&gt;Good candidates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AI code review on pull requests&lt;/li&gt;
&lt;li&gt;Documentation refresh after merges&lt;/li&gt;
&lt;li&gt;RAG indexing after document updates&lt;/li&gt;
&lt;li&gt;Support ticket classification&lt;/li&gt;
&lt;li&gt;Product feedback clustering&lt;/li&gt;
&lt;li&gt;Security review for config changes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then roll it out in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Measure the current run.&lt;/strong&gt; Log average tokens, latency, retries, and failure rate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add deterministic change detection.&lt;/strong&gt; Do not involve the model yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create per-workflow baselines.&lt;/strong&gt; Start with content hashes or event cursors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filter aggressively.&lt;/strong&gt; Exclude files and records the workflow should never see.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build context packets.&lt;/strong&gt; Keep them small, structured, and repeatable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add verification gates.&lt;/strong&gt; Tests, schema checks, citations, or human review.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mark on success only.&lt;/strong&gt; Failed runs stay retryable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compare before and after.&lt;/strong&gt; Track cost, speed, and useful output rate.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the workflow does not improve after this, the agent may not be the problem. The task may need clearer acceptance criteria.&lt;/p&gt;

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

&lt;p&gt;Before you ship an incremental agent workflow, confirm:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each workflow has its own baseline.&lt;/li&gt;
&lt;li&gt;Change detection is deterministic.&lt;/li&gt;
&lt;li&gt;Deleted items are handled.&lt;/li&gt;
&lt;li&gt;Scope filters run before model calls.&lt;/li&gt;
&lt;li&gt;Context packets are structured and small.&lt;/li&gt;
&lt;li&gt;Sensitive data is redacted or permission checked.&lt;/li&gt;
&lt;li&gt;Verification gates run before baseline updates.&lt;/li&gt;
&lt;li&gt;Failed runs remain retryable.&lt;/li&gt;
&lt;li&gt;Token cost and latency are logged.&lt;/li&gt;
&lt;li&gt;The team can explain why each item was marked complete.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is not to make agents busy. The goal is to make them useful on the smallest safe slice of work.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  What is an incremental AI agent workflow?
&lt;/h3&gt;

&lt;p&gt;An incremental AI agent workflow processes only the files, records, tickets, or events that changed since the agent last completed its job. It uses baselines, change detection, scoped context, and success-based marking to avoid reprocessing everything.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is this only useful for coding agents?
&lt;/h3&gt;

&lt;p&gt;No. The same pattern works for RAG indexing, support triage, document review, analytics checks, security scans, product feedback analysis, and any workflow where new or changed items arrive over time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I use timestamps or content hashes for baselines?
&lt;/h3&gt;

&lt;p&gt;Use content hashes when correctness matters and the source can change without a reliable timestamp. Use timestamps or event cursors for queues and databases where ordering is trustworthy. Many production systems use both.&lt;/p&gt;

&lt;h3&gt;
  
  
  How does incremental processing reduce AI cost?
&lt;/h3&gt;

&lt;p&gt;It cuts repeated context. The model sees only changed items plus necessary surrounding context, so token usage, latency, and retries usually drop. It also makes failures easier to isolate and replay.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens if an agent fails after processing some changes?
&lt;/h3&gt;

&lt;p&gt;Do not advance the baseline for failed work. Either keep the whole batch pending or mark only the verified successful units. This keeps retries safe and prevents silent data loss.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can incremental workflows improve AI answer quality?
&lt;/h3&gt;

&lt;p&gt;Yes. Smaller, cleaner context often improves focus. The agent is less likely to chase stale files, irrelevant documents, or old conversations when the workflow gives it a precise change set and acceptance rules.&lt;/p&gt;

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

&lt;p&gt;Small teams benefit early because they feel token waste, slow runs, and review fatigue quickly. You can start with a simple JSON baseline file or database table before adding a full workflow engine.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>saas</category>
      <category>agents</category>
      <category>productivity</category>
    </item>
    <item>
      <title>AI Provider Risk Matrix: Route Requests Without Leaking Customer Data</title>
      <dc:creator>Jack M</dc:creator>
      <pubDate>Sun, 30 Aug 2026 05:44:00 +0000</pubDate>
      <link>https://dev.to/jackm-singularity/ai-provider-risk-matrix-route-requests-without-leaking-customer-data-55ba</link>
      <guid>https://dev.to/jackm-singularity/ai-provider-risk-matrix-route-requests-without-leaking-customer-data-55ba</guid>
      <description>&lt;p&gt;AI builders used to ask one routing question: "Which model is best for this task?"&lt;/p&gt;

&lt;p&gt;That is no longer enough.&lt;/p&gt;

&lt;p&gt;If your app sends customer prompts, files, tool results, CRM notes, support tickets, or analytics questions to several model providers, every request now carries a second question: &lt;strong&gt;where is this data going, who can retain it, and what happens when the fallback provider is riskier than the primary one?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is where an &lt;strong&gt;AI provider risk matrix&lt;/strong&gt; helps. It gives your product one practical way to route LLM requests by capability, cost, latency, data retention, jurisdiction, BYOK support, and customer trust requirements. Not as a legal document. Not as vendor drama. As engineering control.&lt;/p&gt;

&lt;p&gt;Recent developer discussions around model gateways, provider marketplaces, BYOK, token budgets, and data retention show the same pattern: the AI stack is becoming multi-provider by default. That gives builders leverage, but it also creates quiet failure modes. Let's build the missing layer before that happens.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why provider risk is now a product architecture problem
&lt;/h2&gt;

&lt;p&gt;A modern AI product often uses more than one model path:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a cheap model for classification&lt;/li&gt;
&lt;li&gt;a stronger model for final answers&lt;/li&gt;
&lt;li&gt;an embedding provider for retrieval&lt;/li&gt;
&lt;li&gt;a local model for sensitive preprocessing&lt;/li&gt;
&lt;li&gt;a gateway for failover&lt;/li&gt;
&lt;li&gt;a hosted provider for vision or speech&lt;/li&gt;
&lt;li&gt;a separate agent runtime for tool-heavy tasks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is useful. It is also a trust boundary map.&lt;/p&gt;

&lt;p&gt;Each provider may differ on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;whether prompts are retained&lt;/li&gt;
&lt;li&gt;whether outputs are retained&lt;/li&gt;
&lt;li&gt;whether data can train models&lt;/li&gt;
&lt;li&gt;where the provider is headquartered&lt;/li&gt;
&lt;li&gt;where processing happens&lt;/li&gt;
&lt;li&gt;whether BYOK is supported&lt;/li&gt;
&lt;li&gt;whether zero data retention is available&lt;/li&gt;
&lt;li&gt;whether logs can be disabled&lt;/li&gt;
&lt;li&gt;whether customer-specific keys are possible&lt;/li&gt;
&lt;li&gt;whether regulated workloads are allowed&lt;/li&gt;
&lt;li&gt;whether a subprocessor list exists&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Developers often discover these details late, after the integration works. That is backwards. Provider eligibility should be decided before routing rules go live.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with task risk labels
&lt;/h2&gt;

&lt;p&gt;Do not begin by ranking providers. Begin by labeling requests.&lt;/p&gt;

&lt;p&gt;A provider risk matrix only works if each AI task has a risk class. Keep the first version simple.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Risk label&lt;/th&gt;
&lt;th&gt;Example tasks&lt;/th&gt;
&lt;th&gt;Data allowed&lt;/th&gt;
&lt;th&gt;Routing rule&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Public&lt;/td&gt;
&lt;td&gt;Rewrite public docs, generate sample code, classify public pages&lt;/td&gt;
&lt;td&gt;Public data only&lt;/td&gt;
&lt;td&gt;Any approved provider&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Internal&lt;/td&gt;
&lt;td&gt;Summarize internal specs, draft roadmap notes&lt;/td&gt;
&lt;td&gt;Non-customer business data&lt;/td&gt;
&lt;td&gt;Approved providers with retention review&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Customer&lt;/td&gt;
&lt;td&gt;Support tickets, CRM notes, uploaded documents&lt;/td&gt;
&lt;td&gt;Customer-owned data&lt;/td&gt;
&lt;td&gt;Zero-retention or contracted providers only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sensitive&lt;/td&gt;
&lt;td&gt;Secrets, health/legal/financial notes, private identifiers&lt;/td&gt;
&lt;td&gt;Highly restricted data&lt;/td&gt;
&lt;td&gt;Local, masked, or explicitly approved path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Regulated&lt;/td&gt;
&lt;td&gt;Compliance-bound workloads&lt;/td&gt;
&lt;td&gt;Policy-bound data&lt;/td&gt;
&lt;td&gt;Legal/security-approved path only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This table gives your app something concrete to enforce. Without task labels, your gateway is guessing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example task registry
&lt;/h3&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;TaskRisk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;public&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="s1"&gt;internal&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="s1"&gt;customer&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="s1"&gt;sensitive&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="s1"&gt;regulated&lt;/span&gt;&lt;span class="dl"&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;AiTask&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;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;risk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TaskRisk&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;allowedInputs&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;requiresCitations&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;requiresHumanReview&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;boolean&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;const&lt;/span&gt; &lt;span class="nx"&gt;tasks&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;AiTask&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;docs_rewrite&lt;/span&gt;&lt;span class="p"&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="s1"&gt;docs_rewrite&lt;/span&gt;&lt;span class="dl"&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Rewrite public documentation&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="s1"&gt;public&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;allowedInputs&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="s1"&gt;public_markdown&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;ticket_summary&lt;/span&gt;&lt;span class="p"&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="s1"&gt;ticket_summary&lt;/span&gt;&lt;span class="dl"&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Summarize customer support ticket&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="s1"&gt;customer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;allowedInputs&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="s1"&gt;ticket_body&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="s1"&gt;account_metadata&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;requiresCitations&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="na"&gt;contract_clause_review&lt;/span&gt;&lt;span class="p"&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="s1"&gt;contract_clause_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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Review uploaded contract clause&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="s1"&gt;regulated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;allowedInputs&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="s1"&gt;customer_document&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;requiresHumanReview&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This registry can live beside your prompt templates, evals, and tool definitions. The goal is not bureaucracy. The goal is to stop sensitive tasks from using a provider path meant for harmless text generation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build the provider risk matrix
&lt;/h2&gt;

&lt;p&gt;Now create a provider registry. You do not need a perfect vendor risk platform on day one. You need enough structured metadata to keep routing honest.&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;ProviderRiskTier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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="s1"&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="s1"&gt;high&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="s1"&gt;blocked&lt;/span&gt;&lt;span class="dl"&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;RetentionPolicy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zero_retention&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="s1"&gt;limited_retention&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="s1"&gt;retains_prompts&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="s1"&gt;unknown&lt;/span&gt;&lt;span class="dl"&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;ProviderProfile&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;displayName&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;riskTier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ProviderRiskTier&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;retention&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RetentionPolicy&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;supportsBYOK&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;approvedTaskRisks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TaskRisk&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nl"&gt;regions&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;trainsOnCustomerData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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;maxDataClass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TaskRisk&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;notes&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;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;providers&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;ProviderProfile&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;local_small_model&lt;/span&gt;&lt;span class="p"&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="s1"&gt;local_small_model&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Local small model&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;riskTier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;low&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;retention&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zero_retention&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;supportsBYOK&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;approvedTaskRisks&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="s1"&gt;public&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="s1"&gt;internal&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="s1"&gt;customer&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="s1"&gt;sensitive&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;regions&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="s1"&gt;local&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;trainsOnCustomerData&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;maxDataClass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sensitive&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Good for preprocessing, classification, redaction, and low-risk drafts.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;hosted_frontier_primary&lt;/span&gt;&lt;span class="p"&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="s1"&gt;hosted_frontier_primary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hosted frontier primary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;riskTier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;medium&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;retention&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;limited_retention&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;supportsBYOK&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="na"&gt;approvedTaskRisks&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="s1"&gt;public&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="s1"&gt;internal&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="s1"&gt;customer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;regions&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="s1"&gt;us&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="s1"&gt;eu&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;trainsOnCustomerData&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;maxDataClass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;customer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Use for complex reasoning after privacy filters run.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;experimental_coding_model&lt;/span&gt;&lt;span class="p"&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="s1"&gt;experimental_coding_model&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Experimental coding model&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;riskTier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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;retention&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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;supportsBYOK&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;approvedTaskRisks&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="s1"&gt;public&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;regions&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="s1"&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;trainsOnCustomerData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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;maxDataClass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;public&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Only public examples and synthetic benchmark tasks.&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 the important part: the matrix does not say one provider is universally safe or unsafe. It says what each provider is allowed to process.&lt;/p&gt;

&lt;p&gt;That is the difference between vendor preference and production policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add routing rules that fail closed
&lt;/h2&gt;

&lt;p&gt;A risk matrix is only useful if your routing layer enforces it. The router should check task risk before it checks price.&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;riskRank&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;risk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TaskRisk&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;public&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;internal&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="na"&gt;customer&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;sensitive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;regulated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
  &lt;span class="p"&gt;}[&lt;/span&gt;&lt;span class="nx"&gt;risk&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;canUseProvider&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;AiTask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ProviderProfile&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;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;riskTier&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;blocked&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="kc"&gt;false&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;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;approvedTaskRisks&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;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;risk&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&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;riskRank&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;risk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;riskRank&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxDataClass&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&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;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;risk&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;customer&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="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;retention&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;retains_prompts&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="kc"&gt;false&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;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;risk&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;regulated&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="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;retention&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zero_retention&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="kc"&gt;false&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;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trainsOnCustomerData&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&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;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trainsOnCustomerData&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unknown&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="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;risk&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;public&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="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;chooseProvider&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;AiTask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ProviderProfile&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;eligible&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;candidates&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;provider&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;canUseProvider&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;provider&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;eligible&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&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="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;`No approved provider for task: &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;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;return&lt;/span&gt; &lt;span class="nx"&gt;eligible&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="c1"&gt;// Replace with quality/cost/latency scoring after eligibility.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key principle: &lt;strong&gt;privacy eligibility comes before optimization&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;After a provider passes eligibility, you can rank by model quality, latency, cost per task, context length, or uptime. But an unsafe provider should never win because it is cheaper.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do not let fallback break the policy
&lt;/h2&gt;

&lt;p&gt;Fallbacks are where many routing systems quietly fail.&lt;/p&gt;

&lt;p&gt;A provider outage happens. Latency spikes. The gateway falls back to another model. The user still gets an answer, so the incident looks solved.&lt;/p&gt;

&lt;p&gt;But did the fallback provider have the same retention approval? Did it support the same region? Was it approved for customer data? Did it use a customer-owned key or a shared platform key?&lt;/p&gt;

&lt;p&gt;Your fallback graph needs the same risk checks as the primary path.&lt;/p&gt;

&lt;p&gt;Validate the primary and every fallback at deploy time. If any provider in the chain is not approved for the task, fail the route plan before production traffic reaches it.&lt;/p&gt;

&lt;p&gt;This one check prevents a common mistake: treating fallback as an operations concern instead of a data governance concern.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use BYOK, but do not treat it as magic
&lt;/h2&gt;

&lt;p&gt;Bring-your-own-key is valuable because it can give customers more control over billing, provider relationship, and sometimes data handling. It is also easy to overtrust.&lt;/p&gt;

&lt;p&gt;BYOK does not automatically answer every question. You still need to know which provider receives the data, whether logs include prompts, whether keys are encrypted and scoped, and whether failed requests are retried through another provider. For multi-tenant apps, store BYOK configuration as a tenant-scoped policy, not just a secret.&lt;/p&gt;

&lt;p&gt;This policy should travel with each request so the router can answer a better question: not "does this model work?" but "is this model allowed for this tenant and this task?"&lt;/p&gt;

&lt;h2&gt;
  
  
  Add a privacy filter before the gateway
&lt;/h2&gt;

&lt;p&gt;A risk matrix should reduce unnecessary data exposure, not just choose a provider.&lt;/p&gt;

&lt;p&gt;Before a request reaches a hosted model, run a privacy filter that can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;remove secrets&lt;/li&gt;
&lt;li&gt;mask emails and phone numbers when not needed&lt;/li&gt;
&lt;li&gt;replace names with stable placeholders&lt;/li&gt;
&lt;li&gt;strip irrelevant document chunks&lt;/li&gt;
&lt;li&gt;downgrade tasks from customer risk to internal risk when data is fully synthetic or masked&lt;/li&gt;
&lt;li&gt;block the request when masking would break correctness&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In production, use stronger detectors, structured parsers, allowlists, and tests. The pattern matters: do not send raw data by default.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to log without creating a new liability
&lt;/h2&gt;

&lt;p&gt;Audit logs are necessary, but raw prompt logs can become a second sensitive database.&lt;/p&gt;

&lt;p&gt;Log enough to explain the routing decision without storing everything forever.&lt;/p&gt;

&lt;p&gt;Useful fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;request ID&lt;/li&gt;
&lt;li&gt;tenant ID hash&lt;/li&gt;
&lt;li&gt;task ID&lt;/li&gt;
&lt;li&gt;task risk label&lt;/li&gt;
&lt;li&gt;chosen provider&lt;/li&gt;
&lt;li&gt;fallback provider, if used&lt;/li&gt;
&lt;li&gt;retention class at request time&lt;/li&gt;
&lt;li&gt;region decision&lt;/li&gt;
&lt;li&gt;BYOK flag&lt;/li&gt;
&lt;li&gt;prompt template version&lt;/li&gt;
&lt;li&gt;input hash&lt;/li&gt;
&lt;li&gt;output hash&lt;/li&gt;
&lt;li&gt;redaction summary&lt;/li&gt;
&lt;li&gt;policy decision&lt;/li&gt;
&lt;li&gt;reviewer ID, if human approval happened&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Avoid storing raw prompts unless the task requires it and your retention policy allows it.&lt;/p&gt;

&lt;p&gt;That is enough to explain the decision without turning observability into oversharing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compare providers by risk dimension, not vibes
&lt;/h2&gt;

&lt;p&gt;Here is a practical scoring model you can adapt.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Low risk&lt;/th&gt;
&lt;th&gt;Medium risk&lt;/th&gt;
&lt;th&gt;High risk&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Prompt retention&lt;/td&gt;
&lt;td&gt;Zero retention&lt;/td&gt;
&lt;td&gt;Short retention with contract&lt;/td&gt;
&lt;td&gt;Unknown or broad retention&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Training use&lt;/td&gt;
&lt;td&gt;Explicitly disabled&lt;/td&gt;
&lt;td&gt;Disabled for paid/API tier&lt;/td&gt;
&lt;td&gt;Unknown or opt-out unclear&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Region&lt;/td&gt;
&lt;td&gt;Meets tenant policy&lt;/td&gt;
&lt;td&gt;Region unclear but allowed&lt;/td&gt;
&lt;td&gt;Conflicts with tenant policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BYOK&lt;/td&gt;
&lt;td&gt;Tenant-scoped and encrypted&lt;/td&gt;
&lt;td&gt;Platform key with controls&lt;/td&gt;
&lt;td&gt;Shared key with weak isolation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Logs&lt;/td&gt;
&lt;td&gt;Metadata-only by default&lt;/td&gt;
&lt;td&gt;Payload logs limited&lt;/td&gt;
&lt;td&gt;Raw prompt logs retained&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Subprocessors&lt;/td&gt;
&lt;td&gt;Published and reviewed&lt;/td&gt;
&lt;td&gt;Published but broad&lt;/td&gt;
&lt;td&gt;Unknown&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fallback behavior&lt;/td&gt;
&lt;td&gt;Policy-checked&lt;/td&gt;
&lt;td&gt;Partially checked&lt;/td&gt;
&lt;td&gt;Silent cross-provider fallback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Support for deletion&lt;/td&gt;
&lt;td&gt;Clear deletion path&lt;/td&gt;
&lt;td&gt;Manual process&lt;/td&gt;
&lt;td&gt;Unknown&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Use these dimensions consistently in code review. The matrix should make risk visible before an incident forces the conversation.&lt;/p&gt;

&lt;h2&gt;
  
  
  How this changes your implementation workflow
&lt;/h2&gt;

&lt;p&gt;For solo builders and small teams, the workflow can be lightweight:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a task registry.&lt;/li&gt;
&lt;li&gt;Label each task by data risk.&lt;/li&gt;
&lt;li&gt;Create provider profiles.&lt;/li&gt;
&lt;li&gt;Add policy checks before model routing.&lt;/li&gt;
&lt;li&gt;Add privacy filtering before hosted calls.&lt;/li&gt;
&lt;li&gt;Validate primary and fallback providers together.&lt;/li&gt;
&lt;li&gt;Log routing decisions without storing raw prompts by default.&lt;/li&gt;
&lt;li&gt;Review the matrix whenever a provider, gateway, or model changes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This gives you a production habit, not a giant compliance program.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes to avoid
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Routing by cheapest model first:&lt;/strong&gt; cost matters, but eligibility must come before price.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assuming every API tier has the same policy:&lt;/strong&gt; record the exact product, region, and account terms you reviewed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forgetting embeddings:&lt;/strong&gt; embeddings, rerankers, vision, speech, and parsers can leak sensitive input too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logging raw prompts forever:&lt;/strong&gt; use sampling, expiry, hashing, and redaction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Letting agents choose providers:&lt;/strong&gt; provider eligibility should be deterministic policy code, not a model decision.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A simple implementation checklist
&lt;/h2&gt;

&lt;p&gt;Use this as a starting point:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Every AI task has a risk label.&lt;/li&gt;
&lt;li&gt;[ ] Every provider has a retention classification.&lt;/li&gt;
&lt;li&gt;[ ] Every provider has approved task classes.&lt;/li&gt;
&lt;li&gt;[ ] Fallback providers are checked against the same policy as primary providers.&lt;/li&gt;
&lt;li&gt;[ ] BYOK settings are tenant-scoped.&lt;/li&gt;
&lt;li&gt;[ ] Privacy filters run before hosted calls.&lt;/li&gt;
&lt;li&gt;[ ] Embeddings and rerankers are included in the matrix.&lt;/li&gt;
&lt;li&gt;[ ] Audit logs record decisions without raw prompts by default.&lt;/li&gt;
&lt;li&gt;[ ] New providers require a review before production traffic.&lt;/li&gt;
&lt;li&gt;[ ] Tests fail if a sensitive task can route to an unapproved provider.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Add a policy test before you ship
&lt;/h2&gt;

&lt;p&gt;The best risk matrix is boring because tests catch mistakes early.&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;describe&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;it&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="s1"&gt;vitest&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;tasks&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="s1"&gt;./tasks&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;providers&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="s1"&gt;./providers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;canUseProvider&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="s1"&gt;./router&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;provider risk policy&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;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;blocks customer tasks from prompt-retaining providers&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;task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ticket_summary&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;provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;experimental_coding_model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;retention&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;retains_prompts&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="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;canUseProvider&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;provider&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="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="s1"&gt;allows public docs tasks on experimental providers&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;canUseProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;docs_rewrite&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;experimental_coding_model&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This turns trust rules into code review artifacts. When someone adds a new provider, the diff shows what changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final takeaway
&lt;/h2&gt;

&lt;p&gt;Multi-provider AI routing is becoming normal. That is good for cost, resilience, and model quality. But it also means your product needs a clear answer to a simple trust question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which providers are allowed to see which data, for which task, under which tenant policy?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;An AI provider risk matrix gives you that answer in code.&lt;/p&gt;

&lt;p&gt;Start small. Label tasks. Classify providers. Enforce routing before optimization. Check fallbacks. Keep logs useful but restrained.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  What is an AI provider risk matrix?
&lt;/h3&gt;

&lt;p&gt;An AI provider risk matrix is a structured table or config file that classifies model providers by retention policy, region, BYOK support, logging behavior, training use, and approved task types. It helps your AI routing layer decide which provider is allowed for each request.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is an AI provider risk matrix the same as an LLM gateway?
&lt;/h3&gt;

&lt;p&gt;No. An LLM gateway executes routing, retries, caching, and observability. A provider risk matrix is the policy data the gateway should use before it routes sensitive tasks. The matrix says what is allowed; the gateway enforces it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do solo builders need provider risk tiers?
&lt;/h3&gt;

&lt;p&gt;Yes, but the first version can be simple. Even a solo developer can label tasks as public, customer, sensitive, or regulated and prevent accidental routing to unapproved providers. This is especially useful when adding fallbacks or testing new models.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does BYOK solve AI data privacy risk?
&lt;/h3&gt;

&lt;p&gt;BYOK helps, but it is not a full privacy strategy. You still need tenant-scoped key storage, provider approval, routing rules, fallback limits, logging controls, and a clear policy for prompt and output retention.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I log raw prompts for debugging?
&lt;/h3&gt;

&lt;p&gt;Only when you truly need them and your policy allows it. Prefer metadata, hashes, redaction summaries, prompt versions, provider decisions, and short retention windows. Raw prompt logs can become a sensitive data store.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens if no provider is approved for a task?
&lt;/h3&gt;

&lt;p&gt;Fail closed. Return a safe error, ask for human review, run a local redaction step, or require an admin to approve a new provider. Do not silently downgrade to a less trusted provider just to complete the request.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>saas</category>
      <category>privacy</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
