<?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: Raju Dandigam</title>
    <description>The latest articles on DEV Community by Raju Dandigam (@raju_dandigam).</description>
    <link>https://dev.to/raju_dandigam</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%2F1726463%2F38d1e46f-d122-4fa3-b130-772169c24466.png</url>
      <title>DEV Community: Raju Dandigam</title>
      <link>https://dev.to/raju_dandigam</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/raju_dandigam"/>
    <language>en</language>
    <item>
      <title>Docker Sandboxes Changed the Trust Boundary for AI Coding Agents</title>
      <dc:creator>Raju Dandigam</dc:creator>
      <pubDate>Sun, 20 Sep 2026 16:42:38 +0000</pubDate>
      <link>https://dev.to/raju_dandigam/docker-sandboxes-changed-the-trust-boundary-for-ai-coding-agents-10k6</link>
      <guid>https://dev.to/raju_dandigam/docker-sandboxes-changed-the-trust-boundary-for-ai-coding-agents-10k6</guid>
      <description>&lt;p&gt;A coding agent that can only suggest a patch is one kind of risk. An agent with a shell, package manager, Docker daemon, credentials, and network access is another.&lt;/p&gt;

&lt;p&gt;The useful security question is not “Is the agent inside a container?” It is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which resources cross the isolation boundary, in which direction, and with what authority?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Docker Sandboxes make that question concrete. A local sandbox runs the agent inside a microVM with its own kernel and Docker Engine. The agent has broad control inside that VM—including &lt;code&gt;sudo&lt;/code&gt;—while access to the host is mediated through explicit workspace, credential, network, skills, and MCP boundaries.&lt;/p&gt;

&lt;p&gt;That is a stronger model than starting an agent directly on a developer laptop. It is not the same as making the agent harmless.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the workspace mode
&lt;/h2&gt;

&lt;p&gt;There are three materially different file boundaries:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Host repository&lt;/th&gt;
&lt;th&gt;Agent writes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Direct mount&lt;/td&gt;
&lt;td&gt;Shared read-write&lt;/td&gt;
&lt;td&gt;Appear immediately in the working tree&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clone mode&lt;/td&gt;
&lt;td&gt;Host repository mounted read-only&lt;/td&gt;
&lt;td&gt;Private in-VM clone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mountless&lt;/td&gt;
&lt;td&gt;Not shared&lt;/td&gt;
&lt;td&gt;Sandbox filesystem only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Direct mode is convenient for an interactive edit-review loop. It also means the agent can change &lt;code&gt;package.json&lt;/code&gt; scripts, CI configuration, editor tasks, AI configuration, and other files that may execute later on the host.&lt;/p&gt;

&lt;p&gt;Clone mode changes the review boundary. The agent works on a private clone while the original host repository remains read-only. That is a better default for broad exploratory tasks or unfamiliar repositories. Mountless mode is strongest when the task does not require host files.&lt;/p&gt;

&lt;p&gt;The choice should follow the task, not developer habit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Isolation does not end at the filesystem
&lt;/h2&gt;

&lt;p&gt;Docker's local sandbox model separates several capabilities:&lt;/p&gt;

&lt;h3&gt;
  
  
  Docker Engine
&lt;/h3&gt;

&lt;p&gt;The sandbox receives a private Docker Engine rather than access to the host daemon. This matters because mounting the host Docker socket into an ordinary container can effectively grant host-level control.&lt;/p&gt;

&lt;h3&gt;
  
  
  Credentials
&lt;/h3&gt;

&lt;p&gt;Provider credentials can be injected by a host-side proxy into permitted outbound requests, so raw key values do not need to enter the VM. This reduces credential exposure, but the agent can still exercise whatever authority those proxied credentials grant.&lt;/p&gt;

&lt;h3&gt;
  
  
  Network
&lt;/h3&gt;

&lt;p&gt;Outbound TCP traffic passes through a host proxy and a deny-by-default policy. Review the active allowlist. Broad domains may permit more services than the task needs.&lt;/p&gt;

&lt;p&gt;“Deny by default” does not necessarily mean “nothing is reachable.” Docker's Balanced preset begins with a baseline allowlist for common model providers, package managers, code hosts, registries, and cloud services; Open and Locked Down differ materially. Inspect the effective rules with &lt;code&gt;sbx policy ls&lt;/code&gt; and narrow them for the task instead of inferring the policy from the product name.&lt;/p&gt;

&lt;h3&gt;
  
  
  Shared skills
&lt;/h3&gt;

&lt;p&gt;A shared skill store is a deliberate exception: one sandbox can modify instructions or scripts later consumed by another. If several sandboxes share it read-write, treat them as participating in the same trust boundary.&lt;/p&gt;

&lt;h3&gt;
  
  
  MCP servers
&lt;/h3&gt;

&lt;p&gt;The MCP gateway is another explicit bridge. Remote servers remain outside the VM. Local stdio MCP servers run on the host, not inside the sandbox. A host-side MCP tool can therefore have authority the sandbox itself does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Threat-model the whole action path
&lt;/h2&gt;

&lt;p&gt;For every capability, write down four facts:&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;CapabilityBoundary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;workspace&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;network&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;credential&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;mcp&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;skill&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;into_sandbox&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;out_of_sandbox&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;both&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;authority&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;reviewBeforeUse&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;For example:&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;policy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CapabilityBoundary&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="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;workspace&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;both&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;authority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;private clone only&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;reviewBeforeUse&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;network&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;out_of_sandbox&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;authority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;registry and model API only&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;reviewBeforeUse&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="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mcp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;both&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;authority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;read-only issue tracker&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;reviewBeforeUse&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 forces “the agent has MCP” into a specific statement about a specific server and tool set.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical review sequence
&lt;/h2&gt;

&lt;p&gt;Before running an autonomous coding task:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Prefer clone or mountless mode unless live host edits are required.&lt;/li&gt;
&lt;li&gt;Remove network destinations the task does not need.&lt;/li&gt;
&lt;li&gt;Provide task-scoped credentials with the least useful authority.&lt;/li&gt;
&lt;li&gt;Review every local MCP server as host code.&lt;/li&gt;
&lt;li&gt;Disable shared skills when cross-sandbox mutation is unnecessary.&lt;/li&gt;
&lt;li&gt;Inspect changes before executing modified hooks, scripts, or CI files.&lt;/li&gt;
&lt;li&gt;Keep human approval around publishing, deployment, and other irreversible actions.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The last step matters because isolation controls &lt;em&gt;where&lt;/em&gt; code runs. It does not decide whether a proposed business action is appropriate.&lt;/p&gt;

&lt;p&gt;The discussion above is about Docker's documented &lt;strong&gt;local&lt;/strong&gt; sandbox model. Cloud sandboxes have their own lifecycle, credential, and connectivity behavior. Record which environment executed the task before treating a sandbox result as security evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  A sandbox is a boundary, not a verdict
&lt;/h2&gt;

&lt;p&gt;MicroVM isolation, a private Docker daemon, proxied credentials, and explicit network policy significantly improve the execution boundary for coding agents. The remaining risk travels through the resources intentionally shared across it.&lt;/p&gt;

&lt;p&gt;That is the design lesson worth carrying to any agent environment: grant the smallest workspace, network, credential, and tool surface that can complete the task—and treat every bridge back to the host as part of the security model.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.docker.com/ai/sandboxes/" rel="noopener noreferrer"&gt;Docker Sandboxes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.docker.com/ai/sandboxes/security/" rel="noopener noreferrer"&gt;Docker Sandboxes security model&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>docker</category>
      <category>ai</category>
      <category>security</category>
      <category>devops</category>
    </item>
    <item>
      <title>Your Cypress Retry Passed - But the Test Is Still Telling You Something</title>
      <dc:creator>Raju Dandigam</dc:creator>
      <pubDate>Sat, 19 Sep 2026 17:36:31 +0000</pubDate>
      <link>https://dev.to/raju_dandigam/your-cypress-retry-passed-but-the-test-is-still-telling-you-something-37hf</link>
      <guid>https://dev.to/raju_dandigam/your-cypress-retry-passed-but-the-test-is-still-telling-you-something-37hf</guid>
      <description>&lt;p&gt;Attempt one fails. Attempt two passes. CI turns green.&lt;/p&gt;

&lt;p&gt;Did the system recover from harmless noise, or did the retry hide an intermittent product defect?&lt;/p&gt;

&lt;p&gt;Cypress retries are useful because browser tests meet networks, animations, databases, and asynchronous UI state. The mistake is treating a passing retry as if the first failure never happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  A result has two dimensions
&lt;/h2&gt;

&lt;p&gt;Store the final verdict and the attempt history separately:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Attempts&lt;/th&gt;
&lt;th&gt;Final result&lt;/th&gt;
&lt;th&gt;Reliability signal&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;Stable in this run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fail, Pass&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;Flaky evidence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fail, Fail, Fail&lt;/td&gt;
&lt;td&gt;Fail&lt;/td&gt;
&lt;td&gt;Consistent failure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pass, Fail&lt;/td&gt;
&lt;td&gt;Depends on strategy&lt;/td&gt;
&lt;td&gt;Non-repeatable behavior&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A pipeline may reasonably allow &lt;code&gt;Fail, Pass&lt;/code&gt; while a team investigates. It should not classify it as equivalent to &lt;code&gt;Pass&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The first failed attempt preserves evidence about the unstable boundary: an element appeared late, a request escaped the stub, state leaked from a previous test, or a backend was unavailable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choose retry semantics explicitly
&lt;/h2&gt;

&lt;p&gt;Standard Cypress retries stop when a test passes:&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;defineConfig&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;cypress&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;runMode&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;openMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is an availability-oriented policy: give the test more chances before failing the build.&lt;/p&gt;

&lt;p&gt;For a high-signal suite, Cypress also provides experimental retry strategies. This configuration fails the final result if &lt;strong&gt;any&lt;/strong&gt; attempt failed, while stopping as soon as a later pass proves flakiness:&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;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;experimentalStrategy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;detect-flake-but-always-fail&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;experimentalOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;maxRetries&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;stopIfAnyPassed&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="na"&gt;openMode&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;runMode&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;That policy is intentionally strict. Use it for release-critical journeys where intermittent behavior is itself a failure. Experimental retries are global, use boolean &lt;code&gt;openMode&lt;/code&gt; and &lt;code&gt;runMode&lt;/code&gt;, and can change in future Cypress releases, so pin and review the version.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preserve the failed attempt
&lt;/h2&gt;

&lt;p&gt;Cypress gives failed retry screenshots attempt-number suffixes. If you use Cypress Cloud, retry history, screenshots, video, and failure details are available in the test result history.&lt;/p&gt;

&lt;p&gt;For local CI artifacts, retain evidence when any attempt failed—not only when the final verdict failed. In the Node event lifecycle, each test result contains attempts:&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;hadFailedAttempt&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;CypressCommandLine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CypressRunResult&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;results&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;finished&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;true&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;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;runs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&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="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;attempts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;attempt&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;attempt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact reporting integration depends on your runner, but the policy should be clear: a flaky pass may unblock the job, yet it must still create a triage record with its failed-attempt artifacts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diagnose the class of flake
&lt;/h2&gt;

&lt;p&gt;Do not begin by increasing the retry count. Classify the failure first.&lt;/p&gt;

&lt;h3&gt;
  
  
  Application race
&lt;/h3&gt;

&lt;p&gt;The UI exposes an intermediate state as actionable. Fix the application state model or wait on a user-visible invariant.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test race
&lt;/h3&gt;

&lt;p&gt;The test uses arbitrary sleeps or reads before the page is ready. Replace time guesses with retryable queries and assertions.&lt;/p&gt;

&lt;h3&gt;
  
  
  State leakage
&lt;/h3&gt;

&lt;p&gt;The outcome depends on order, cached data, or an uncleared session. Run the spec repeatedly in isolation and in shuffled suite order.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dependency instability
&lt;/h3&gt;

&lt;p&gt;A real network or service participates in a test intended to be deterministic. Stub it, or move the test to a separately classified integration suite.&lt;/p&gt;

&lt;h3&gt;
  
  
  Genuine environmental noise
&lt;/h3&gt;

&lt;p&gt;Occasional browser or infrastructure failures do happen. Track their rate so “temporary” does not become permanent background failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat flake rate as owned work
&lt;/h2&gt;

&lt;p&gt;Use a rolling window, not one alarming run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flake rate = tests with mixed attempts / tests executed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Segment by spec, browser, operating system, and failure signature. Do not put unbounded error text into metric labels; keep detailed evidence in logs and artifacts.&lt;/p&gt;

&lt;p&gt;Set an ownership rule: quarantine only with an issue, an owner, an expiry, and a reason. A quarantined test without an exit condition is a deleted test with extra steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give flakiness its own budget
&lt;/h2&gt;

&lt;p&gt;A release pipeline can separate product failure from suite instability without ignoring either:&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;ReliabilityGate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;failedTests&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;flakyTests&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;maxFlakyTests&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;mayRelease&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ReliabilityGate&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;gate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;failedTests&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;gate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;flakyTests&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;gate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxFlakyTests&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;That example is a policy shape, not a recommended threshold. A payment or authentication journey may allow zero flaky tests; a lower-risk suite may temporarily tolerate a small, owned backlog. Tighten the budget as causes are removed, and fail immediately when a known critical spec exhibits mixed attempts.&lt;/p&gt;

&lt;p&gt;Trend the first-failure signature separately from the final verdict. If ten unrelated tests start failing first on the same network wait, the shared dependency—not ten selectors—is probably the better investigation target.&lt;/p&gt;

&lt;p&gt;Retries answer a narrow question: can this test pass on another attempt? They do not answer whether the application is reliable. Preserve the first failure, choose the final-status policy deliberately, and make flaky green different from stable green.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.cypress.io/app/guides/test-retries" rel="noopener noreferrer"&gt;Cypress test retries&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.cypress.io/app/references/experiments#Experimental-Test-Retries" rel="noopener noreferrer"&gt;Cypress experimental retry strategies&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>cypress</category>
      <category>testing</category>
      <category>devops</category>
      <category>sre</category>
    </item>
    <item>
      <title>Testing Streaming AI Interfaces with Cypress Without Asserting Every Token</title>
      <dc:creator>Raju Dandigam</dc:creator>
      <pubDate>Fri, 18 Sep 2026 16:02:27 +0000</pubDate>
      <link>https://dev.to/raju_dandigam/testing-streaming-ai-interfaces-with-cypress-without-asserting-every-token-9a4</link>
      <guid>https://dev.to/raju_dandigam/testing-streaming-ai-interfaces-with-cypress-without-asserting-every-token-9a4</guid>
      <description>&lt;p&gt;A streaming AI test becomes brittle the moment it expects this exact sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hel → Hello → Hello, I → Hello, I can help
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Chunk boundaries are transport details. They can change with buffering, provider behavior, network timing, or a client-library upgrade while the visible result remains correct.&lt;/p&gt;

&lt;p&gt;The UI still has deterministic responsibilities. It must show progress, distinguish a tool call from ordinary text, ignore stale events, support cancellation, and end in a coherent state. Test those contracts instead of every token.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model the stream as events
&lt;/h2&gt;

&lt;p&gt;Do not let the component treat an incoming byte stream as an undocumented collection of callbacks. Normalize it first:&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;AgentEvent&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;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;started&lt;/span&gt;&lt;span class="dl"&gt;"&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="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text_delta&lt;/span&gt;&lt;span class="dl"&gt;"&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;text&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;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tool_started&lt;/span&gt;&lt;span class="dl"&gt;"&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;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="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tool_completed&lt;/span&gt;&lt;span class="dl"&gt;"&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;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="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;completed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="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="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;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;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The renderer can derive a small set of user-visible states:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;idle → connecting → streaming → using_tool → streaming → complete
                                  └──────────────→ failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That state machine is the stable testing surface. “Two chunks arrived instead of three” usually is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inject a controllable stream in tests
&lt;/h2&gt;

&lt;p&gt;For component or end-to-end tests, put the transport behind an interface. In test builds, expose a synthetic event source:&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="kr"&gt;declare&lt;/span&gt; &lt;span class="nb"&gt;global&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Window&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;agentTestStream&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AgentEvent&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&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;Now Cypress controls causality without calling a model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;shows a tool phase and completes the answer&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="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;visit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/assistant?stream=test&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[data-testid=ask]&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;window&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;agentTestStream&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;agentTestStream&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;started&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;run-1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nx"&gt;agentTestStream&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text_delta&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;run-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;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Checking the 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="nx"&gt;agentTestStream&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tool_started&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;run-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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;search_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="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[data-testid=status]&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;contain&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;Searching policy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;window&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;agentTestStream&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;agentTestStream&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tool_completed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;run-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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;search_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="nx"&gt;agentTestStream&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text_delta&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;run-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;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Returns are accepted within 30 days.&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;agentTestStream&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;completed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;run-1&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;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[data-testid=answer]&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="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;contain&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;Checking the 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="nf"&gt;and&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;contain&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;30 days&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[data-testid=status]&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;contain&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;Complete&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 verifies progressive rendering and the final semantic landmarks. It does not care how the provider would divide the sentence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the failures streaming makes easy to miss
&lt;/h2&gt;

&lt;p&gt;The highest-value cases are usually not the happy path.&lt;/p&gt;

&lt;h3&gt;
  
  
  A stale run writes into a newer answer
&lt;/h3&gt;

&lt;p&gt;Start &lt;code&gt;run-1&lt;/code&gt;, then start &lt;code&gt;run-2&lt;/code&gt;. Emit a late delta from &lt;code&gt;run-1&lt;/code&gt; and assert that it is ignored. Every event should carry a run identifier, and the renderer should accept only the active one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cancellation changes the terminal state
&lt;/h3&gt;

&lt;p&gt;After the user clicks Stop, emit another delta. The UI must remain cancelled and must not append the text. Also assert that the transport's abort function was called.&lt;/p&gt;

&lt;h3&gt;
  
  
  A tool fails after text has appeared
&lt;/h3&gt;

&lt;p&gt;Partial prose should not leave the interface looking complete. Emit &lt;code&gt;tool_started&lt;/code&gt;, then &lt;code&gt;failed&lt;/code&gt;, and assert that the error is visible while the incomplete answer is clearly marked.&lt;/p&gt;

&lt;h3&gt;
  
  
  Duplicate events arrive
&lt;/h3&gt;

&lt;p&gt;Reconnects can replay events. Give events stable IDs or sequence numbers and assert that a duplicate delta is not rendered twice.&lt;/p&gt;

&lt;h3&gt;
  
  
  The stream ends without a terminal event
&lt;/h3&gt;

&lt;p&gt;Close the transport after several deltas but before &lt;code&gt;completed&lt;/code&gt; or &lt;code&gt;failed&lt;/code&gt;. The UI should move to an explicit interrupted state instead of presenting partial prose as a finished answer. This catches a class of bugs that an HTTP status assertion cannot see.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use network interception at the right layer
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;cy.intercept()&lt;/code&gt; is useful for controlling the initial request, authentication errors, HTTP status, and response delay. It is less useful when the test needs precise control over a long series of application-level stream events. An injected adapter keeps the browser behavior real while making the event schedule deterministic.&lt;/p&gt;

&lt;p&gt;Keep one integration test against the actual streaming endpoint to verify framing and parsing. Keep most UI state tests provider-free and synthetic. That split makes failures easier to classify: protocol problem or rendering problem.&lt;/p&gt;

&lt;p&gt;At the protocol layer, include fixtures where one logical event is split across network chunks and several events arrive in one chunk. Your parser must reconstruct frames before the UI reducer sees them. The component test can then remain deliberately unaware of TCP, SSE, or fetch buffering.&lt;/p&gt;

&lt;h2&gt;
  
  
  Assert responsibilities, not prose
&lt;/h2&gt;

&lt;p&gt;A robust streaming test usually checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the visible state transition;&lt;/li&gt;
&lt;li&gt;ordering around tool activity;&lt;/li&gt;
&lt;li&gt;cancellation and stale-event handling;&lt;/li&gt;
&lt;li&gt;accessibility announcements;&lt;/li&gt;
&lt;li&gt;the final structured outcome or a few semantic landmarks;&lt;/li&gt;
&lt;li&gt;absence of duplicated content.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It should rarely check every intermediate string.&lt;/p&gt;

&lt;p&gt;For accessibility, assert that status changes are announced once and that rapidly arriving deltas do not flood a live region. The visual answer may update continuously while assistive technology receives milestone events such as “searching,” “approval required,” and “complete.”&lt;/p&gt;

&lt;p&gt;Streaming interfaces are asynchronous state machines wearing a chat UI. Once tests target that state machine, they become both stricter about real bugs and less sensitive to irrelevant token timing.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.cypress.io/api/commands/intercept" rel="noopener noreferrer"&gt;Cypress network interception&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.cypress.io/app/core-concepts/retry-ability" rel="noopener noreferrer"&gt;Cypress retry-ability&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>cypress</category>
      <category>testing</category>
      <category>ai</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Build an Agent UI That Explains Its State with Angular Signals</title>
      <dc:creator>Raju Dandigam</dc:creator>
      <pubDate>Fri, 18 Sep 2026 00:09:49 +0000</pubDate>
      <link>https://dev.to/raju_dandigam/build-an-agent-ui-that-explains-its-state-with-angular-signals-28op</link>
      <guid>https://dev.to/raju_dandigam/build-an-agent-ui-that-explains-its-state-with-angular-signals-28op</guid>
      <description>&lt;p&gt;Most chat interfaces have three states: empty, loading, and finished. Agent workflows have many more.&lt;/p&gt;

&lt;p&gt;An agent may be planning, waiting for a tool, requesting approval, retrying a recoverable error, or suppressing an unsafe action. If the UI represents all of those as one spinner, users cannot tell whether the system is working, blocked, or about to change something.&lt;/p&gt;

&lt;p&gt;The better model is not “messages plus loading.” It is an explicit state machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define states the product can explain
&lt;/h2&gt;

&lt;p&gt;Start with observable runtime states—not the model's hidden reasoning:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;AgentUiState&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;idle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;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;planning&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;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;using_tool&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&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="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;waiting_for_approval&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;proposalId&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;recovering&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;attempt&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="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;blocked&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;reasonCode&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;completed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="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;failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This vocabulary should come from actual runtime events. Do not fabricate a “thinking” narrative that implies access to private chain-of-thought.&lt;/p&gt;

&lt;h2&gt;
  
  
  Store facts; compute presentation
&lt;/h2&gt;

&lt;p&gt;Angular signals work well when the event stream is the source of truth and presentation is derived from it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;computed&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;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@angular/core&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;AgentEvent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;run_started&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tool_started&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;approval_required&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_started&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;run_completed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;run_failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;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;proposalId&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;attempt&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;message&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;events&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AgentEvent&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;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;computed&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AgentUiState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&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;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;events&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="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;event&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;idle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;switch &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="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;run_started&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;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;planning&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tool_started&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;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;using_tool&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;approval_required&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;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;waiting_for_approval&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;proposalId&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;proposalId&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;retry_started&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;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;recovering&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;attempt&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;attempt&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;run_completed&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;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;completed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;outcome&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;outcome&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;run_failed&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;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;failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&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;message&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Angular's &lt;a href="https://angular.dev/guide/signals" rel="noopener noreferrer"&gt;signals guide&lt;/a&gt; recommends &lt;code&gt;computed()&lt;/code&gt; for derived state and warns against using effects to propagate state changes. That distinction matters here. The event list is state; the current label, available actions, and accessibility message are derivations.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;effect()&lt;/code&gt; only for a real side effect such as analytics or persistence—and keep it independent from the state transition itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat approval as a real state
&lt;/h2&gt;

&lt;p&gt;Approval is not a modal layered over “loading.” It pauses one proposal and creates a new user decision.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;@switch (state().kind) {
  @case ('using_tool') {
    &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;aria-live=&lt;/span&gt;&lt;span class="s"&gt;"polite"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Using {{ state().tool }}&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  }
  @case ('waiting_for_approval') {
    &lt;span class="nt"&gt;&amp;lt;app-action-review&lt;/span&gt;
      &lt;span class="na"&gt;[proposalId]=&lt;/span&gt;&lt;span class="s"&gt;"state().proposalId"&lt;/span&gt;
      &lt;span class="na"&gt;(approved)=&lt;/span&gt;&lt;span class="s"&gt;"approve($event)"&lt;/span&gt;
      &lt;span class="na"&gt;(rejected)=&lt;/span&gt;&lt;span class="s"&gt;"reject($event)"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  }
  @case ('recovering') {
    &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;aria-live=&lt;/span&gt;&lt;span class="s"&gt;"polite"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Recovering, attempt {{ state().attempt }}&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  }
  @case ('blocked') {
    &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;role=&lt;/span&gt;&lt;span class="s"&gt;"alert"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Action blocked: {{ state().reasonCode }}&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The review component should show the proposed tool, bounded arguments, evidence freshness, and scope of approval. “Continue?” is not enough for a consequential action.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prevent stale streams from winning
&lt;/h2&gt;

&lt;p&gt;A common UI bug occurs when request A is cancelled, request B starts, and a late event from A overwrites B's state. Give each event a run ID and ignore events for inactive runs:&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;activeRunId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;signal&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="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;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;function&lt;/span&gt; &lt;span class="nf"&gt;acceptEvent&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="kr"&gt;string&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;AgentEvent&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;runId&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nf"&gt;activeRunId&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;current&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cancellation should be a runtime operation as well as a visual one. Removing a spinner does not stop a network request or tool call.&lt;/p&gt;

&lt;p&gt;For reconnectable streams, add an event ID or monotonically increasing sequence number. Ignore duplicates, detect gaps, and request a snapshot when the UI cannot safely reconstruct state. A signal will faithfully render bad ordering if the transport contract never defined ordering.&lt;/p&gt;

&lt;p&gt;Use immutable updates such as &lt;code&gt;events.update(...)&lt;/code&gt;. Angular's readonly signal surface does not prevent deep mutation of an object or array, and mutating a retained value in place can make state changes harder to reason about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the state accessible and actionable
&lt;/h2&gt;

&lt;p&gt;Expose &lt;code&gt;aria-busy="true"&lt;/code&gt; only while work is genuinely progressing. An approval state is not busy: focus should move to the review controls, the proposed effect should be described, and Approve and Reject should remain keyboard accessible. Announce concise state changes through an &lt;code&gt;aria-live&lt;/code&gt; region, but do not stream every token into it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test transitions, not animation timing
&lt;/h2&gt;

&lt;p&gt;Component tests can drive events deterministically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;shows approval after a tool proposal&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;mount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;AgentPanelComponent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;run_started&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;approval_required&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;proposalId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;synthetic-proposal&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;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;button&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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/approve/i&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;be.visible&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/synthetic-proposal/i&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;exist&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;Cypress documents current Angular component-testing support in its &lt;a href="https://docs.cypress.io/app/component-testing/angular/overview" rel="noopener noreferrer"&gt;Angular guide&lt;/a&gt;. Keep model and tool calls stubbed for these state tests; use a smaller number of integration tests for the real event protocol.&lt;/p&gt;

&lt;h2&gt;
  
  
  Explain what the system knows
&lt;/h2&gt;

&lt;p&gt;An agent UI should answer three questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What observable stage is active?&lt;/li&gt;
&lt;li&gt;Does the user need to act?&lt;/li&gt;
&lt;li&gt;What outcome was verified?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Angular signals make those answers easy to derive from a structured event stream. The hard part is defining honest states. Do that first, and the UI stops being a chat box with a spinner—it becomes a trustworthy view of the workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://angular.dev/guide/signals" rel="noopener noreferrer"&gt;Angular Signals&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.cypress.io/app/component-testing/angular/overview" rel="noopener noreferrer"&gt;Cypress Angular component testing&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>angular</category>
      <category>ai</category>
      <category>typescript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Structured Output Is an API: Evolving Gemini Schemas Without Breaking Your Agent</title>
      <dc:creator>Raju Dandigam</dc:creator>
      <pubDate>Thu, 17 Sep 2026 16:58:46 +0000</pubDate>
      <link>https://dev.to/raju_dandigam/structured-output-is-an-api-evolving-gemini-schemas-without-breaking-your-agent-44c6</link>
      <guid>https://dev.to/raju_dandigam/structured-output-is-an-api-evolving-gemini-schemas-without-breaking-your-agent-44c6</guid>
      <description>&lt;p&gt;Structured output solves an important model problem: instead of asking Gemini for “JSON-like” prose, you provide a schema and receive syntactically structured data.&lt;/p&gt;

&lt;p&gt;Then the software-engineering problem begins.&lt;/p&gt;

&lt;p&gt;Once a tool, UI, downstream agent, database, or audit process depends on that JSON, the model output is an API. Renaming a field can break consumers. Adding an enum value can send an older UI into an impossible state. Valid JSON can still describe an invalid business decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Version the contract inside the data
&lt;/h2&gt;

&lt;p&gt;Suppose a travel assistant initially returns:&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;TravelDecisionV1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;schemaVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;book&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;ask&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;wait&lt;/span&gt;&lt;span class="dl"&gt;"&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;p&gt;Later, the product needs suppression and evidence references. Quietly changing the original shape is risky. Introduce a new discriminated version:&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;TravelDecisionV2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;schemaVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;act&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;ask&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;wait&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;suppress&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;reasonCode&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;evidenceIds&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;confidence&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;value&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;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;classifier&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;model&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rule&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="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;TravelDecision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;TravelDecisionV1&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nx"&gt;TravelDecisionV2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The version belongs in the persisted value, not only in the deployment configuration. Stored outputs can outlive the model call that created them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generate against a schema—and validate again
&lt;/h2&gt;

&lt;p&gt;Gemini supports structured output through a provided JSON Schema. Its JavaScript SDK can use a JSON Schema together with Zod validation. The abbreviated example below keeps the model identifier in configuration:&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;GoogleGenAI&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;@google/genai&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="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;z&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;decisionSchema&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;schemaVersion&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;literal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;decision&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;act&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;ask&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;wait&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;suppress&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
  &lt;span class="na"&gt;reasonCode&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;min&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;evidenceIds&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="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;jsonSchema&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;toJSONSchema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;decisionSchema&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;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;GoogleGenAI&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;interactions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GEMINI_MODEL&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Evaluate the synthetic travel-change fixture.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;response_format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;mime_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;jsonSchema&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;const&lt;/span&gt; &lt;span class="nx"&gt;decision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;decisionSchema&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;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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;output_text&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;a href="https://ai.google.dev/gemini-api/docs/structured-output" rel="noopener noreferrer"&gt;Gemini structured-output documentation&lt;/a&gt; explicitly recommends application validation. Gemini implements a subset of JSON Schema, and very large or deeply nested schemas may be rejected.&lt;/p&gt;

&lt;p&gt;For a public contract, make the schema as closed and bounded as the supported Gemini subset allows. Limit array sizes, constrain string formats where meaningful, and reject unexpected properties at the application boundary. A permissive schema followed by a strict consumer only moves the failure downstream.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate four kinds of validity
&lt;/h2&gt;

&lt;p&gt;“The response was valid” can mean several different things:&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;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Syntax&lt;/td&gt;
&lt;td&gt;The response parses as JSON&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shape&lt;/td&gt;
&lt;td&gt;Required properties and enum values match the schema&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Semantics&lt;/td&gt;
&lt;td&gt;Referenced evidence IDs exist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Policy&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;act&lt;/code&gt; is permitted for this user and current state&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Structured generation helps primarily with the first two. Your application still owns the last two.&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;validateSemantics&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TravelDecisionV2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;knownIds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Set&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="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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;decision&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;act&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;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;evidenceIds&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ACT_REQUIRES_EVIDENCE&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;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;evidenceIds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;knownIds&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;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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;UNKNOWN_EVIDENCE_REFERENCE&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;Do not retry every validation failure with “please fix your JSON.” A syntax failure, an unsupported enum, and a policy violation require different handling. A policy violation should normally be blocked, not reprompted until the model finds a permissive answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prefer additive evolution
&lt;/h2&gt;

&lt;p&gt;Changes are usually safer when they are additive:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;add an optional field;&lt;/li&gt;
&lt;li&gt;introduce a new version before removing the old one;&lt;/li&gt;
&lt;li&gt;keep readers tolerant of known historical versions;&lt;/li&gt;
&lt;li&gt;migrate persisted records explicitly;&lt;/li&gt;
&lt;li&gt;reject unknown versions rather than guessing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Enum additions deserve special care. TypeScript consumers often use exhaustive switches:&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;renderDecision&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TravelDecisionV2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;act&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;Executing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ask&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;Needs information&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wait&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;Monitoring&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;suppress&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;No 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;default&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;Unsupported decision&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;That fallback protects the UI, but it should also emit a bounded compatibility error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Contract-test model and schema changes
&lt;/h2&gt;

&lt;p&gt;For each supported model configuration, test:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;a normal V2 result;&lt;/li&gt;
&lt;li&gt;every enum branch;&lt;/li&gt;
&lt;li&gt;missing evidence;&lt;/li&gt;
&lt;li&gt;an old persisted V1 record;&lt;/li&gt;
&lt;li&gt;an unknown future version;&lt;/li&gt;
&lt;li&gt;schema-compliant but policy-invalid data.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Do not assert exact prose in &lt;code&gt;reasonCode&lt;/code&gt; explanations. Assert the stable schema, references, and business constraints.&lt;/p&gt;

&lt;p&gt;Persist a small generation envelope beside the output:&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;GenerationEnvelope&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;schemaVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;schemaHash&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;modelId&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;generatedAt&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;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TravelDecisionV2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;schemaHash&lt;/code&gt; is not a replacement for the human-readable version. It detects drift when two deployments both claim to produce V2 but were built from different schema documents. The model identifier supports diagnosis; it should not become the only migration key.&lt;/p&gt;

&lt;h2&gt;
  
  
  JSON is the start of the contract
&lt;/h2&gt;

&lt;p&gt;Structured output makes agent integration substantially cleaner, but it does not remove API design. Version the value, validate it at runtime, distinguish semantic and policy failures, and preserve migration tests.&lt;/p&gt;

&lt;p&gt;If another component can break when the model's JSON changes, you already have an API—even if there is no HTTP endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://ai.google.dev/gemini-api/docs/structured-output" rel="noopener noreferrer"&gt;Gemini structured outputs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://json-schema.org/" rel="noopener noreferrer"&gt;JSON Schema&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>google</category>
      <category>gemini</category>
      <category>typescript</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Give Your Coding Agent Eyes: Debug Cypress Through a Live Browser Session</title>
      <dc:creator>Raju Dandigam</dc:creator>
      <pubDate>Wed, 16 Sep 2026 17:31:00 +0000</pubDate>
      <link>https://dev.to/raju_dandigam/give-your-coding-agent-eyes-debug-cypress-through-a-live-browser-session-5ffi</link>
      <guid>https://dev.to/raju_dandigam/give-your-coding-agent-eyes-debug-cypress-through-a-live-browser-session-5ffi</guid>
      <description>&lt;p&gt;A coding agent can run &lt;code&gt;cypress run&lt;/code&gt; and read the exit code. That tells it whether a spec passed. It does not show the Command Log, the DOM at the failed command, the accessibility tree, or the browser state a human sees in Cypress open mode.&lt;/p&gt;

&lt;p&gt;This gap matters when the failure is visual or stateful. “Element not found” could mean the selector is wrong, the page never loaded, an overlay covered the control, or the test inspected the wrong retry attempt.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;cypress tap&lt;/code&gt; adds a terminal interface to a live Cypress open-mode session. The interesting part is not that an AI can execute another command. It is that the agent can inspect the same evidence a developer would use before proposing a fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  The loop is run, wait, inspect, then edit
&lt;/h2&gt;

&lt;p&gt;Start Cypress and select a Chromium-based browser:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx cypress open &lt;span class="nt"&gt;--e2e&lt;/span&gt; &lt;span class="nt"&gt;--browser&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;chrome
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In another terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx cypress tap specs &lt;span class="nt"&gt;--json&lt;/span&gt;
npx cypress tap run cypress/e2e/login.cy.ts &lt;span class="nt"&gt;--json&lt;/span&gt;
npx cypress tap status &lt;span class="nt"&gt;--json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;run&lt;/code&gt; command requests a run and returns immediately. It does &lt;strong&gt;not&lt;/strong&gt; mean the spec has started or finished. A reliable agent must poll &lt;code&gt;status&lt;/code&gt;, set its own timeout, and wait for a terminal &lt;code&gt;passed&lt;/code&gt; or &lt;code&gt;failed&lt;/code&gt; state.&lt;/p&gt;

&lt;p&gt;There is a subtle stale-state trap: the previous run's verdict remains readable until the next run starts. The JSON status includes &lt;code&gt;startedAt&lt;/code&gt;; compare it with the run you requested before trusting the verdict.&lt;/p&gt;

&lt;p&gt;That produces a bounded protocol:&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;TapStatus&lt;/span&gt; &lt;span class="o"&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="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;not connected&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;browser not selected&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;spec not selected&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;loading&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;running&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;passed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;startedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="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;waitForVerdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requestedAfter&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;for &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;attempt&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;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;attempt&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;status&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;runJson&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;TapStatus&lt;/span&gt;&lt;span class="o"&gt;&amp;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;npx cypress tap status --json&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;started&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;startedAt&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;startedAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;started&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;requestedAfter&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;status&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;passed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&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;Cypress session did not produce a fresh verdict&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;The timeout is part of the correctness model. A build failure can leave the session in &lt;code&gt;loading&lt;/code&gt;, so “poll until success” can otherwise become an agent loop.&lt;/p&gt;

&lt;p&gt;There is another important contract: &lt;code&gt;cypress tap status&lt;/code&gt; exits successfully for a determinable stage, including a reported test failure. Automation must branch on the JSON &lt;code&gt;status&lt;/code&gt;, not treat shell exit code zero as a passing spec. Prefer &lt;code&gt;--json&lt;/code&gt;, pin Cypress in the lockfile, and let the agent inspect &lt;code&gt;cypress tap --help&lt;/code&gt; so its command assumptions match the attached session.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inspect the failing attempt, not only the latest screen
&lt;/h2&gt;

&lt;p&gt;Once the run fails, get the reporter view:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx cypress tap reporter &lt;span class="nt"&gt;--json&lt;/span&gt;
npx cypress tap reporter &lt;span class="nt"&gt;--test-id&lt;/span&gt; r3 &lt;span class="nt"&gt;--attempt&lt;/span&gt; 1 &lt;span class="nt"&gt;--json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test-level report includes routes, hooks, the Command Log, and failure details. If retries occurred, inspect the failed attempt instead of assuming the final attempt represents the original failure.&lt;/p&gt;

&lt;p&gt;Then narrow the investigation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx cypress tap &lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--test-id&lt;/span&gt; r3 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--command-id&lt;/span&gt; 7 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--attempt&lt;/span&gt; 1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--json&lt;/span&gt;

npx cypress tap pin &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--test-id&lt;/span&gt; r3 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--command-id&lt;/span&gt; 7 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--attempt&lt;/span&gt; 1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--at&lt;/span&gt; after

npx cypress tap aria &lt;span class="nt"&gt;--selector&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt;
npx cypress tap inspect &lt;span class="nt"&gt;--selector&lt;/span&gt; &lt;span class="s2"&gt;"[data-testid=submit]"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;pin&lt;/code&gt; restores a command snapshot into the app-under-test frame. &lt;code&gt;dom&lt;/code&gt;, &lt;code&gt;aria&lt;/code&gt;, and &lt;code&gt;inspect&lt;/code&gt; can then read that historical state. This is far stronger evidence than asking an agent to infer browser state from an error string.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put limits around the agent
&lt;/h2&gt;

&lt;p&gt;Browser access should not become an unbounded “fix until green” loop. I use a small policy:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run one named spec.&lt;/li&gt;
&lt;li&gt;Poll for a fresh verdict with a deadline.&lt;/li&gt;
&lt;li&gt;Inspect only the failing test and relevant command.&lt;/li&gt;
&lt;li&gt;Allow one focused code change.&lt;/li&gt;
&lt;li&gt;Rerun the same spec.&lt;/li&gt;
&lt;li&gt;Stop and request human review if the second run fails differently.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The agent should also report what it observed and why the proposed edit addresses that evidence. A green rerun is necessary, but it does not prove the change preserved the intended assertion.&lt;/p&gt;

&lt;h2&gt;
  
  
  Know the current boundary
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;cypress tap&lt;/code&gt; is beta and requires Cypress 15.21.0 or later. It works with &lt;code&gt;cypress open&lt;/code&gt;, not headless &lt;code&gt;cypress run&lt;/code&gt;, and currently requires a Chromium-based browser. Its commands and output may still change between releases.&lt;/p&gt;

&lt;p&gt;Beta also changes the evidence contract. Store the Cypress version, browser, testing type, session identifier, spec path, attempt, and &lt;code&gt;startedAt&lt;/code&gt; beside any captured report. A JSON field that changes in a later release should fail your adapter clearly instead of being interpreted as a new test result.&lt;/p&gt;

&lt;p&gt;It also exposes page structure to the terminal. Treat DOM text, routes, and console properties as potentially sensitive, especially when a coding agent sends context to a remote model.&lt;/p&gt;

&lt;p&gt;The useful mental model is simple: the agent is not replacing the test runner or the reviewer. It is gaining a structured window into the live evidence between “failed” and “I know why.”&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.cypress.io/app/tooling/cypress-tap" rel="noopener noreferrer"&gt;Cypress tap documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.cypress.io/app/guides/test-retries" rel="noopener noreferrer"&gt;Cypress test retries&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>cypress</category>
      <category>testing</category>
      <category>ai</category>
      <category>debugging</category>
    </item>
    <item>
      <title>Your Agent Eval Set Is Rotting: Build a Failure-Mining Loop for Google ADK</title>
      <dc:creator>Raju Dandigam</dc:creator>
      <pubDate>Tue, 15 Sep 2026 19:18:29 +0000</pubDate>
      <link>https://dev.to/raju_dandigam/your-agent-eval-set-is-rotting-build-a-failure-mining-loop-for-google-adk-1moc</link>
      <guid>https://dev.to/raju_dandigam/your-agent-eval-set-is-rotting-build-a-failure-mining-loop-for-google-adk-1moc</guid>
      <description>&lt;p&gt;An agent evaluation set starts healthy. It contains the obvious intents, a few tool failures, and the happy paths used during development.&lt;/p&gt;

&lt;p&gt;Six months later, production has changed. New tools exist. Users phrase requests differently. A fallback introduced last quarter now handles 30% of traffic. Yet CI still runs the same twelve examples and reports green.&lt;/p&gt;

&lt;p&gt;The problem is not only stale prompts. &lt;strong&gt;The eval set itself is rotting.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A durable evaluation program needs a controlled path from observed failures back into tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;observed run -&amp;gt; candidate -&amp;gt; human review -&amp;gt; sanitization
             -&amp;gt; eval case -&amp;gt; CI -&amp;gt; release comparison
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  First, define a failure candidate
&lt;/h2&gt;

&lt;p&gt;Do not copy complete production conversations into a test folder. Create a bounded intermediate record:&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;FailureCandidate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;candidateId&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;detectedAt&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;intent&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;terminalOutcome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="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="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wrong_outcome&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;firstFailureRole&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;model&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tool&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;handoff&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;policy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;toolSequence&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;reasonCode&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;sourceReference&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// restricted internal pointer&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This record is for triage. It is not automatically an eval case, and it should not contain raw customer content unless your approved process requires it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mine patterns, not anecdotes
&lt;/h2&gt;

&lt;p&gt;The loudest incident is not necessarily the most representative. Select candidates across several dimensions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;intent and workflow;&lt;/li&gt;
&lt;li&gt;tool and failure role;&lt;/li&gt;
&lt;li&gt;successful recovery versus escalation;&lt;/li&gt;
&lt;li&gt;common and rare trajectory shapes;&lt;/li&gt;
&lt;li&gt;model, prompt, policy, and dataset version;&lt;/li&gt;
&lt;li&gt;outcome severity.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A simple trajectory signature helps deduplicate repeated symptoms:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;signature&lt;/span&gt;&lt;span class="p"&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;FailureCandidate&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="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
      &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;intent&lt;/span&gt;&lt;span class="p"&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;terminalOutcome&lt;/span&gt;&lt;span class="p"&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;reasonCode&lt;/span&gt;&lt;span class="p"&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;toolSequence&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;&amp;gt;&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="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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The hash does not prove two failures have the same cause. It creates a grouping candidate for human review. Keep a few examples from a large cluster, but do not allow one outage to dominate the suite.&lt;/p&gt;

&lt;h2&gt;
  
  
  Convert only after review
&lt;/h2&gt;

&lt;p&gt;A useful promotion checklist is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;confirm the failure is real rather than an instrumentation or harness defect;&lt;/li&gt;
&lt;li&gt;identify the smallest reproducible input and environment state;&lt;/li&gt;
&lt;li&gt;remove personal, proprietary, and transient data;&lt;/li&gt;
&lt;li&gt;label the expected tool behavior and business outcome;&lt;/li&gt;
&lt;li&gt;record why the case deserves long-term coverage;&lt;/li&gt;
&lt;li&gt;assign an owner and review date.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The resulting case should be synthetic or safely transformed:&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;"caseId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"refund-policy-stale-cache"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"input"&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 this synthetic order still be refunded?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fixture"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"order-after-policy-window.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expected"&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;"requiredTools"&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;"lookup_order"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"lookup_policy"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"forbiddenTools"&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;"issue_refund"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"outcome"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ask_or_decline"&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;h2&gt;
  
  
  Evaluate more than the final answer
&lt;/h2&gt;

&lt;p&gt;Google's Agents CLI evaluation model distinguishes dimensions such as tool-use quality, multi-turn tool use, trajectory quality, task success, and final-response quality. That separation is important: a fluent answer can follow the wrong path, while an awkward answer may have used tools correctly. See the &lt;a href="https://google.github.io/agents-cli/guide/evaluation/" rel="noopener noreferrer"&gt;official evaluation guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Use deterministic checks for stable facts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a protected tool was absent;&lt;/li&gt;
&lt;li&gt;authorization preceded execution;&lt;/li&gt;
&lt;li&gt;a handoff included required fields;&lt;/li&gt;
&lt;li&gt;the run completed within a controlled call budget.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use semantic graders for questions that genuinely require judgment, such as helpfulness or groundedness. Keep their reports separate so engineers know whether CI found a structural violation or a probabilistic quality regression.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where local traces help
&lt;/h2&gt;

&lt;p&gt;I maintain &lt;a href="https://github.com/rajudandigam/agent-inspect" rel="noopener noreferrer"&gt;AgentInspect&lt;/a&gt;; it is one concrete implementation of this broader pattern. With &lt;code&gt;agent-inspect@6.19.0&lt;/code&gt;, a TypeScript team can turn supported local traces into bounded facts and deterministic trajectory checks. It is not a production sampling or dataset-management platform. Production selection, privacy review, and ADK evaluation remain separate responsibilities.&lt;/p&gt;

&lt;p&gt;That boundary is useful: the same failure can move through distinct trust zones instead of silently becoming training or test data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add retirement, not only promotion
&lt;/h2&gt;

&lt;p&gt;Eval suites also need deletion rules. Retire or revise a case when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;its feature no longer exists;&lt;/li&gt;
&lt;li&gt;its fixture depends on an obsolete schema;&lt;/li&gt;
&lt;li&gt;several cases test the same invariant;&lt;/li&gt;
&lt;li&gt;the original expected behavior is no longer correct.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Track suite composition by intent and failure role. A growing case count is not evidence of improving coverage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measure the health of the suite itself
&lt;/h2&gt;

&lt;p&gt;Give each case provenance and a review clock:&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;EvalCaseMetadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;caseId&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;promotedFrom&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;owner&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;addedAt&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;reviewAfter&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;intent&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;firstFailureRole&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;model&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tool&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;handoff&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;policy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="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 small dashboard can then expose coverage by intent, age, failure role, and last meaningful failure. Flag cases that have not been reviewed after a tool-schema or policy revision. The point is not to delete old cases automatically; it is to make stale expectations visible before they become release authority.&lt;/p&gt;

&lt;p&gt;Watch the promotion funnel as well: candidates detected, candidates reviewed, cases accepted, duplicates rejected, and cases retired. A sudden drop between detection and review often signals an ownership problem rather than an agent-quality improvement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Close the loop deliberately
&lt;/h2&gt;

&lt;p&gt;The goal is not to replay production. It is to convert reviewed failures into small, safe, owned regression cases. Start with the last ten meaningful incidents. Group them, select representatives, sanitize them, and label both trajectory and outcome expectations.&lt;/p&gt;

&lt;p&gt;An eval set stays useful when production teaches it—through a controlled review boundary, not an automatic data pipe.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://google.github.io/agents-cli/guide/evaluation/" rel="noopener noreferrer"&gt;Google Agents CLI evaluation guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/rajudandigam/agent-inspect/tree/agent-inspect%406.19.0" rel="noopener noreferrer"&gt;AgentInspect 6.19.0&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>google</category>
      <category>ai</category>
      <category>testing</category>
      <category>devops</category>
    </item>
    <item>
      <title>Google ADK Callbacks Are a Policy Plane, Not Just Hooks</title>
      <dc:creator>Raju Dandigam</dc:creator>
      <pubDate>Mon, 14 Sep 2026 17:43:33 +0000</pubDate>
      <link>https://dev.to/raju_dandigam/google-adk-callbacks-are-a-policy-plane-not-just-hooks-551j</link>
      <guid>https://dev.to/raju_dandigam/google-adk-callbacks-are-a-policy-plane-not-just-hooks-551j</guid>
      <description>&lt;p&gt;An agent proposes &lt;code&gt;refund_order&lt;/code&gt;. The tool exists, its arguments are valid JSON, and the model sounds confident. None of that answers the question that matters: &lt;strong&gt;is this action allowed now?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Google's Agent Development Kit gives us interception points before and after agents, models, and tools. The tempting implementation is to add a log statement to each callback. The more useful design is to treat those callbacks as a small policy plane.&lt;/p&gt;

&lt;p&gt;That does not mean placing every business rule in a callback. It means using the boundary to make four decisions explicit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;may this operation proceed?&lt;/li&gt;
&lt;li&gt;should its inputs be normalized?&lt;/li&gt;
&lt;li&gt;which budget or authorization applies?&lt;/li&gt;
&lt;li&gt;what evidence should be recorded?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A callback can change control flow
&lt;/h2&gt;

&lt;p&gt;ADK callbacks are not passive listeners. A &lt;code&gt;beforeModelCallback&lt;/code&gt; can return a response and skip the model call. A &lt;code&gt;beforeToolCallback&lt;/code&gt; can return a tool result and skip the tool. After-callbacks can replace results. That makes callback return values part of application behavior, not merely observability. &lt;a href="https://adk.dev/callbacks/" rel="noopener noreferrer"&gt;The ADK callback documentation&lt;/a&gt; explains these interception semantics for TypeScript and the other supported SDKs.&lt;/p&gt;

&lt;p&gt;Callback order is part of that behavior. ADK can run callback lists and plugin callbacks in sequence, with earlier callbacks able to modify inputs seen by later ones or short-circuit the remaining chain. Treat the registered order as reviewed configuration: authentication and tenant resolution should not accidentally run after a callback that can approve or execute an operation.&lt;/p&gt;

&lt;p&gt;Start with a small decision vocabulary:&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;PolicyDecision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;allow&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;block&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;reasonCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;policyVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;authorizeTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;toolName&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="nx"&gt;args&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="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;PolicyDecision&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;toolName&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refund_order&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;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;approved&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="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;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;block&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;reasonCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;APPROVAL_REQUIRED&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;policyVersion&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-policy-3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;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;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;reasonCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POLICY_SATISFIED&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;policyVersion&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-policy-3&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;This decision is deterministic and reviewable. It does not ask the model whether its own proposed action is safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the callback thin
&lt;/h2&gt;

&lt;p&gt;The callback should delegate to policy code rather than growing into an untestable collection of &lt;code&gt;if&lt;/code&gt; statements. The following is deliberately schematic because exact tool callback types can change between ADK releases:&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;beforeTool&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;args&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nl"&gt;args&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="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;authorizeTool&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="nx"&gt;args&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;policyEvidence&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&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="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;reasonCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reasonCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;policyVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;policyVersion&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outcome&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;block&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="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;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 execution blocked by policy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;reasonCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reasonCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// continue with normal tool execution&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Register that function through the agent's &lt;code&gt;beforeToolCallback&lt;/code&gt;. Use &lt;code&gt;beforeModelCallback&lt;/code&gt; for model-input limits or serving an approved cache entry, and &lt;code&gt;afterToolCallback&lt;/code&gt; for result normalization and outcome evidence.&lt;/p&gt;

&lt;p&gt;The boundary becomes easier to review when responsibilities stay distinct:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Boundary&lt;/th&gt;
&lt;th&gt;Suitable responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Before model&lt;/td&gt;
&lt;td&gt;Input limits, budget checks, approved cache lookup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Before tool&lt;/td&gt;
&lt;td&gt;Authorization, argument validation, freshness checks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;After tool&lt;/td&gt;
&lt;td&gt;Result normalization, outcome classification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;After agent&lt;/td&gt;
&lt;td&gt;Final evidence and response-level validation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Test absence, not only the error message
&lt;/h2&gt;

&lt;p&gt;A guardrail test is incomplete if it checks only the returned text. The callback might return “blocked” after the real tool already ran.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;does not execute a refund without approval&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;refund&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;vi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;authorizeTool&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_order&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;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;synthetic-123&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;approved&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outcome&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;await&lt;/span&gt; &lt;span class="nf"&gt;refund&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reasonCode&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;APPROVAL_REQUIRED&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;refund&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;not&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toHaveBeenCalled&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;At integration level, preserve evidence that the policy callback ran and the protected tool did not. That is stronger than matching a model-generated refusal sentence.&lt;/p&gt;

&lt;p&gt;Also test the installed callback chain, not only the pure policy function. A unit test can prove &lt;code&gt;authorizeTool()&lt;/code&gt; works while one runner quietly omits the plugin. A useful integration assertion records the callback name, policy version, decision, proposal ID, and whether execution began. For a blocked proposal, &lt;code&gt;tool.execution.started&lt;/code&gt; must be absent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Callbacks are not the entire security architecture
&lt;/h2&gt;

&lt;p&gt;A callback runs inside the application process. It can contain a bug, be omitted from another agent, or be registered in the wrong order. High-risk authorization should also be enforced at the tool or service boundary. The application still needs least-privilege credentials, idempotency, audit records, and tests.&lt;/p&gt;

&lt;p&gt;Google explicitly recommends &lt;strong&gt;ADK Plugins&lt;/strong&gt; for modular security guardrails instead of scattering individual callbacks across agents. That is the right graduation path when the same policy must apply consistently to several agents.&lt;/p&gt;

&lt;p&gt;Plugins still run inside your application. They improve reuse and consistency; they do not replace authorization at the resource server. Think of the callback or plugin as the orchestration decision point and the downstream service as the final enforcement point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make policy visible
&lt;/h2&gt;

&lt;p&gt;Callbacks are most valuable when they expose a control point that already exists conceptually. Give each decision a stable reason code and policy version. Record bounded facts, not raw prompts or hidden reasoning. Test both the allowed path and the absence of a forbidden side effect.&lt;/p&gt;

&lt;p&gt;Used that way, an ADK callback is more than a hook. It is where an agent proposal becomes an application decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://adk.dev/callbacks/" rel="noopener noreferrer"&gt;Google ADK callbacks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://adk.dev/safety/" rel="noopener noreferrer"&gt;Google ADK safety and security&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>google</category>
      <category>ai</category>
      <category>typescript</category>
      <category>security</category>
    </item>
    <item>
      <title>Prompts Are Code. Genkit Makes the Runtime Reviewable.</title>
      <dc:creator>Raju Dandigam</dc:creator>
      <pubDate>Sun, 13 Sep 2026 16:40:15 +0000</pubDate>
      <link>https://dev.to/raju_dandigam/prompts-are-code-genkit-makes-the-runtime-reviewable-gh3</link>
      <guid>https://dev.to/raju_dandigam/prompts-are-code-genkit-makes-the-runtime-reviewable-gh3</guid>
      <description>&lt;p&gt;A prompt can look perfect in a model playground and still fail as a product.&lt;/p&gt;

&lt;p&gt;The production input arrives in a different shape. Authentication data leaks into the prompt. The model returns prose where the UI expects JSON. A three-line wording change improves two examples and breaks eight others. Nobody can explain which version generated yesterday’s result.&lt;/p&gt;

&lt;p&gt;The lesson is not that prompts are unimportant.&lt;/p&gt;

&lt;p&gt;It is that a prompt is one artifact inside a larger runtime.&lt;/p&gt;

&lt;p&gt;Genkit is useful for TypeScript teams because it connects prompts, flows, schemas, tools, context, traces, and evaluations in one application model. The result is not automatically reliable—but the important boundaries become reviewable.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Series note:&lt;/strong&gt; This is Part 8 of &lt;em&gt;Reliable Google AI Agents in TypeScript&lt;/em&gt;. The examples were checked against Genkit 1.42.0 and &lt;code&gt;@genkit-ai/google-genai&lt;/code&gt; 1.42.0 in September 2026.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Put the prompt in a versioned artifact
&lt;/h2&gt;

&lt;p&gt;Genkit’s Dotprompt format stores template content, model configuration, schemas, and tool-loop settings in &lt;code&gt;.prompt&lt;/code&gt; files.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight handlebars"&gt;&lt;code&gt;---
model: googleai/gemini-flash-latest
config:
  temperature: 0.3
input:
  schema:
    hotelName: string
    oldPrice: number
    newPrice: number
output:
  schema:
    title: "string"
    body: string
---
Write a concise price-drop alert for &lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;hotelName&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;.
The price changed from &lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;oldPrice&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt; to &lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;newPrice&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;.
Do not invent amenities, policies, or availability.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keeping this in &lt;code&gt;prompts/price-alert.prompt&lt;/code&gt; produces several practical benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;prompt changes appear in Git review;&lt;/li&gt;
&lt;li&gt;model and generation settings travel with the template;&lt;/li&gt;
&lt;li&gt;input and output expectations are visible;&lt;/li&gt;
&lt;li&gt;the Developer UI can iterate on the prompt without burying it in service code;&lt;/li&gt;
&lt;li&gt;variants can be evaluated against the same examples.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But a &lt;code&gt;.prompt&lt;/code&gt; file should not own authorization, retries, billing rules, or irreversible side effects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap the prompt in a typed flow
&lt;/h2&gt;

&lt;p&gt;A Genkit flow creates the application boundary around generation. It has a stable name, validated input and output, trace identity, and a deployment surface.&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;genkit&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;genkit&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;googleAI&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;@genkit-ai/google-genai&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;ai&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;genkit&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;googleAI&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;AlertInput&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;hotelName&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;min&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;oldPrice&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;positive&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;newPrice&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;positive&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;AlertCopy&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;title&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;min&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;body&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;min&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="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;AlertOutput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;AlertCopy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;shouldSend&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;const&lt;/span&gt; &lt;span class="nx"&gt;priceAlertPrompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;price-alert&lt;/span&gt;&lt;span class="dl"&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;priceAlertFlow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;defineFlow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;priceAlertFlow&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AlertInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;outputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AlertOutput&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="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="o"&gt;=&amp;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;newPrice&lt;/span&gt; &lt;span class="o"&gt;&amp;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;oldPrice&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;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;shouldSend&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="kd"&gt;const&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="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;priceAlertPrompt&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;copy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;AlertCopy&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;output&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="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;shouldSend&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;The price rule runs before the model. Zod validates the generated structure after it. Gemini is responsible for bounded language generation—not for redefining whether a price actually dropped.&lt;/p&gt;

&lt;p&gt;That separation is the difference between a prompt demo and an application workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep execution context out of model input
&lt;/h2&gt;

&lt;p&gt;AI applications often have data the code needs but the model does not.&lt;/p&gt;

&lt;p&gt;An auth token may be needed to call a user-scoped service. A tenant ID may be required for a database query. Neither should be interpolated into the prompt merely because a tool needs it.&lt;/p&gt;

&lt;p&gt;Genkit’s context object provides a side channel that propagates through flows, prompts, and tools.&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;TripSummary&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;tripId&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="na"&gt;destination&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="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;searchUserTrips&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;defineTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;searchUserTrips&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Search trips owned by the signed-in user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;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;query&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="p"&gt;}),&lt;/span&gt;
    &lt;span class="na"&gt;outputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;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;TripSummary&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="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="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="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;userId&lt;/span&gt; &lt;span class="o"&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;auth&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;uid&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;userId&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;Authentication required&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;tripStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;userId&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="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 model can decide to call &lt;code&gt;searchUserTrips&lt;/code&gt;. The tool derives ownership from trusted execution context rather than a model-generated user ID.&lt;/p&gt;

&lt;p&gt;That is a small design decision with a large security effect.&lt;/p&gt;

&lt;p&gt;Context is still not automatically trusted. The HTTP or function binding that invokes the flow must authenticate the request and populate &lt;code&gt;context.auth&lt;/code&gt;. Genkit propagates the object; your boundary establishes its integrity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat model output as untrusted data
&lt;/h2&gt;

&lt;p&gt;An output schema improves the boundary, but structured output is not business authorization.&lt;/p&gt;

&lt;p&gt;For example, the model could generate a valid alert body that mentions an unavailable amenity. Or it could return a valid title for a user who opted out of notifications.&lt;/p&gt;

&lt;p&gt;Use different controls for different failure classes:&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;Appropriate control&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Wrong field type&lt;/td&gt;
&lt;td&gt;Zod/output schema&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User opted out&lt;/td&gt;
&lt;td&gt;Deterministic policy before generation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unsupported factual claim&lt;/td&gt;
&lt;td&gt;Grounding plus evidence validation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Poor tone&lt;/td&gt;
&lt;td&gt;Evaluator or human review&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duplicate delivery&lt;/td&gt;
&lt;td&gt;Idempotent side-effect boundary&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A schema answers “Can downstream code safely parse this?” It does not answer “Should the business act on it?”&lt;/p&gt;

&lt;h2&gt;
  
  
  Observe the whole flow
&lt;/h2&gt;

&lt;p&gt;Genkit traces flows, model calls, tools, and intermediate operations in the Developer UI. Its telemetry is based on OpenTelemetry, while production export depends on the telemetry and observability integration you configure.&lt;/p&gt;

&lt;p&gt;That creates two complementary views:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The native Genkit trace shows the actual flow, prompt/model operation, tools, latency, and schema failures.&lt;/li&gt;
&lt;li&gt;A framework-neutral evidence layer can express a stable cross-framework contract for pull-request review or CI.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;priceAlertFlow
├─ validate_input
├─ business_rule          price_drop=true
├─ prompt.price-alert     version=git:8f3c2a1
├─ validate_output        schema=AlertCopy
└─ return                 shouldSend=true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/rajudandigam/agent-inspect" rel="noopener noreferrer"&gt;AgentInspect&lt;/a&gt; can complement the native trace when the application maps these important operations into a run and wants deterministic required/forbidden checks or a redacted review artifact. It should not duplicate every span, and this article does not imply a first-class Genkit adapter already exists.&lt;/p&gt;

&lt;p&gt;The stable contract is more important than owning every telemetry signal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evaluate changes against a dataset
&lt;/h2&gt;

&lt;p&gt;Prompt review by intuition is not enough.&lt;/p&gt;

&lt;p&gt;Genkit supports evaluation datasets and evaluators through its tooling. A useful price-alert dataset should include:&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"input"&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;"hotelName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Example Hotel"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"oldPrice"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;240&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"newPrice"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;190&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;"reference"&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;"shouldSend"&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="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;"input"&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;"hotelName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Example Hotel"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"oldPrice"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;190&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"newPrice"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;240&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;"reference"&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;"shouldSend"&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="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;The real suite should also include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;equal prices;&lt;/li&gt;
&lt;li&gt;missing or malformed input;&lt;/li&gt;
&lt;li&gt;unusually large values;&lt;/li&gt;
&lt;li&gt;prompt injection inside &lt;code&gt;hotelName&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;unsupported amenity claims;&lt;/li&gt;
&lt;li&gt;an unauthenticated tool request;&lt;/li&gt;
&lt;li&gt;a consent-policy block;&lt;/li&gt;
&lt;li&gt;a duplicate delivery attempt.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some checks should remain deterministic: schema validity, correct &lt;code&gt;shouldSend&lt;/code&gt;, no raw auth data, and no notification for a price increase. Other qualities—clarity, tone, or faithfulness to retrieved evidence—may use an evaluator or human review.&lt;/p&gt;

&lt;p&gt;A useful evaluation suite does not require every sentence to match. It asks whether the workflow still satisfies its product contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploy the flow, not a loose prompt
&lt;/h2&gt;

&lt;p&gt;Genkit flows can be served from a Node.js application with the supported Express integration and deployed to a platform such as Cloud Run.&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;startFlowServer&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;@genkit-ai/express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;startFlowServer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;flows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;priceAlertFlow&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 deployed unit is the workflow with its schema, context handling, tools, and telemetry—not a text template floating independently from the code that constrains it.&lt;/p&gt;

&lt;p&gt;That matters operationally:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a rollback can restore compatible prompt and code together;&lt;/li&gt;
&lt;li&gt;a trace identifies the named flow;&lt;/li&gt;
&lt;li&gt;an evaluation compares versions using the same dataset;&lt;/li&gt;
&lt;li&gt;a security review can see where trusted context enters;&lt;/li&gt;
&lt;li&gt;a release gate can distinguish structural failures from subjective quality changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The runtime is the product
&lt;/h2&gt;

&lt;p&gt;“Prompts are code” is a useful starting point because it moves prompt work into version control.&lt;/p&gt;

&lt;p&gt;Production engineering has to go further.&lt;/p&gt;

&lt;p&gt;The prompt is the instruction artifact. The flow is the application contract. Context carries trusted execution data. Tools provide bounded capabilities. Schemas protect downstream code. Traces explain the run. Evaluations protect behavior as the system changes.&lt;/p&gt;

&lt;p&gt;Genkit brings those pieces into one TypeScript development model.&lt;/p&gt;

&lt;p&gt;That is the level at which a Gemini application becomes maintainable—not when its prompt sounds impressive, but when its complete workflow can be reviewed, tested, observed, and safely changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://genkit.dev/docs/js/dotprompt/" rel="noopener noreferrer"&gt;Genkit Dotprompt&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://genkit.dev/docs/js/flows/" rel="noopener noreferrer"&gt;Genkit flows&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://genkit.dev/docs/js/context/" rel="noopener noreferrer"&gt;Passing trusted data through Genkit context&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://genkit.dev/docs/js/observability/telemetry-collection/" rel="noopener noreferrer"&gt;Genkit telemetry collection&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://genkit.dev/docs/evaluation" rel="noopener noreferrer"&gt;Genkit evaluation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Earlier in the series:&lt;/strong&gt; &lt;a href="https://dev.to/raju_dandigam/gemini-function-calling-is-not-an-agent-runtime-4ijl"&gt;Gemini Function Calling Is Not an Agent Runtime&lt;/a&gt; · &lt;a href="https://dev.to/raju_dandigam/testing-google-adk-typescript-agents-without-chasing-sentences-3d25"&gt;Testing Google ADK TypeScript Agents Without Chasing Sentences&lt;/a&gt; · &lt;a href="https://dev.to/raju_dandigam/from-local-traces-to-production-observability-for-google-ai-agents-1l50"&gt;From Local Traces to Production Observability for Google AI Agents&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>typescript</category>
      <category>gemini</category>
      <category>googlecloud</category>
    </item>
    <item>
      <title>A Screenshot Is Not an Agent Failure Artifact</title>
      <dc:creator>Raju Dandigam</dc:creator>
      <pubDate>Sat, 12 Sep 2026 19:13:05 +0000</pubDate>
      <link>https://dev.to/raju_dandigam/a-screenshot-is-not-an-agent-failure-artifact-4phi</link>
      <guid>https://dev.to/raju_dandigam/a-screenshot-is-not-an-agent-failure-artifact-4phi</guid>
      <description>&lt;p&gt;A screenshot of an execution tree looks useful in a pull request.&lt;/p&gt;

&lt;p&gt;It is also missing almost everything a skeptical reviewer should ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which trace produced it?&lt;/li&gt;
&lt;li&gt;Was the trace changed after capture?&lt;/li&gt;
&lt;li&gt;Which redaction policy was applied?&lt;/li&gt;
&lt;li&gt;Did the crop remove a warning?&lt;/li&gt;
&lt;li&gt;Can I inspect the failing step without asking the author to reproduce it?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Raw JSONL answers some of those questions and creates another problem: it may contain identifiers, attributes, or bounded previews that should not leave the developer's machine.&lt;/p&gt;

&lt;p&gt;The practical handoff unit is neither screenshot nor raw trace. It is a &lt;strong&gt;derived, reviewable evidence artifact&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A picture is a view, not the evidence chain
&lt;/h2&gt;

&lt;p&gt;A screenshot preserves pixels. It does not preserve provenance.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Reviewer question&lt;/th&gt;
&lt;th&gt;Screenshot&lt;/th&gt;
&lt;th&gt;Evidence package&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;What path ran?&lt;/td&gt;
&lt;td&gt;Sometimes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Which source produced it?&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Source run IDs and hashes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What was redacted?&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Redaction report and policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Did a file change later?&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Packaged-file hashes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Can I inspect offline?&lt;/td&gt;
&lt;td&gt;Only the crop&lt;/td&gt;
&lt;td&gt;HTML, JSON, summary, trace copy&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is the model I want instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;private source trace
        |
        | derive + redact
        v
reviewable package
  ├─ evidence.html
  ├─ summary.md
  ├─ redacted trace
  ├─ check results
  ├─ redaction report
  └─ evidence.json ----&amp;gt; hashes + provenance
                              |
                              v
                       offline verification
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The source remains private. The reviewer gets a bounded object with enough context to question the result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build a bundle from one run or a session
&lt;/h2&gt;

&lt;p&gt;AgentInspect's Evidence v2 bundle is one implementation of this pattern. These commands were verified against &lt;a href="https://www.npmjs.com/package/agent-inspect/v/6.17.6" rel="noopener noreferrer"&gt;&lt;code&gt;agent-inspect@6.17.6&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Create a package for one local run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx agent-inspect bundle &amp;lt;run-id&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dir&lt;/span&gt; .agent-inspect &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--profile&lt;/span&gt; share &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--out&lt;/span&gt; ./agent-evidence
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or package a multi-run session with stricter redaction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx agent-inspect bundle &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--session&lt;/span&gt; sess-support-042 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dir&lt;/span&gt; .agent-inspect &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--profile&lt;/span&gt; strict &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--out&lt;/span&gt; ./agent-evidence
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The source traces are read-only. The output contains derived copies and review surfaces such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;agent-evidence/
├─ evidence.html
├─ evidence.json
├─ trace.html
├─ trace.jsonl
├─ summary.md
├─ metadata.json
├─ check-results.json
├─ redaction-report.json
├─ eval-results.json
├─ performance-summary.json
└─ assets/runs/...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;evidence.html&lt;/code&gt; is the convenient entry point. &lt;code&gt;evidence.json&lt;/code&gt; is what makes the folder inspectable as a package.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the manifest records
&lt;/h2&gt;

&lt;p&gt;The Evidence v2 manifest separates four facts:&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;"evidenceFormatVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.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;"generator"&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;"agent-inspect"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"6.17.6"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"runIds"&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;"run_example"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"traceSchemaVersions"&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;"1.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;"sourceHashes"&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;"runId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"run_example"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"algorithm"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sha256"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"hash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;span class="nl"&gt;"policy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"redactionProfile"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"share"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"verificationPolicy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"share"&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;"files"&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;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"trace.jsonl"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"sha256"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"redacted-trace"&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;The important separation is between &lt;code&gt;sourceHashes&lt;/code&gt; and &lt;code&gt;files[].sha256&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;source hashes identify the pre-redaction bytes used to derive the artifact;&lt;/li&gt;
&lt;li&gt;file hashes protect the exact bytes inside the finalized package.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The source hash does not reveal the redacted content. It also does not preserve the original for you; retention remains a separate policy decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify before review
&lt;/h2&gt;

&lt;p&gt;Run the offline verifier:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx agent-inspect bundle verify ./agent-evidence
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;the Evidence manifest shape;&lt;/li&gt;
&lt;li&gt;required listed files;&lt;/li&gt;
&lt;li&gt;SHA-256 hashes for packaged files;&lt;/li&gt;
&lt;li&gt;unexpected files, which fail by default;&lt;/li&gt;
&lt;li&gt;the recorded assessment; and&lt;/li&gt;
&lt;li&gt;generator and source provenance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If someone edits &lt;code&gt;summary.md&lt;/code&gt;, replaces the redacted trace, or drops an unlisted file into the finalized directory, verification should stop passing.&lt;/p&gt;

&lt;p&gt;That makes a precise claim: the finalized package is internally consistent with its manifest.&lt;/p&gt;

&lt;p&gt;Imagine a reviewer receives two folders named &lt;code&gt;agent-evidence&lt;/code&gt;. One contains the original bundle. In the other, someone corrected a sentence in &lt;code&gt;summary.md&lt;/code&gt; after generation because the explanation was unclear. The edit may be innocent, but the artifact now describes bytes different from the ones recorded in the manifest.&lt;/p&gt;

&lt;p&gt;The correct workflow is to fix the source or approved summary input, regenerate the bundle, and verify again. Hash failure does not tell you whether a change was malicious. It tells you that the finalized review object is no longer the same object.&lt;/p&gt;

&lt;h2&gt;
  
  
  Integrity is not authenticity
&lt;/h2&gt;

&lt;p&gt;This boundary is easy to overstate, so it deserves blunt language.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;bundle verify&lt;/code&gt; is an integrity check. Evidence v2 is &lt;strong&gt;not cryptographically signed by a trusted identity&lt;/strong&gt;. Anyone who can replace the entire folder could create a different valid bundle.&lt;/p&gt;

&lt;p&gt;It is also not:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a live hash-chained write-ahead log;&lt;/li&gt;
&lt;li&gt;proof that every event was durable before a side effect;&lt;/li&gt;
&lt;li&gt;proof of replayability or exactly-once execution;&lt;/li&gt;
&lt;li&gt;nonrepudiation; or&lt;/li&gt;
&lt;li&gt;a compliance or audit certification.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If authorship or trusted delivery matters, add signing, access control, and trusted storage in your approved pipeline. Do not rename a local hash check into a stronger security property.&lt;/p&gt;

&lt;h2&gt;
  
  
  “Share-checked” still means “review it”
&lt;/h2&gt;

&lt;p&gt;The default &lt;code&gt;share&lt;/code&gt; profile is intended for pull requests and internal support. &lt;code&gt;strict&lt;/code&gt; is a better starting point for public or external sharing.&lt;/p&gt;

&lt;p&gt;The bundle command assesses the &lt;strong&gt;redacted artifact&lt;/strong&gt; and refuses &lt;code&gt;UNSAFE&lt;/code&gt; or &lt;code&gt;UNKNOWN&lt;/code&gt; output unless &lt;code&gt;--allow-unsafe&lt;/code&gt; is explicitly supplied. That is a useful fail-closed guard. It is not a guarantee that arbitrary free text contains no sensitive information.&lt;/p&gt;

&lt;p&gt;Before attaching a package:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;open &lt;code&gt;evidence.html&lt;/code&gt; and &lt;code&gt;summary.md&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;review &lt;code&gt;redaction-report.json&lt;/code&gt; and &lt;code&gt;check-results.json&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;run &lt;code&gt;bundle verify&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;confirm the destination is approved; and&lt;/li&gt;
&lt;li&gt;apply your organization's data-retention and sharing policy.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Capture less in the first place. Redaction should not be the only protection between a raw customer payload and a public issue.&lt;/p&gt;

&lt;p&gt;For pull requests, I would also record the verified bundle path or CI artifact name in a small behavior-evidence section. That gives reviewers one canonical object instead of a screenshot in Slack, a trace in local storage, and a summary pasted into the PR with no durable relationship among them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Better evidence changes the review conversation
&lt;/h2&gt;

&lt;p&gt;A good artifact does not decide whether an agent change is acceptable. It lets everyone inspect the same bounded object:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the first explicit failure;&lt;/li&gt;
&lt;li&gt;the observed outcome;&lt;/li&gt;
&lt;li&gt;the contract finding;&lt;/li&gt;
&lt;li&gt;the sanitized execution path;&lt;/li&gt;
&lt;li&gt;the applied policy; and&lt;/li&gt;
&lt;li&gt;the package's integrity status.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That moves the discussion from “trust my screenshot” to “here is the evidence, here is what was removed, and here is what the verifier can—and cannot—prove.”&lt;/p&gt;

&lt;p&gt;The pinned &lt;a href="https://github.com/rajudandigam/agent-inspect/blob/e09d63b3ca36afb1c5caf3ece63f710b64c40b69/docs/BUNDLES.md" rel="noopener noreferrer"&gt;bundle documentation&lt;/a&gt; and &lt;a href="https://github.com/rajudandigam/agent-inspect/blob/e09d63b3ca36afb1c5caf3ece63f710b64c40b69/docs/EVIDENCE-FORMAT.md" rel="noopener noreferrer"&gt;Evidence format&lt;/a&gt; document the current layout and trust boundaries.&lt;/p&gt;

&lt;p&gt;What would your reviewer need before they could safely inspect—and challenge—an agent failure artifact?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devops</category>
      <category>security</category>
      <category>opensource</category>
    </item>
    <item>
      <title>One Passing Agent Run Is Not a Release Signal</title>
      <dc:creator>Raju Dandigam</dc:creator>
      <pubDate>Fri, 11 Sep 2026 18:58:48 +0000</pubDate>
      <link>https://dev.to/raju_dandigam/one-passing-agent-run-is-not-a-release-signal-ao5</link>
      <guid>https://dev.to/raju_dandigam/one-passing-agent-run-is-not-a-release-signal-ao5</guid>
      <description>&lt;p&gt;I can make almost any agent change look good with one carefully chosen prompt.&lt;/p&gt;

&lt;p&gt;Run it once. Watch the right tool fire. Read a polished answer. Record the demo.&lt;/p&gt;

&lt;p&gt;Ship it?&lt;/p&gt;

&lt;p&gt;Not yet.&lt;/p&gt;

&lt;p&gt;A single trace is excellent evidence for one execution: what ran, what failed, how long it took, and whether declared invariants held. A release decision asks a different question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Does this change behave acceptably across the representative cases we care about?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That shift—from &lt;strong&gt;run&lt;/strong&gt; to &lt;strong&gt;set&lt;/strong&gt;—is where agent testing starts to resemble engineering instead of demonstration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give each evidence layer one job
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://www.npmjs.com/package/agent-inspect/v/6.17.6" rel="noopener noreferrer"&gt;&lt;code&gt;agent-inspect@6.17.6&lt;/code&gt;&lt;/a&gt;, I use three local, deterministic 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;Question&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Suite&lt;/td&gt;
&lt;td&gt;Did each named case satisfy its expected checks?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cohort&lt;/td&gt;
&lt;td&gt;What moved between baseline and candidate groups?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gate&lt;/td&gt;
&lt;td&gt;Should CI accept the recorded evidence?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;representative traces
        |
        v
      suite --------&amp;gt; named case results
        |
        v
baseline vs candidate -&amp;gt; cohort deltas
        |
        v
       gate --------&amp;gt; CI exit code + evidence
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These commands read persisted traces. They do not rerun the agent, invoke a model, or decide whether prose is semantically good.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with named cases, not random traffic
&lt;/h2&gt;

&lt;p&gt;A compact suite makes the review set explicit:&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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"refund-release"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"traces"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./.agent-inspect"&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="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;"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;"eligible-order"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"runId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"refund-eligible"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"requireTools"&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;"lookup_order"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"refund_order"&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;"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;"unknown-order"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"runId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"refund-unknown"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"forbidTools"&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;"refund_order"&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;"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;"approval-required"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"runId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"refund-approval"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"requireTools"&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;"request_approval"&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;Validate the configuration and run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx agent-inspect suite validate &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--config&lt;/span&gt; agent-inspect.suite.json

npx agent-inspect suite run &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--config&lt;/span&gt; agent-inspect.suite.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--markdown&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The suite distinguishes a violated expectation from missing evidence. If &lt;code&gt;refund-unknown&lt;/code&gt; does not exist, the case is skipped with a diagnostic; it is not silently treated as a pass. An all-skipped suite cannot become a green release signal.&lt;/p&gt;

&lt;p&gt;That distinction sounds small until CI says “passed” for a case that never ran.&lt;/p&gt;

&lt;h2&gt;
  
  
  The set is part of the test
&lt;/h2&gt;

&lt;p&gt;The most important suite question is not “Did it pass?” It is “Why are these the representative cases?”&lt;/p&gt;

&lt;p&gt;For a refund agent, I would begin with:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;an ordinary eligible order;&lt;/li&gt;
&lt;li&gt;an unknown order that must not trigger a refund;&lt;/li&gt;
&lt;li&gt;an amount requiring approval;&lt;/li&gt;
&lt;li&gt;a missing-data path;&lt;/li&gt;
&lt;li&gt;a retryable dependency failure; and&lt;/li&gt;
&lt;li&gt;a non-retryable tool failure.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Version the suite beside the application. When product behavior intentionally changes, update the evidence set and expectations in the same review.&lt;/p&gt;

&lt;p&gt;A useful review matrix keeps scenario coverage and evidence type visible:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Structural check&lt;/th&gt;
&lt;th&gt;Outcome check&lt;/th&gt;
&lt;th&gt;Semantic check&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Eligible order&lt;/td&gt;
&lt;td&gt;lookup before refund&lt;/td&gt;
&lt;td&gt;refund record exists&lt;/td&gt;
&lt;td&gt;explanation is accurate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unknown order&lt;/td&gt;
&lt;td&gt;refund tool forbidden&lt;/td&gt;
&lt;td&gt;no mutation occurred&lt;/td&gt;
&lt;td&gt;refusal is useful&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Approval required&lt;/td&gt;
&lt;td&gt;approval tool present&lt;/td&gt;
&lt;td&gt;request queued&lt;/td&gt;
&lt;td&gt;message explains next step&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;No single column can substitute for the other two. A structurally correct run can still answer badly; a fluent answer can still hide an unauthorized mutation.&lt;/p&gt;

&lt;p&gt;This is also where “prove the suite can say no” becomes useful. Keep at least one known-bad trace or mutation that must fail. A check that has never rejected evidence and a check that silently stopped running can produce the same green icon.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compare behavior as a cohort
&lt;/h2&gt;

&lt;p&gt;For a prompt, model, tool-schema, or orchestration change, label captured runs:&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;inspectRun&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;agent-inspect&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;inspectRun&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-agent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;runScenario&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;traceDir&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-inspect&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;cohort&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;candidate&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;scenario&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-order&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;approved-model&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;Then compare baseline and candidate traces:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx agent-inspect cohort &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dir&lt;/span&gt; .agent-inspect &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--baseline&lt;/span&gt; before &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--candidate&lt;/span&gt; candidate &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cohort-key&lt;/span&gt; cohort &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--group-by&lt;/span&gt; model &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--metric&lt;/span&gt; errorRate,duration,toolChoice,observationFailure
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A cohort report preserves the run count and the actual aggregate values. Imagine this synthetic result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;before / approved-model (10 runs)
  error rate: 0%
  average duration: 820ms
  dominant tool: lookup_order

candidate / approved-model (10 runs)
  error rate: 10%
  average duration: 970ms
  dominant tool: refund_order
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a reason to investigate, not a statistically universal conclusion. The CLI can describe the recorded groups; it cannot make a small, biased fixture set representative.&lt;/p&gt;

&lt;h2&gt;
  
  
  A positive delta is not automatically bad
&lt;/h2&gt;

&lt;p&gt;Suppose the candidate makes one extra tool call. That might be waste—or a newly required authorization check.&lt;/p&gt;

&lt;p&gt;Suppose duration improves by 20%. That might be a better path—or the candidate skipped retrieval.&lt;/p&gt;

&lt;p&gt;Suppose the dominant tool changed. That might be a regression—or the exact migration you intended.&lt;/p&gt;

&lt;p&gt;Structural metrics describe what changed. Product assertions and semantic evaluation decide whether the change is acceptable. Keep those judgments separate so the report does not pretend to know your domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turn credible evidence into a fail-closed gate
&lt;/h2&gt;

&lt;p&gt;Once cases and thresholds reflect actual release policy, add a gate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx agent-inspect gate &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--suite&lt;/span&gt; agent-inspect.suite.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--format&lt;/span&gt; github &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; ./agent-gate-artifacts &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--evidence-on&lt;/span&gt; fail &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--evidence-profile&lt;/span&gt; share
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or gate a directory directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx agent-inspect gate &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dir&lt;/span&gt; .agent-inspect &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-error-rate&lt;/span&gt; 5 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--forbid-tool&lt;/span&gt; delete_account
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gate exit codes distinguish a policy failure from invalid configuration, unreadable traces, and unsupported output formats. Evidence generation does not suppress the original gate result.&lt;/p&gt;

&lt;p&gt;That behavior matters because agent tooling is especially vulnerable to accidental fail-open configurations. Version 6.17.5 strengthened empty-check handling, rule-execution evidence, observed-outcome requirements, and &lt;code&gt;requiredOrder&lt;/code&gt; semantics; 6.17.6 preserves those behaviors while hardening packaging and ingest boundaries.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a green gate actually means
&lt;/h2&gt;

&lt;p&gt;A passing gate means the encoded checks passed for the inspected evidence set.&lt;/p&gt;

&lt;p&gt;It does &lt;strong&gt;not&lt;/strong&gt; prove:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the cases represent production traffic;&lt;/li&gt;
&lt;li&gt;the final answers are correct;&lt;/li&gt;
&lt;li&gt;the model will repeat the behavior;&lt;/li&gt;
&lt;li&gt;the thresholds are good; or&lt;/li&gt;
&lt;li&gt;the agent is safe in production.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is not a weakness unique to agent tests. It is the normal boundary of evidence-based release decisions. The remedy is not one giant magical score; it is several checks with clear jobs.&lt;/p&gt;

&lt;h2&gt;
  
  
  My smallest credible release loop
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;capture named cases
      -&amp;gt; run deterministic suite
      -&amp;gt; compare baseline/candidate cohorts
      -&amp;gt; inspect semantic quality separately
      -&amp;gt; make CI fail closed
      -&amp;gt; preserve the evidence
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pinned &lt;a href="https://github.com/rajudandigam/agent-inspect/blob/e09d63b3ca36afb1c5caf3ece63f710b64c40b69/docs/SUITES-COHORTS-GATES.md" rel="noopener noreferrer"&gt;suites, cohorts, and gates documentation&lt;/a&gt; labels these surfaces Beta, and the &lt;a href="https://github.com/rajudandigam/agent-inspect/tree/e09d63b3ca36afb1c5caf3ece63f710b64c40b69/examples/recipes/cohort-baseline-candidate" rel="noopener noreferrer"&gt;cohort recipe&lt;/a&gt; includes a deliberately regressing fixture.&lt;/p&gt;

&lt;p&gt;One good run can prove that a path exists. It cannot prove that a release is ready.&lt;/p&gt;

&lt;p&gt;What is the smallest set of agent cases you would require before approving a change?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>testing</category>
      <category>devops</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Three Agent Runs Passed. One Missing Handoff Broke the Workflow</title>
      <dc:creator>Raju Dandigam</dc:creator>
      <pubDate>Thu, 10 Sep 2026 14:45:48 +0000</pubDate>
      <link>https://dev.to/raju_dandigam/three-agent-runs-passed-one-missing-handoff-broke-the-workflow-26kb</link>
      <guid>https://dev.to/raju_dandigam/three-agent-runs-passed-one-missing-handoff-broke-the-workflow-26kb</guid>
      <description>&lt;p&gt;My triage agent extracted a refund request and its order reference.&lt;/p&gt;

&lt;p&gt;The refund specialist said the reference was missing.&lt;/p&gt;

&lt;p&gt;The escalation agent then opened a generic support ticket.&lt;/p&gt;

&lt;p&gt;All three runs completed successfully.&lt;/p&gt;

&lt;p&gt;Viewed separately, each trace told a plausible story. Viewed as one user journey, they contradicted one another. The failure lived &lt;strong&gt;between&lt;/strong&gt; the green runs.&lt;/p&gt;

&lt;p&gt;That is the uncomfortable part of multi-agent debugging: a handoff can lose the one field that mattered while every component reports success.&lt;/p&gt;

&lt;h2&gt;
  
  
  The timestamp trap
&lt;/h2&gt;

&lt;p&gt;The tempting investigation is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;find the triage trace;&lt;/li&gt;
&lt;li&gt;search for a specialist trace a few milliseconds later;&lt;/li&gt;
&lt;li&gt;assume the next escalation belongs to the same request.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That works in a demo with one user. It breaks under queues, retries, parallel sessions, worker concurrency, and clock differences.&lt;/p&gt;

&lt;p&gt;Time proximity is useful for discovery. It is weak evidence of causality.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10:00:00.100 triage completed
10:00:00.102 triage completed
10:00:00.110 specialist started
10:00:00.111 specialist started

Which triage run produced which specialist run?
The timestamps cannot answer.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The relationship needs an identity that survives the boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give the journey an explicit spine
&lt;/h2&gt;

&lt;p&gt;For a cross-run workflow, I want four small pieces of metadata:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;sessionId&lt;/code&gt;: the user journey or workflow instance;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;workflowName&lt;/code&gt;: the reusable workflow type;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;handoffFrom&lt;/code&gt; and &lt;code&gt;handoffTo&lt;/code&gt;: the declared edge; and&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;retryOf&lt;/code&gt; plus &lt;code&gt;attempt&lt;/code&gt; when a run is a retry.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is a synthetic TypeScript example verified against &lt;a href="https://www.npmjs.com/package/agent-inspect/v/6.17.6" rel="noopener noreferrer"&gt;&lt;code&gt;agent-inspect@6.17.6&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;inspectRun&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;step&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;agent-inspect&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;traceDir&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-inspect&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;sessionId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sess-support-042&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;triage&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;inspectRun&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;triage-agent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;extract-request&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;category&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&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;orderRef&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;internal-value&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;span class="nx"&gt;traceDir&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="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;workflowName&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-refund&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;handoffFrom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;triage-agent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;handoffTo&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-specialist&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;span class="c1"&gt;// The bug: orderRef disappears while building the handoff payload.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;specialistInput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;triage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;category&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;specialist&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;inspectRun&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-specialist&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;read-handoff&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;needsEscalation&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;orderRef&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;specialistInput&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;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;receivedFields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;specialistInput&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="na"&gt;orderRefPresent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;orderRef&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;specialistInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;traceDir&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="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;workflowName&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-refund&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;handoffFrom&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-specialist&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;handoffTo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;escalation-agent&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;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;specialist&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;needsEscalation&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;inspectRun&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;escalation-agent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;open-ticket&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;undefined&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="nx"&gt;traceDir&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="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;workflowName&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-refund&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;Notice what the trace does &lt;strong&gt;not&lt;/strong&gt; store: the order reference itself. The field name and presence bit are enough to locate the broken boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inspect one session instead of three files
&lt;/h2&gt;

&lt;p&gt;List grouped workflow activity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx agent-inspect sessions &lt;span class="nt"&gt;--dir&lt;/span&gt; .agent-inspect
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then inspect the declared handoffs and each run's timeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx agent-inspect session sess-support-042 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dir&lt;/span&gt; .agent-inspect &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--timeline&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--diagnostics&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The session view first gives the investigation a bounded shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;session: sess-support-042

triage-agent
  handoff: triage-agent -&amp;gt; refund-specialist
                     |
                     v
refund-specialist
  handoff: refund-specialist -&amp;gt; escalation-agent
                     |
                     v
escalation-agent

three runs: completed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then inspect the specialist run's bounded step metadata:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx agent-inspect view &amp;lt;refund-specialist-run-id&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dir&lt;/span&gt; .agent-inspect &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--verbose&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;read-handoff
  receivedFields: category
  orderRefPresent: false
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The escalation was not the original failure. The field disappeared when &lt;code&gt;specialistInput&lt;/code&gt; was constructed.&lt;/p&gt;

&lt;p&gt;That sounds obvious after the fact. Before the runs are correlated, it is easy to blame the last agent because it produced the visible symptom.&lt;/p&gt;

&lt;h2&gt;
  
  
  Correlation metadata is reviewable architecture
&lt;/h2&gt;

&lt;p&gt;Explicit fields improve more than the trace viewer. They expose intended topology in code review.&lt;/p&gt;

&lt;p&gt;A reviewer can ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Should these runs share a session?&lt;/li&gt;
&lt;li&gt;Is the handoff target correct?&lt;/li&gt;
&lt;li&gt;Which fields are required at this boundary?&lt;/li&gt;
&lt;li&gt;Is this a retry of a specific run or merely another attempt?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AgentInspect refuses to manufacture a causal link from timestamps alone. If &lt;code&gt;attempt&lt;/code&gt; is greater than one without &lt;code&gt;retryOf&lt;/code&gt;, the relationship can be labeled &lt;code&gt;correlated&lt;/code&gt; rather than &lt;code&gt;explicit&lt;/code&gt;, with a diagnostic explaining the ambiguity.&lt;/p&gt;

&lt;p&gt;That distinction matters. A convincing diagram with an invented edge is worse than an incomplete diagram that admits uncertainty.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trace the boundary, but validate it too
&lt;/h2&gt;

&lt;p&gt;Observability explains what executed. It should not be the first place a missing required field is discovered.&lt;/p&gt;

&lt;p&gt;Add runtime validation before sending and after receiving:&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;RefundHandoff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;category&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&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;orderRef&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;assertRefundHandoff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;value&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="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;asserts&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;RefundHandoff&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;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;category&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refund&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;Invalid refund category&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="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orderRef&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orderRef&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Missing orderRef at refund handoff&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The schema check prevents the bad handoff. The session trace shows which boundary and version actually executed when behavior still surprises you. Those are complementary jobs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Record the contract version, not the payload
&lt;/h2&gt;

&lt;p&gt;Multi-agent workflows evolve. The triage agent may emit &lt;code&gt;orderRef&lt;/code&gt; today while a newer specialist expects &lt;code&gt;orderId&lt;/code&gt;. Even when both fields are strings, that is a protocol change.&lt;/p&gt;

&lt;p&gt;Add a bounded contract identifier to the step or run metadata:&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="nx"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;workflowName&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-refund&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;handoffFrom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;triage-agent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;handoffTo&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-specialist&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;handoffContract&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/v2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;receivedFields&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;category&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;orderRef&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;The identifier lets a reviewer ask whether producer and consumer used the same boundary definition without storing the business value itself. It also separates two failure classes that otherwise look identical:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;missing field
  ├─ producer never emitted it
  ├─ transport dropped it
  ├─ mapper renamed it
  └─ consumer expected another contract version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;AgentInspect treats these values as metadata; it does not enforce &lt;code&gt;refund-request/v2&lt;/code&gt;. Your schema validator or message layer still owns that guarantee.&lt;/p&gt;

&lt;h2&gt;
  
  
  What sessions do not solve
&lt;/h2&gt;

&lt;p&gt;A session index is not a workflow engine. It does not deliver messages, guarantee field consumption, or validate every handoff schema. Current handoff-related TraceContract support is not a complete policy language.&lt;/p&gt;

&lt;p&gt;Session identifiers can also become sensitive correlation data. Use random identifiers rather than business values, keep metadata bounded, and apply an appropriate redaction profile before sharing traces outside the team.&lt;/p&gt;

&lt;p&gt;The pinned &lt;a href="https://github.com/rajudandigam/agent-inspect/blob/e09d63b3ca36afb1c5caf3ece63f710b64c40b69/docs/SESSIONS-AND-OUTCOMES.md" rel="noopener noreferrer"&gt;sessions and outcomes documentation&lt;/a&gt; describes the current boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first question is not “Which agent failed?”
&lt;/h2&gt;

&lt;p&gt;For multi-agent systems, start with:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;At which boundary did the state stop matching the workflow's promise?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;One explicit session ID, two handoff endpoints, and a few safe field-presence checks can turn three green traces into one explainable failure.&lt;/p&gt;

&lt;p&gt;Where does your multi-agent system validate a handoff: before sending, after receiving, or both?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>debugging</category>
      <category>typescript</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
