<?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: Saurav Bhattacharya</title>
    <description>The latest articles on DEV Community by Saurav Bhattacharya (@saurav_bhattacharya).</description>
    <link>https://dev.to/saurav_bhattacharya</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%2F3861679%2Fd1fe6e78-61da-46c5-9669-bf7a7f30150d.jpg</url>
      <title>DEV Community: Saurav Bhattacharya</title>
      <link>https://dev.to/saurav_bhattacharya</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/saurav_bhattacharya"/>
    <language>en</language>
    <item>
      <title>One Quality Score Is a Lie: Split Your RAG Judge Into Retrieval, Groundedness, and Relevance</title>
      <dc:creator>Saurav Bhattacharya</dc:creator>
      <pubDate>Wed, 19 Aug 2026 01:02:05 +0000</pubDate>
      <link>https://dev.to/saurav_bhattacharya/one-quality-score-is-a-lie-split-your-rag-judge-into-retrieval-groundedness-and-relevance-473m</link>
      <guid>https://dev.to/saurav_bhattacharya/one-quality-score-is-a-lie-split-your-rag-judge-into-retrieval-groundedness-and-relevance-473m</guid>
      <description>&lt;p&gt;Ask an LLM judge to score your RAG agent's answer and it will hand you a number. A 7. A 4.2. A crisp green 0.85. That number feels like signal. It is actually three different failures averaged into mush, and the average is engineered to hide the one you most need to see.&lt;/p&gt;

&lt;p&gt;I'll make the case with a concrete .NET reference implementation (&lt;a href="https://github.com/sauravbhattacharya001/maf-evals" rel="noopener noreferrer"&gt;maf-evals&lt;/a&gt;), a three-tier agent evaluation built on Microsoft Agent Framework and .NET 8. Every number below came from running it, not from reasoning about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with a single score
&lt;/h2&gt;

&lt;p&gt;A retrieval-augmented answer can be wrong in at least three independent ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;retrieval&lt;/strong&gt; was bad — wrong documents came back, or the knowledge base doesn't cover the question.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;groundedness&lt;/strong&gt; was bad — the retrieved documents were fine, but the answer made claims they don't support. That's a hallucination.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;relevance&lt;/strong&gt; was bad — the answer is perfectly grounded in real documents and still doesn't answer what the user asked.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These have nothing to do with each other. A great retrieval can feed a hallucinated answer. A flawlessly grounded answer can miss the question entirely. When you collapse them into one "quality" score, a mediocre-everywhere answer and a perfect-retrieval-but-hallucinating answer can land on the same 3.5. You cannot tell which knob to turn, because the score was designed to be indifferent to which knob is broken.&lt;/p&gt;

&lt;p&gt;So the maf-evals RAG triad scores all three separately, and never averages them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;| Score        | Isolates                                        |
| Retrieval    | a bad knowledge base or a bad query             |
| Groundedness | claims the retrieved context doesn't support    |
| Relevance    | well-grounded answers that miss the question    |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why the average actively lies
&lt;/h2&gt;

&lt;p&gt;Here's the part that turned my opinion from "nice to have" to "non-negotiable." When you calibrate these judges against human labels — and you &lt;em&gt;must&lt;/em&gt; calibrate them, a threshold picked without calibration is just taste — the Groundedness judge produces this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;| Score        | Exact | Within 1 | MAE  | Bias  | Same band |
| Groundedness | 42%   | 67%      | 1.17 | -0.17 | 75%       |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look at that bias: &lt;strong&gt;-0.17&lt;/strong&gt;. Nearly zero. If you were tracking a single blended quality score, that -0.17 would tell you the judge is essentially unbiased and calibrated. Ship it.&lt;/p&gt;

&lt;p&gt;It is not calibrated. The Groundedness judge fails in two opposite directions at once. It scores outright fabrication at exactly 3.0 every time — a hallucination sails through as a mild warning — &lt;em&gt;and&lt;/em&gt; it penalises well-grounded answers for being slightly off topic. The two errors point opposite ways, so they cancel. The near-zero bias is the average of a systematic over-score and a systematic under-score, and it looks healthy precisely because both are broken.&lt;/p&gt;

&lt;p&gt;The metrics that expose it are the ones the average destroys: a mean absolute error of &lt;strong&gt;1.17&lt;/strong&gt; (over a 1–5 scale, that's enormous) and band agreement of only 75%. You only see those if you refuse to blend.&lt;/p&gt;

&lt;p&gt;That single insight — that fabrication was being scored 3.0 — is why the Groundedness &lt;em&gt;floor&lt;/em&gt; moved from 3.0 to 3.5. At 3.0, every hallucination slipped through as a warning. Moving the floor to 3.5 lifted band agreement from 50% to 75%. A blended score would never have surfaced the problem to move the floor over.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deterministic first, judge last
&lt;/h2&gt;

&lt;p&gt;The triad isn't the first thing that runs. In the pull-request gate (Tier 2), five checks run cheapest-first, and the judge is &lt;em&gt;last&lt;/em&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Rules&lt;/strong&gt; — the same rule engine the live agent uses, so a rule can't drift between production and CI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retrieval&lt;/strong&gt; — did the expected document IDs come back? Exact, free, no model involved.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool calls&lt;/strong&gt; — right tool, right arguments? Compared, not judged.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Meaning&lt;/strong&gt; — embeddings, for cases where wording is free to vary. Deterministic, ~1000x cheaper than a judge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RAG triad&lt;/strong&gt; — the judge, and only now.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The ordering is doctrine, not convenience. Evidence ranks on an &lt;strong&gt;independence&lt;/strong&gt; axis — independent to corruptible — not a cost axis. A document-ID match is independent: it means the same thing every run. A judge score is corruptible: it moves between runs. So the exact checks run first and &lt;em&gt;always block&lt;/em&gt;, while the judge scores get two thresholds — a floor that blocks and a target that warns — because a number that wobbles has no business being a hard gate on its own.&lt;/p&gt;

&lt;p&gt;This is why Retrieval scoring is &lt;em&gt;advisory&lt;/em&gt; in maf-evals, even though it's part of the triad. Calibration caught the judge returning &lt;code&gt;5, 2, 4, 5, 2&lt;/code&gt; for the same input — 17% of cases would flip a merge decision at random. So &lt;code&gt;expectedChunkIds&lt;/code&gt;, an exact and free check, does the actual gating. The judge only annotates.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it looks like in code
&lt;/h2&gt;

&lt;p&gt;A golden case declares the deterministic expectation right next to the judged one. Gating lives in the exact fields; the triad adds color:&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;"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;"refund-within-limit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"query"&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 A-31905 arrived damaged. Please refund me 120 for it."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expectedChunkIds"&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;"refunds#3"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expectedToolCalls"&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;"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;"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;"arguments"&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;"orderId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A-31905"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;120&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;"semanticExpectations"&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;"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;"confirms_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;"anyOf"&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;"Your refund of 120 has been issued."&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"minSimilarity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.55&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;&lt;code&gt;expectedChunkIds&lt;/code&gt; gates retrieval for free. The triad's Retrieval score, being corruptible, only advises on top of it. Groundedness and Relevance — the two that genuinely need a judge, because they're about &lt;em&gt;meaning&lt;/em&gt; — get their floor-and-target bands. Nothing is averaged.&lt;/p&gt;

&lt;p&gt;And it's cheap to be this careful about which score means what, but not free: the judge costs about &lt;strong&gt;250x&lt;/strong&gt; the agent. A full Tier 2 run is ~$0.16. The thing being tested is nearly free; measuring it is the entire bill. That's exactly why you don't want to pay a judge to produce a blended number you then can't act on.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;One quality score is a management metric wearing an engineering costume. It goes up and to the right and tells you nothing you can fix. Split it, and each score points at a specific, actionable failure: bad KB, hallucination, or off-topic. Then gate on the checks that don't wobble, and let the judge report on the two that genuinely require meaning.&lt;/p&gt;

&lt;p&gt;If you want the whole thing — the triad, the calibration harness that caught the 3.0-hallucination bug, the deterministic checks, and the cost tracker that proves the judge is your real bill — it's all runnable .NET here: &lt;strong&gt;&lt;a href="https://github.com/sauravbhattacharya001/maf-evals" rel="noopener noreferrer"&gt;github.com/sauravbhattacharya001/maf-evals&lt;/a&gt;&lt;/strong&gt;. Clone it, run &lt;code&gt;dotnet run --project src/EvalRunner -- calibrate --repeat 3&lt;/code&gt;, and watch a near-zero bias hide a broken judge on your own screen.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>evaluation</category>
      <category>dotnet</category>
      <category>testing</category>
    </item>
    <item>
      <title>Measure the Judge Before You Trust It: Self-Consistency Comes Before Human Agreement</title>
      <dc:creator>Saurav Bhattacharya</dc:creator>
      <pubDate>Wed, 12 Aug 2026 20:47:04 +0000</pubDate>
      <link>https://dev.to/saurav_bhattacharya/measure-the-judge-before-you-trust-it-self-consistency-comes-before-human-agreement-lf6</link>
      <guid>https://dev.to/saurav_bhattacharya/measure-the-judge-before-you-trust-it-self-consistency-comes-before-human-agreement-lf6</guid>
      <description>&lt;p&gt;Here's a question almost no eval pipeline can answer: &lt;em&gt;if you asked your LLM judge to score the exact same response five times, would you get the same number back?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Most teams never check. They wire up an &lt;code&gt;LLM-as-judge&lt;/code&gt;, pick a threshold that feels right ("block anything under 7"), and ship it as a merge gate. Then they spend months wondering why the same PR is green on one run and red on the next. The judge wasn't wrong. It was &lt;em&gt;unstable&lt;/em&gt;, and nobody measured the stability before trusting the number.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://github.com/sauravbhattacharya001/maf-evals" rel="noopener noreferrer"&gt;maf-evals&lt;/a&gt; — a three-tier agent evaluation reference on Microsoft Agent Framework and .NET 8 — partly to force this discipline into the open. The rule that fell out of it: &lt;strong&gt;measure the judge before you trust it, and measure self-consistency before you even think about human agreement.&lt;/strong&gt; Every number below came from actually running the calibration command, not from reasoning about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two questions, in the only order that works
&lt;/h2&gt;

&lt;p&gt;When you calibrate a judge, there are two questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Does the judge agree &lt;strong&gt;with itself&lt;/strong&gt;?&lt;/li&gt;
&lt;li&gt;Does the judge agree &lt;strong&gt;with a human&lt;/strong&gt;?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The second question is meaningless without the first. If a judge gives the same input a 5, then a 2, then a 4, then a 5, then a 2 — comparing its "score" to a human label is comparing to a coin flip. You can't calibrate a ruler that changes length every time you pick it up.&lt;/p&gt;

&lt;p&gt;So maf-evals runs them in order. Twelve hand-labelled cases, each judged three times, self-consistency first.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;dotnet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;src/EvalRunner&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;calibrate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--repeat&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;3&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Self-consistency: where a comfortable average hides a broken metric
&lt;/h2&gt;

&lt;p&gt;Here's the self-consistency table from an actual run. Three RAG-triad scores — Retrieval, Groundedness, Relevance — each judged three times across the calibration set:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Score&lt;/th&gt;
&lt;th&gt;Mean SD&lt;/th&gt;
&lt;th&gt;Worst range&lt;/th&gt;
&lt;th&gt;Verdict flips&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Retrieval&lt;/td&gt;
&lt;td&gt;0.20&lt;/td&gt;
&lt;td&gt;3.0&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;17%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Groundedness&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;0.0&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Relevance&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;0.0&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Look at Retrieval. A &lt;strong&gt;mean standard deviation of 0.20&lt;/strong&gt; looks fantastic. If you stopped there — as most dashboards do — you'd conclude the judge is rock solid and promote it to a blocking gate.&lt;/p&gt;

&lt;p&gt;That average is lying to you. Given the same input five times, Retrieval returned &lt;code&gt;5, 2, 4, 5, 2&lt;/code&gt;. Most cases were perfectly stable, which dragged the &lt;em&gt;mean&lt;/em&gt; SD down to a cozy 0.20 — while two cases swung a full three points. The consequence is the column that actually matters: &lt;strong&gt;17% of cases would flip a merge decision at random.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Think about what that means in CI. Nearly one in five pull requests, near the threshold, gets a merge verdict decided by nothing but which sample the judge happened to draw. That's not a quality gate. That's a random number generator with a nice UI.&lt;/p&gt;

&lt;p&gt;The fix isn't to tune the prompt until Retrieval calms down. The fix is a doctrine: &lt;strong&gt;a score that moves between runs has no business blocking a merge.&lt;/strong&gt; Retrieval was demoted to advisory only. The actual gating for "did the right documents come back?" is done by an exact, free, deterministic check — comparing returned chunk IDs against &lt;code&gt;expectedChunkIds&lt;/code&gt;. No judge, no wobble.&lt;/p&gt;

&lt;p&gt;This is the core independence idea in the whole repo. Evidence ranks on an &lt;strong&gt;independence axis&lt;/strong&gt; — independent to corruptible — not a cost axis. &lt;strong&gt;Rules gate; judges report.&lt;/strong&gt; A deterministic ID comparison is unforgeable and never flips, so it gates. A judge's opinion moves between runs, so it reports.&lt;/p&gt;

&lt;h2&gt;
  
  
  Only &lt;em&gt;now&lt;/em&gt; do you ask about human agreement
&lt;/h2&gt;

&lt;p&gt;Groundedness and Relevance passed self-consistency (0.00 SD), so they've earned the right to be compared against human labels. Here's that table:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Score&lt;/th&gt;
&lt;th&gt;Exact&lt;/th&gt;
&lt;th&gt;Within 1&lt;/th&gt;
&lt;th&gt;MAE&lt;/th&gt;
&lt;th&gt;Bias&lt;/th&gt;
&lt;th&gt;Correlation&lt;/th&gt;
&lt;th&gt;Same band&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Retrieval&lt;/td&gt;
&lt;td&gt;75%&lt;/td&gt;
&lt;td&gt;92%&lt;/td&gt;
&lt;td&gt;0.42&lt;/td&gt;
&lt;td&gt;-0.42&lt;/td&gt;
&lt;td&gt;0.88&lt;/td&gt;
&lt;td&gt;83%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Groundedness&lt;/td&gt;
&lt;td&gt;42%&lt;/td&gt;
&lt;td&gt;67%&lt;/td&gt;
&lt;td&gt;1.17&lt;/td&gt;
&lt;td&gt;-0.17&lt;/td&gt;
&lt;td&gt;0.44&lt;/td&gt;
&lt;td&gt;75%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Relevance&lt;/td&gt;
&lt;td&gt;25%&lt;/td&gt;
&lt;td&gt;83%&lt;/td&gt;
&lt;td&gt;0.92&lt;/td&gt;
&lt;td&gt;-0.42&lt;/td&gt;
&lt;td&gt;0.74&lt;/td&gt;
&lt;td&gt;83%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Now look at Groundedness, and notice the trap. Its &lt;strong&gt;bias is -0.17&lt;/strong&gt; — practically zero. If bias were your health metric, you'd call it well-calibrated and move on.&lt;/p&gt;

&lt;p&gt;Groundedness is broken in two opposite directions at once. It scores outright fabrication at &lt;em&gt;exactly 3.0&lt;/em&gt;, every single time — and it separately penalises well-grounded answers for being slightly off-topic. Those two errors point in opposite directions, so they cancel in the aggregate, and the bias reads as a healthy -0.17. The metrics that expose the truth are &lt;strong&gt;mean absolute error (1.17)&lt;/strong&gt; and &lt;strong&gt;band agreement (75%)&lt;/strong&gt; — not the average.&lt;/p&gt;

&lt;p&gt;Which is why the Groundedness floor moved from 3.0 to 3.5. At a 3.0 floor, every hallucination — parked at exactly 3.0 — slipped through as a mere warning instead of a block. That single threshold change lifted band agreement from &lt;strong&gt;50% to 75%&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That's the whole point of calibration: the threshold wasn't chosen by taste. It was chosen because the labelled fixtures showed hallucinations clustering at 3.0, so the floor had to sit above them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two thresholds, because judges wobble and rules don't
&lt;/h2&gt;

&lt;p&gt;Because judge scores move between runs, a single cut-off turns every borderline case into a coin flip — exactly the 17% problem. So in maf-evals, every judge score gets &lt;strong&gt;two&lt;/strong&gt; thresholds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a &lt;strong&gt;floor&lt;/strong&gt; that blocks, and&lt;/li&gt;
&lt;li&gt;a &lt;strong&gt;target&lt;/strong&gt; that warns.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The band between them absorbs the wobble. Deterministic checks — chunk-ID matches, tool-call comparisons, rule evaluations — get &lt;em&gt;no&lt;/em&gt; band. They always block, because they don't move.&lt;/p&gt;

&lt;p&gt;Conceptually, the gate logic looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;GateOutcome&lt;/span&gt; &lt;span class="nf"&gt;ApplyScore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JudgeScore&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ScoreBand&lt;/span&gt; &lt;span class="n"&gt;band&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Deterministic checks never reach here — they block or pass, no band.&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;band&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Floor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;GateOutcome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Block&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;F1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &amp;lt; floor &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;band&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Floor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;F1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&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="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;band&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;GateOutcome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;F1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &amp;lt; target &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;band&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;F1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;GateOutcome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Pass&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Groundedness after calibration: floor raised so hallucinations (which&lt;/span&gt;
&lt;span class="c1"&gt;// the judge parks at exactly 3.0) fall below it and BLOCK.&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;groundedness&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ScoreBand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Floor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4.0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The floor at 3.5 isn't a guess. It's the number the labelled data demanded, verified by the jump in band agreement.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule that keeps you honest
&lt;/h2&gt;

&lt;p&gt;One more constraint makes this whole thing trustworthy: &lt;strong&gt;re-run calibration after you change the judge model, the labelling guide, or any threshold.&lt;/strong&gt; Scores from different judges are not comparable. A 3 from &lt;code&gt;gpt-4o&lt;/code&gt; and a 3 from next quarter's model are different quantities wearing the same label. If you swap the judge and keep the old thresholds, you've silently thrown away the calibration and you're back to picking numbers by feel.&lt;/p&gt;

&lt;p&gt;And the calibration set itself is built so the three scores &lt;em&gt;pull apart&lt;/em&gt; from each other — twelve cases engineered so a judge can't get a good grade by collapsing retrieval, groundedness, and relevance into one vague sense of "quality." If your calibration cases don't isolate the failure modes, your calibration proves nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;An LLM judge is a measuring instrument, and you would never trust a measuring instrument you hadn't checked against a reference. Yet teams routinely promote a raw judge score straight to a merge gate.&lt;/p&gt;

&lt;p&gt;The order is non-negotiable:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Self-consistency first.&lt;/strong&gt; If the judge disagrees with itself, nothing else matters. Watch the &lt;em&gt;worst-case range and verdict-flip rate&lt;/em&gt;, not the mean SD — a comfortable 0.20 average hid a 17% coin flip.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Human agreement second&lt;/strong&gt;, and only for scores that survived step one. Watch MAE and band agreement, not bias — Groundedness's near-zero bias hid a metric broken in two directions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anything that wobbles gets demoted&lt;/strong&gt; from gate to advisory, and the deterministic check does the real gating.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Rules gate; judges report. Measure the judge before you trust it. If a score moves between runs, it doesn't belong on the merge path.&lt;/p&gt;

&lt;p&gt;The full three-tier implementation — guardrails inside the agent, the PR gate, trajectory judging, plus the calibration harness and an adversarial safety suite — is on Microsoft Agent Framework and .NET 8 here: &lt;strong&gt;&lt;a href="https://github.com/sauravbhattacharya001/maf-evals" rel="noopener noreferrer"&gt;github.com/sauravbhattacharya001/maf-evals&lt;/a&gt;&lt;/strong&gt;. If you're about to trust an LLM judge with a blocking decision, run &lt;code&gt;calibrate --repeat 3&lt;/code&gt; on your own judge first. The number that comes back will change how you gate.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>evaluation</category>
      <category>dotnet</category>
      <category>testing</category>
    </item>
    <item>
      <title>Your Agent's Context Window Overflowed and It Answered Anyway</title>
      <dc:creator>Saurav Bhattacharya</dc:creator>
      <pubDate>Wed, 12 Aug 2026 01:03:33 +0000</pubDate>
      <link>https://dev.to/saurav_bhattacharya/your-agents-context-window-overflowed-and-it-answered-anyway-1cd7</link>
      <guid>https://dev.to/saurav_bhattacharya/your-agents-context-window-overflowed-and-it-answered-anyway-1cd7</guid>
      <description>&lt;p&gt;Your agent works great in the demo. Then someone hands it a real ticket with a 40-message thread, three attached logs, and a stack trace, and it confidently answers using the &lt;em&gt;first half&lt;/em&gt; of the context — because the second half fell off the back of the window. No error. No exception. Just a quietly wrong answer with full confidence. This is context overflow, and it is one of the most under-instrumented failure modes in production agents.&lt;/p&gt;

&lt;p&gt;Here is the uncomfortable part: &lt;strong&gt;your model-as-judge will not catch it, and it shouldn't be asked to.&lt;/strong&gt; Context truncation is not a subjective quality problem. It is an observable, deterministic fact about what actually entered the model. You can prove it happened. That makes it a Tier 1 problem, and treating it like one changes everything about how you defend against it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is invisible
&lt;/h2&gt;

&lt;p&gt;Most agent frameworks silently truncate. You assemble a prompt from system instructions, retrieved documents, tool outputs, and conversation history, and if it exceeds the window, the framework (or the provider) drops the overflow — usually from the middle or the oldest turns. The model still returns a fluent, plausible response. Your evals are green. Your users get answers built on a partial view of the problem.&lt;/p&gt;

&lt;p&gt;The reason this slips through is that teams evaluate the &lt;em&gt;output text&lt;/em&gt; and never inspect the &lt;em&gt;resolved input&lt;/em&gt;. They ask "did the answer look good?" instead of "did the evidence the agent needed actually make it into the call?" Those are different questions, and only one of them is answerable without opinion.&lt;/p&gt;

&lt;h2&gt;
  
  
  The independence axis, not the cost axis
&lt;/h2&gt;

&lt;p&gt;This is where I want to plant a flag, because it's the thing that separates real agent evaluation from "LLM-as-judge gives you a 7/10" tooling. Evidence should be ranked on an &lt;strong&gt;independence axis&lt;/strong&gt; — from independent to corruptible — not a cost axis of cheap to expensive.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tier 1 — externally observable proof the agent can't forge.&lt;/strong&gt; Did the input fit the window? Did the required document ID appear in the resolved prompt? Was the tool output non-empty? Did the run finish inside its timeout? These are facts. Valid or not, present or not, truncated or not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tier 2 — statistical signal against a baseline the agent didn't author.&lt;/strong&gt; Is the retrieved chunk actually similar to the task embedding? Did the token count spike 3x versus the rolling baseline for this task type? Did the diff change anything?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tier 3 — model-as-judge.&lt;/strong&gt; A shared-substrate opinion. A signal, never a verdict.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Context overflow lives squarely in Tier 1. You don't need a smarter model to tell you the prompt didn't fit — you need to measure the resolved prompt.&lt;/p&gt;

&lt;p&gt;There's a second reason Tier 3 is the wrong tool here, and it's structural: &lt;strong&gt;a model judging what another model saw is circular.&lt;/strong&gt; Judge and judged share a substrate; there's no independent ground truth in that loop. Tier 1 and Tier 2 can run over the agent's actual trajectory precisely because they inspect artifacts the agent didn't get to write — the byte count of the assembled prompt, the presence of a chunk ID, the embedding of the input. The judge can only offer opinion about text, and it should only ever inspect artifacts the judged agent didn't author.&lt;/p&gt;

&lt;h2&gt;
  
  
  Instrument the resolved input
&lt;/h2&gt;

&lt;p&gt;The fix is boring and effective: capture the fully-resolved prompt at the moment of the call and gate on it deterministically, before you spend a judge token.&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;encoding_for_model&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;tiktoken&lt;/span&gt;&lt;span class="dl"&gt;"&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;ResolvedCall&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;taskId&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;requiredIds&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;// doc/chunk IDs this task needs&lt;/span&gt;
  &lt;span class="nl"&gt;resolvedPrompt&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;// the ACTUAL assembled prompt sent&lt;/span&gt;
  &lt;span class="nl"&gt;windowLimit&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="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;GateResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;pass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;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;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;contextGate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ResolvedCall&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;GateResult&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;enc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;encoding_for_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;gpt-4o&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;tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;enc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resolvedPrompt&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;enc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;free&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GateResult&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="c1"&gt;// Tier 1: did it physically fit?&lt;/span&gt;
  &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;tier&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;pass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;windowLimit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`resolved prompt = &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; tokens (limit &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;windowLimit&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Tier 1: did every required piece of evidence survive assembly?&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;missing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;requiredIds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="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;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resolvedPrompt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="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="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;tier&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;pass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;
      &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`dropped required evidence: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
      &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;all required evidence present&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;results&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 this does &lt;em&gt;not&lt;/em&gt; do: it doesn't ask whether the answer was good. It asks whether the answer was even &lt;em&gt;possible&lt;/em&gt; given what entered the model. If a required chunk ID never made it into the resolved prompt, the run is dead on arrival — block it, don't grade it. This is the real-time gate: deterministic, roughly free, fast enough to sit in the hot path and stop a bad run before it ships.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two halves: score the output, trace the run
&lt;/h2&gt;

&lt;p&gt;You cannot gate on the resolved prompt if you never captured it. This is why evaluation and tracing ship as one workflow, not two products.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;agent-eval&lt;/strong&gt; scores and gates the output using the tier doctrine above — it's the thing that knows "context overflowed" is a Tier 1 red, not a judge's vibe. &lt;strong&gt;AgentLens&lt;/strong&gt; captures the trace of &lt;em&gt;how the agent got there&lt;/em&gt;: every model and tool step, the resolved inputs, the raw outputs. That trace is what gives Tier 1 and Tier 2 something unforgeable to score against, because the agent didn't author its own trace — the harness did. Without the trace, "did the required evidence enter the window?" is unanswerable. With it, it's a two-line check.&lt;/p&gt;

&lt;p&gt;Run them together and the division of labor is clean: AgentLens records the ground truth of the run, agent-eval decides whether that ground truth clears the gate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ship the 80%
&lt;/h2&gt;

&lt;p&gt;Most agent failures are not subtle disagreements a judge must arbitrate. They are stale caches, crashes, malformed JSON, hallucinated file paths, empty tool results — and context that silently overflowed. All of that is caught at Tier 1 and Tier 2 alone, deterministically, at roughly zero cost, in the hot path. Reserve the model-as-judge for the genuinely subjective ~20% tail, and label its output honestly: &lt;strong&gt;opinion, not evidence.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The context window isn't a config detail. It's the boundary of what your agent could possibly know at inference time. Measure that boundary, gate on it, and you'll retire a whole category of confident-but-wrong answers before a judge ever needs an opinion.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>observability</category>
      <category>evaluation</category>
    </item>
    <item>
      <title>Your Golden Dataset Is Rotting: The Eval Oracle Nobody Re-Validates</title>
      <dc:creator>Saurav Bhattacharya</dc:creator>
      <pubDate>Sun, 09 Aug 2026 01:01:57 +0000</pubDate>
      <link>https://dev.to/saurav_bhattacharya/your-golden-dataset-is-rotting-the-eval-oracle-nobody-re-validates-4id3</link>
      <guid>https://dev.to/saurav_bhattacharya/your-golden-dataset-is-rotting-the-eval-oracle-nobody-re-validates-4id3</guid>
      <description>&lt;p&gt;We talk about agents drifting. We almost never talk about the thing we measure them against drifting. But your golden dataset — the fixtures, expected outputs, and "known good" traces your evals grade against — is code that ships to production and then never gets a code review again. It rots. And when your oracle rots, a green dashboard stops meaning your agent is correct. It means your agent still agrees with a snapshot of the world you took eight months ago.&lt;/p&gt;

&lt;p&gt;This is the failure mode senior teams keep rediscovering the hard way: the agent is fine, the harness is fine, and the &lt;em&gt;test oracle&lt;/em&gt; is the thing that's wrong. Nobody re-validates it, because passing tests feel like the end of the story instead of a claim that also decays.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why oracles rot
&lt;/h2&gt;

&lt;p&gt;A golden dataset encodes assumptions about the world at the moment you captured it. A few months later:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The API you called changed its response shape, so your "expected output" is now describing an endpoint that no longer exists.&lt;/li&gt;
&lt;li&gt;The correct answer changed. "Current CEO," "latest stable version," "the recommended flag" — these have expiration dates baked in.&lt;/li&gt;
&lt;li&gt;A human labeled the golden answer under a policy that has since been updated, so your eval now enforces last quarter's rules.&lt;/li&gt;
&lt;li&gt;The expected string was &lt;em&gt;itself generated by a model&lt;/em&gt;, and you never checked whether it was actually right. You froze a plausible guess and called it truth.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these trip an alarm. Your suite is green the whole way down, because green means "matches the fixture," not "matches reality."&lt;/p&gt;

&lt;h2&gt;
  
  
  The tier lens: which evidence actually expires?
&lt;/h2&gt;

&lt;p&gt;This is where it helps to stop ranking evidence by cost (cheap vs expensive) and start ranking it by &lt;strong&gt;independence&lt;/strong&gt; — how forgeable the signal is, and by extension how well it holds up over time. &lt;code&gt;agent-eval&lt;/code&gt; sorts evidence into three tiers on exactly that axis:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tier 1 — externally observable proof the agent can't forge.&lt;/strong&gt; Valid JSON, the file exists, the URL resolves, it compiled, the tests passed, it finished inside the timeout, the output is non-empty.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tier 2 — statistical signal against a baseline the agent didn't author.&lt;/strong&gt; Embedding similarity to the task, length and repetition checks, whether the diff actually changed anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tier 3 — model-as-judge.&lt;/strong&gt; A shared-substrate &lt;em&gt;opinion&lt;/em&gt;. A signal, never a verdict.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now re-read the rot list through that lens. Tier 1 checks barely rot: "is this valid JSON" and "does this file exist" mean the same thing today as they did last year. The oracle for Tier 1 &lt;em&gt;is the world&lt;/em&gt;, and the world re-validates itself every run. Tier 2 rots slowly and legibly: your baseline embedding shifts as the task definition shifts, which is a signal you can watch. It's &lt;strong&gt;Tier 3 and hand-frozen golden strings&lt;/strong&gt; that rot fastest and most silently — because their oracle is an opinion or a snapshot, and opinions and snapshots don't refresh themselves.&lt;/p&gt;

&lt;p&gt;That's the argument for two structural rules a lot of teams learn late:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Tier 1 + Tier 2 are your real-time gate&lt;/strong&gt; — deterministic, roughly free, fast enough to block a run in the hot path. &lt;strong&gt;Tier 3 is offline-only&lt;/strong&gt; — metered, slow, non-deterministic, so it can't sit in the latency budget of a live request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tier 1 + Tier 2 may run over the agent's full trajectory. Tier 3 may not.&lt;/strong&gt; A model judging another model's reasoning is circular — judge and judged share a substrate, so there's no independent ground truth in the loop. Tier 3 gets to inspect artifacts the judged agent didn't get to write, and nothing more.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A rotting golden dataset is what happens when you let a frozen Tier 3 opinion masquerade as a Tier 1 fact.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ship the 80%, then re-validate the tail
&lt;/h2&gt;

&lt;p&gt;Most real failures — stale output, a crash, a malformed response, a hallucinated file path, an empty answer — are caught at Tier 1 + Tier 2 alone, and those checks age gracefully. Reserve the judge for the ~20% subjective tail, and label its output as "opinion, not evidence." That framing also tells you &lt;em&gt;where to point your re-validation budget&lt;/em&gt;: not at the whole suite, but at the fixtures whose correctness is an opinion or a snapshot. Those are the ones with an expiration date.&lt;/p&gt;

&lt;p&gt;Here's the cheap version: give every golden fixture a freshness contract, and let Tier 1 checks re-prove the world-facing ones on every 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="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Tier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;GoldenFixture&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;oracleTier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Tier&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;        &lt;span class="c1"&gt;// how the "truth" was established&lt;/span&gt;
  &lt;span class="nl"&gt;capturedAt&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;// ISO timestamp&lt;/span&gt;
  &lt;span class="nl"&gt;maxAgeDays&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// expiration for snapshot/opinion oracles&lt;/span&gt;
  &lt;span class="nl"&gt;reverify&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Tier 1 re-proof against the world&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;checkOracleHealth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GoldenFixture&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;problems&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="c1"&gt;// Tier 1 fixtures can re-prove themselves: does the URL/file still resolve?&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;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reverify&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reverify&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;problems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: world-facing oracle no longer holds`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Snapshot/opinion oracles (frozen strings, judge labels) expire.&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;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;oracleTier&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ageDays&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="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;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;capturedAt&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;86&lt;/span&gt;&lt;span class="nx"&gt;_400_000&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;ageDays&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxAgeDays&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;problems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: oracle is &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ageDays&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;d old (max &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxAgeDays&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;), re-validate before trusting`&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;return&lt;/span&gt; &lt;span class="nx"&gt;problems&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run that as a meta-eval — an eval on your evals. When it goes red, you don't touch the agent. You go re-validate the oracle. The point isn't the specific thresholds; it's making "this expected value has an expiration date" a first-class property instead of a tribal assumption.&lt;/p&gt;

&lt;h2&gt;
  
  
  You can't re-validate what you can't see
&lt;/h2&gt;

&lt;p&gt;Here's the part teams miss: to re-validate a fixture, you need to know how the agent actually produced the output you're grading, not just the final string. That's where the eval half and the trace half meet. &lt;code&gt;agent-eval&lt;/code&gt; scores and gates the output — the tiers, the drift, the hallucination checks above. &lt;strong&gt;AgentLens&lt;/strong&gt; captures the &lt;em&gt;trace&lt;/em&gt; of how the agent got there: every model call and tool step, the resolved inputs, the raw outputs. Two halves of one workflow.&lt;/p&gt;

&lt;p&gt;The trace is what makes Tier 1 + Tier 2 possible in the first place — those tiers need trajectory data the agent didn't get to author, and AgentLens is where that unforgeable record lives. It's also what makes rot &lt;em&gt;diagnosable&lt;/em&gt;: when your meta-eval flags a stale fixture, the trace tells you whether the expected value was a real observation or a model's frozen guess, and which tool response it was pinned to. Without the trace you're re-guessing the oracle. With it, you're auditing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;An eval is a claim about the world, and claims expire. Rank your evidence by independence, not price: Tier 1 and Tier 2 hold up because their oracle is reality; Tier 3 and frozen golden strings are opinions with a shelf life. Gate on the durable tiers in real time, keep the judge offline and clearly labeled as opinion, and run a meta-eval that treats oracle freshness as a real signal. Pair &lt;code&gt;agent-eval&lt;/code&gt; for the scoring with AgentLens for the trace, and a red fixture stops being a mystery. Because the most dangerous eval isn't the one that fails. It's the one that passes against a world that no longer exists.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>evaluation</category>
      <category>observability</category>
    </item>
    <item>
      <title>Your Agent Passes Every Turn and Fails the Conversation</title>
      <dc:creator>Saurav Bhattacharya</dc:creator>
      <pubDate>Fri, 07 Aug 2026 01:02:16 +0000</pubDate>
      <link>https://dev.to/saurav_bhattacharya/your-agent-passes-every-turn-and-fails-the-conversation-1237</link>
      <guid>https://dev.to/saurav_bhattacharya/your-agent-passes-every-turn-and-fails-the-conversation-1237</guid>
      <description>&lt;p&gt;Your agent eval suite grades one turn at a time. Prompt in, output out, score it, move on. That model is fine for a completion endpoint. It is quietly wrong for anything that holds a conversation, and it is the reason your "94% pass rate" coexists with users who rage-quit on turn six.&lt;/p&gt;

&lt;p&gt;The unit of failure for a conversational agent is not the turn. It is the &lt;strong&gt;session&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The per-turn blind spot
&lt;/h2&gt;

&lt;p&gt;Consider a support agent. Turn 1: user asks about a refund. The agent answers correctly and passes every eval. Turn 4: user clarifies they mean a &lt;em&gt;partial&lt;/em&gt; refund. The agent answers that correctly too. Turn 6: the agent quotes the &lt;em&gt;original&lt;/em&gt; refund amount again, having silently dropped the "partial" constraint three turns ago.&lt;/p&gt;

&lt;p&gt;Every single turn passes an isolated eval. Each output is grounded, well-formatted, on-topic. The failure only exists &lt;em&gt;across&lt;/em&gt; turns: a dropped constraint, a contradiction with turn 4, a promise never kept. Per-turn grading is structurally blind to it, because the bug is in the relationship between outputs, not in any one of them.&lt;/p&gt;

&lt;p&gt;This is not an exotic edge case. Constraint decay, self-contradiction, and forgotten commitments are the dominant failure class for multi-turn agents. And they are invisible to the eval architecture most teams actually run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where session-level signal lives on the tier ladder
&lt;/h2&gt;

&lt;p&gt;If you have read anything I have written, you know I rank eval evidence on an &lt;strong&gt;independence&lt;/strong&gt; axis, not a cost axis, from evidence the agent cannot forge down to opinion it shares a substrate with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tier 1: externally observable proof the agent cannot fake.&lt;/strong&gt; Valid JSON, a file that exists, tests that passed, finished within timeout, non-empty.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tier 2: statistical signal vs a baseline the agent did not author.&lt;/strong&gt; Embedding similarity, repetition, whether a diff actually changed anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tier 3: model-as-judge.&lt;/strong&gt; Shared-substrate opinion. A signal, never a verdict.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The mistake teams make with multi-turn is assuming session evaluation is inherently a Tier 3 problem, "just ask a judge if the whole conversation was coherent." It is not. A huge fraction of session failures are &lt;strong&gt;Tier 1 and Tier 2&lt;/strong&gt;, if you look at the trajectory instead of the last message:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Did turn 6's amount &lt;strong&gt;contradict&lt;/strong&gt; a value the agent itself committed to in turn 4? That is a deterministic diff over structured extractions, Tier 1. No judge needed.&lt;/li&gt;
&lt;li&gt;Did a constraint the user set ("partial", "in EUR", "before Friday") &lt;strong&gt;survive&lt;/strong&gt; to the final output? Set-membership check over resolved slots, Tier 1/2.&lt;/li&gt;
&lt;li&gt;Did the agent &lt;strong&gt;repeat&lt;/strong&gt; an earlier answer verbatim instead of advancing? Repetition and embedding-similarity across turns, Tier 2.&lt;/li&gt;
&lt;li&gt;Did every user question get a corresponding answer, or did one get &lt;strong&gt;dropped&lt;/strong&gt;? Count and coverage check, Tier 1.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These run over the &lt;strong&gt;trajectory&lt;/strong&gt;, deterministically, at ~$0, fast enough to gate. And Tier 1+2 are &lt;em&gt;allowed&lt;/em&gt; to run over trajectories precisely because they do not share a substrate with the agent. Tier 3 cannot: a model judging another model's multi-turn reasoning is circular, because judge and judged share the same failure priors, so there is no independent ground truth. Reserve the judge for the genuinely subjective session tail ("was the tone appropriate across the escalation?"), offline, clearly labeled opinion.&lt;/p&gt;

&lt;h2&gt;
  
  
  You cannot check a trajectory you did not capture
&lt;/h2&gt;

&lt;p&gt;Here is the operational catch: every Tier 1/2 session check above needs the &lt;strong&gt;trace&lt;/strong&gt;, the resolved inputs, the committed values, the tool outputs at each step. Not a summary the agent wrote. The actual sequence.&lt;/p&gt;

&lt;p&gt;This is why the two halves ship as one workflow. &lt;strong&gt;AgentLens&lt;/strong&gt; captures the trace: every model and tool step, the resolved inputs, the raw outputs, unforgeable and agent-did-not-author records of what actually happened turn by turn. &lt;strong&gt;agent-eval&lt;/strong&gt; scores and gates against that trace using the tier doctrine above. The eval is only as trustworthy as the trace it runs on, and the trace is only useful if something scores it. Trace without eval is a debugging log; eval without trace is a judge guessing.&lt;/p&gt;

&lt;p&gt;Here is a Tier 1 session gate, contradiction detection over committed values, that needs zero model calls:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RefundCommitment&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;turn&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="na"&gt;amount&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="na"&gt;scope&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;full&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;partial&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

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

&lt;span class="c1"&gt;// Pulled from the AgentLens trace: what the agent actually committed to,&lt;/span&gt;
&lt;span class="c1"&gt;// per turn, not what it claims in its final summary.&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;evalSessionConsistency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;commitments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Commitment&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;failures&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="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;i&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="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;commitments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&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;prev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;commitments&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cur&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;commitments&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="c1"&gt;// A later turn silently reverting an earlier committed scope/amount&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;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scope&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scope&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;failures&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;`turn &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;turn&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: scope changed &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; `&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
        &lt;span class="s2"&gt;`but amount stayed &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (stale value?)`&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;tier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;passed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;failures&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;failures&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// deterministic, ~$0, safe to block the run&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;No judge scored that. It is a diff over trace data the agent could not author, and it catches the exact turn-6 bug that six green per-turn evals waved through.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ship the 80%
&lt;/h2&gt;

&lt;p&gt;The reflex is to throw a smart judge at multi-turn coherence. Resist it. Most session failures, dropped constraints, contradictions, repeated answers, unanswered questions, are caught deterministically at Tier 1+2 over the trajectory, at ~$0, fast enough to block before the bad turn ships. That is the 80%.&lt;/p&gt;

&lt;p&gt;Reserve the model-as-judge for the ~20% subjective tail, tone, empathy, whether an escalation &lt;em&gt;felt&lt;/em&gt; handled, offline, metered, and labeled for what it is: opinion, not evidence.&lt;/p&gt;

&lt;p&gt;Grade the session, not the sentence. Capture the trajectory with AgentLens, gate it with agent-eval, and stop letting a wall of green per-turn checks certify a conversation that fell apart on turn six.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>evaluation</category>
      <category>observability</category>
    </item>
    <item>
      <title>Your Agent Said It Worked. Go Check the World, Not the Sentence.</title>
      <dc:creator>Saurav Bhattacharya</dc:creator>
      <pubDate>Wed, 05 Aug 2026 01:02:59 +0000</pubDate>
      <link>https://dev.to/saurav_bhattacharya/your-agent-said-it-worked-go-check-the-world-not-the-sentence-1m2f</link>
      <guid>https://dev.to/saurav_bhattacharya/your-agent-said-it-worked-go-check-the-world-not-the-sentence-1m2f</guid>
      <description>&lt;p&gt;Your agent said it created the ticket. The eval passed. The ticket does not exist.&lt;/p&gt;

&lt;p&gt;This is the failure mode that action-taking agents introduce and that output-grading evals sail right past. When an agent's job is to &lt;em&gt;say&lt;/em&gt; something, grading the text is grading the job. When an agent's job is to &lt;em&gt;do&lt;/em&gt; something — write a file, open a PR, charge a card, send an email — the text it emits is a &lt;strong&gt;claim about a side effect&lt;/strong&gt;, not the side effect itself. An agent that has learned to produce confident, well-formatted "Done! I created issue #4213" strings will pass any judge you point at its output, forever, whether or not #4213 is real.&lt;/p&gt;

&lt;p&gt;If you take one thing from this post: &lt;strong&gt;for action agents, the artifact under eval is the world, not the sentence.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Claims are cheap, effects are load-bearing
&lt;/h2&gt;

&lt;p&gt;A model-as-judge reading "I created the ticket and assigned it to the on-call" has exactly zero independent information about whether a ticket exists. Judge and agent share a substrate; the judge is just a second language model agreeing that the sentence &lt;em&gt;sounds&lt;/em&gt; like success. That is circular, and it is the whole reason &lt;a href="https://www.npmjs.com/package/agent-eval" rel="noopener noreferrer"&gt;agent-eval&lt;/a&gt; ranks evidence on an &lt;strong&gt;independence axis&lt;/strong&gt; — independent to corruptible — rather than a cost axis of cheap to expensive.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tier 1 — externally observable proof the agent can't forge.&lt;/strong&gt; The ticket exists when you &lt;code&gt;GET /issues/4213&lt;/code&gt; and get a 200. The file exists on disk. The PR is open. The row is in the database. None of this can be hallucinated into being.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tier 2 — statistical signal vs a baseline the agent didn't author.&lt;/strong&gt; The created ticket's title actually embeds-similar to the task you gave it (not a real ticket for the wrong thing). The diff changed the file it claimed to change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tier 3 — model-as-judge.&lt;/strong&gt; Was the ticket &lt;em&gt;well-written&lt;/em&gt;? That's an opinion, a signal, never a verdict — and it only earns a seat after Tiers 1 and 2 have confirmed the ticket is real.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For side-effecting agents, Tier 1 is not the nice-to-have. It's the whole game. Most of your production failures — the API call 500'd, the write hit a read-only mount, the agent retried and created the ticket &lt;em&gt;twice&lt;/em&gt;, the "sent" email bounced — are caught here for ~$0, deterministically, fast enough to sit in the hot path and block the run. Tier 3 can't do any of that: it's offline-only, metered, and non-deterministic. You do not want a slow model opinion standing between your user and a retry.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify the effect, don't grade the claim
&lt;/h2&gt;

&lt;p&gt;Here's the shape of a Tier 1 side-effect check. Note what it does &lt;em&gt;not&lt;/em&gt; do: it never reads the agent's own summary of what happened.&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;EffectCheck&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;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="c1"&gt;// Observe the world independently. The agent does not get to write this.&lt;/span&gt;
  &lt;span class="nl"&gt;observe&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&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="c1"&gt;// Assert the observed state matches the task, not the agent's story.&lt;/span&gt;
  &lt;span class="nl"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;observed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&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;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;verifyEffect&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&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;check&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EffectCheck&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;observed&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;check&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observe&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;observed&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;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tier&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;pass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;check&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: effect not found in world`&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;check&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;observed&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;tier&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;pass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;check&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: effect exists but wrong shape`&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;tier&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;pass&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="c1"&gt;// The agent claimed it opened issue #4213 for the task "flaky login test".&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ticketExists&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;verifyEffect&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;github-issue&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;observe&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="nx"&gt;gh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;issues&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="nx"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;issue_number&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4213&lt;/span&gt; &lt;span class="p"&gt;})&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;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;issue&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;issue&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;open&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nf"&gt;embedSimilar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;issue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;flaky login test&lt;/span&gt;&lt;span class="dl"&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="mf"&gt;0.75&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Tier 2 riding along&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;observe&lt;/code&gt; function is the entire point. It calls GitHub, not the agent. A hallucinated issue number returns &lt;code&gt;null&lt;/code&gt; and fails at Tier 1 before any judge is ever invoked. This is how you "ship the 80%": stale, crashed, wrong-shape, and hallucinated-effect failures all die here, cheaply, leaving only the genuinely subjective tail — &lt;em&gt;is this a good ticket?&lt;/em&gt; — for the metered judge, clearly labeled opinion, not evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trace is what makes this debuggable
&lt;/h2&gt;

&lt;p&gt;There's a second problem hiding in the code above. When &lt;code&gt;verifyEffect&lt;/code&gt; returns &lt;code&gt;effect not found&lt;/code&gt;, &lt;em&gt;why&lt;/em&gt;? Did the agent call &lt;code&gt;issues.create&lt;/code&gt; and get rate-limited? Did it call the wrong endpoint? Did it call the right one, get a 201 back with issue #4299, and then hallucinate #4213 into its summary? The pass/fail tells you the world is wrong. It doesn't tell you where the agent went off the rails.&lt;/p&gt;

&lt;p&gt;That's what &lt;a href="https://www.npmjs.com/package/agentlens" rel="noopener noreferrer"&gt;AgentLens&lt;/a&gt; is for. It captures the &lt;strong&gt;trace&lt;/strong&gt; of how the agent got to its claim — every model step and tool call, the resolved inputs, and the raw outputs the agent actually received. So when Tier 1 goes red, you replay the trajectory: &lt;code&gt;issues.create&lt;/code&gt; returned 201 with &lt;code&gt;number: 4299&lt;/code&gt;, and the agent's final message said 4213. Now you know it's a summarization bug, not a permissions bug, and you fix the right thing.&lt;/p&gt;

&lt;p&gt;The pairing runs deeper than debugging, though. Tier 1 and Tier 2 need something to score &lt;em&gt;against&lt;/em&gt;, and it has to be data the agent didn't author. The trace is exactly that. The raw &lt;code&gt;201&lt;/code&gt; response body sitting in AgentLens is unforgeable ground truth: agent-eval reads the real returned issue number from the trace and compares it to what the agent claimed, and the lie surfaces instantly. agent-eval scores and gates the output; AgentLens captures the trajectory that makes the score both debuggable and trustworthy. They're two halves of one loop — you can't do independent evals on trajectory data you didn't independently capture.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule
&lt;/h2&gt;

&lt;p&gt;Stop asking "did the agent say it worked?" Ask "did the world change the way the task required?" For action agents those are different questions, and only one of them is load-bearing. Verify the effect at Tier 1, corroborate its shape at Tier 2, and reserve the judge for the 20% where taste actually matters — with the trace underneath so a red gate points you at a fix instead of a shrug.&lt;/p&gt;

&lt;p&gt;Your agent's summary is a hypothesis. Go check.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>evaluation</category>
      <category>observability</category>
    </item>
    <item>
      <title>Your New Eval Rule Is Untested Code Guarding Production</title>
      <dc:creator>Saurav Bhattacharya</dc:creator>
      <pubDate>Sun, 02 Aug 2026 01:01:44 +0000</pubDate>
      <link>https://dev.to/saurav_bhattacharya/your-new-eval-rule-is-untested-code-guarding-production-2a1p</link>
      <guid>https://dev.to/saurav_bhattacharya/your-new-eval-rule-is-untested-code-guarding-production-2a1p</guid>
      <description>&lt;p&gt;You wrote a new eval. It caught the failure you just saw in production. You shipped it as a gate. Congratulations — you now have a piece of untested code sitting on the hot path of every agent run, deciding what ships and what gets blocked.&lt;/p&gt;

&lt;p&gt;We treat agent evals as if writing them is the hard part. It isn't. The hard part is knowing your eval actually discriminates: that it goes red on the bad traces and green on the good ones, and not the other way around. An eval you haven't run against known-labeled traces is a coin flip with a dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure nobody names: the false-negative gate
&lt;/h2&gt;

&lt;p&gt;Here's the quiet disaster. You add a rule to catch hallucinated file paths. It has a regex bug. It matches nothing. Every run passes. Your dashboard is green. You feel safe. Three weeks later a customer finds the exact failure your "gate" was supposed to block, and you discover the rule never fired once.&lt;/p&gt;

&lt;p&gt;A green eval is not evidence of a healthy agent. It's evidence that &lt;em&gt;either&lt;/em&gt; the agent is healthy &lt;em&gt;or&lt;/em&gt; your eval is broken — and you have no way to tell those apart unless you've fed it a trace you already know is bad and watched it go red.&lt;/p&gt;

&lt;p&gt;Evals are code. Code that guards production gets tested against fixtures. Somehow evals got a pass on this, and it's the single biggest source of false confidence I see.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the tiers actually help you here
&lt;/h2&gt;

&lt;p&gt;The reason this matters so much is tied to how you should be ranking eval evidence in the first place. Not on a cost axis — cheap-to-expensive is the wrong mental model. Rank it on an &lt;strong&gt;independence axis&lt;/strong&gt;: how forgeable is the signal by the agent producing the output?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tier 1 — externally observable proof the agent can't fake.&lt;/strong&gt; Valid JSON, the file exists on disk, it compiled, tests passed, it finished before the timeout, the output isn't empty. Ground truth, no opinion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tier 2 — statistical signal against a baseline the agent didn't author.&lt;/strong&gt; Embedding similarity between output and the task spec, length and repetition checks, did the diff actually change anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tier 3 — model-as-judge.&lt;/strong&gt; A shared-substrate &lt;em&gt;opinion&lt;/em&gt;. A signal, never a verdict.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This ranking is exactly what makes your eval suite &lt;em&gt;testable&lt;/em&gt;. Tier 1 and Tier 2 are deterministic, run at roughly zero cost, and are fast — which is why they're your &lt;strong&gt;real-time gate&lt;/strong&gt;: they can sit in the hot path and block a run. Because they're deterministic, you can pin them against a fixed corpus of traces and get the same answer every time. That's what "testing your eval" even means.&lt;/p&gt;

&lt;p&gt;Tier 3 can't do that. It's non-deterministic, metered, and slow, so it's &lt;strong&gt;offline-only&lt;/strong&gt; — it can't live in the hot path, and it can't be regression-tested in the same clean way because it won't give you a stable answer twice. There's a deeper problem too: a model judging another model's reasoning is &lt;strong&gt;circular&lt;/strong&gt;. Judge and judged share a substrate; there's no independent ground truth. So Tier 3 is only allowed to inspect artifacts the judged agent didn't get to write, and even then it's "opinion, not evidence." You don't gate on it and you don't pretend you can unit-test it into reliability.&lt;/p&gt;

&lt;p&gt;The practical upshot: &lt;strong&gt;ship the 80%.&lt;/strong&gt; Most real failures — stale output, a crash, malformed format, a hallucinated path, an empty result — are all caught at Tier 1+2 alone, deterministically, for free. Reserve the judge for the ~20% subjective tail and label it loudly as opinion. And it's precisely the Tier 1+2 rules — the deterministic ones — that you can and must test before trusting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing an eval like it's code
&lt;/h2&gt;

&lt;p&gt;The move is boring and it works: keep a corpus of labeled traces, and assert that each eval produces the label you expect. A new rule doesn't ship until it goes red on the known-bad and green on the known-good.&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;Verdict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pass&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;fail&lt;/span&gt;&lt;span class="dl"&gt;"&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;Trace&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;output&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;claimedPaths&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;existsOnDisk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// A Tier 1 eval: no hallucinated file paths. Deterministic, ~$0.&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;noHallucinatedPaths&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Trace&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Verdict&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;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;claimedPaths&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;every&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;p&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;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;existsOnDisk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&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;pass&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;fail&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="c1"&gt;// The fixtures are the point. Each is a trace you ALREADY labeled.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fixtures&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Trace&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Verdict&lt;/span&gt; &lt;span class="p"&gt;}[]&lt;/span&gt; &lt;span class="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;trace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;good-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;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wrote src/index.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;claimedPaths&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;src/index.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;existsOnDisk&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="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;expected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pass&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bad-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;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;edited src/does-not-exist.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;claimedPaths&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;src/does-not-exist.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;existsOnDisk&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="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="na"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fail&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// if this comes back "pass", your GATE is broken&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expected&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;fixtures&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;got&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;noHallucinatedPaths&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;got&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`Eval regression on &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: expected &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, got &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;got&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;bad-1&lt;/code&gt; fixture is the whole game. Without it, a regex typo or an inverted condition ships silently and your gate becomes decoration. With it, a broken eval fails &lt;em&gt;your&lt;/em&gt; CI instead of failing your customer.&lt;/p&gt;

&lt;h2&gt;
  
  
  This only works if your traces are trustworthy
&lt;/h2&gt;

&lt;p&gt;Everything above assumes you have real traces with real, unforgeable inputs to score against — and that's exactly where most teams fall down. If your "trace" is a summary the agent wrote about its own run, you've handed the graded student the answer key.&lt;/p&gt;

&lt;p&gt;This is why the two halves ship as a unit. &lt;strong&gt;agent-eval&lt;/strong&gt; scores and gates the agent's output — it's where the tier doctrine lives, where drift and hallucination checks run, where a red gate blocks a run. But it can only score what it can see. &lt;strong&gt;AgentLens&lt;/strong&gt; captures the &lt;em&gt;trace&lt;/em&gt; of how the agent got there: every model and tool step, the resolved inputs, the raw outputs, none of it authored after the fact by the agent to look good. That trace data is what gives Tier 1+2 something unforgeable to grade against — and it's what lets you build a labeled corpus in the first place, because you're mining real runs instead of inventing fixtures from imagination.&lt;/p&gt;

&lt;p&gt;agent-eval tells you the run failed. AgentLens lets you reopen it, see the exact step where it went wrong, label it, and drop it into your fixture set so the eval that missed it can never miss it again. One captures the trace; the other scores it. Neither is useful alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-line policy
&lt;/h2&gt;

&lt;p&gt;Write it on the wall: &lt;strong&gt;no eval reaches the gate without a red fixture.&lt;/strong&gt; If you can't produce a trace that makes the rule fail, you haven't written an eval — you've written a comment that happens to return &lt;code&gt;"pass"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Test your evals against known-bad traces before you trust them to guard anything. Deterministic Tier 1+2 rules make that testing possible; capture real traces so the fixtures are real; keep the judge offline and honest about being an opinion. That's the difference between a gate and a green light nobody's checked.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>evaluation</category>
      <category>testing</category>
    </item>
    <item>
      <title>Your Agent's Deadline Is a Correctness Test, Not an SLO</title>
      <dc:creator>Saurav Bhattacharya</dc:creator>
      <pubDate>Fri, 31 Jul 2026 01:02:28 +0000</pubDate>
      <link>https://dev.to/saurav_bhattacharya/your-agents-deadline-is-a-correctness-test-not-an-slo-56g1</link>
      <guid>https://dev.to/saurav_bhattacharya/your-agents-deadline-is-a-correctness-test-not-an-slo-56g1</guid>
      <description>&lt;p&gt;Ask an engineer to list their agent's failure modes and you'll hear about hallucinations, wrong tool calls, and bad JSON. Ask about &lt;em&gt;time&lt;/em&gt; and you get a shrug. Yet the single most common thing a production agent does when it goes wrong is not fail loudly — it just &lt;strong&gt;takes too long&lt;/strong&gt;. It loops. It retries a flaky tool. It waits on a model call that never streams a first token. And your eval suite, which runs after the fact on whatever output eventually showed up, grades it green.&lt;/p&gt;

&lt;p&gt;This is the blind spot: we treat latency as an SRE dashboard concern, divorced from correctness. But for an agent, a deadline miss &lt;em&gt;is&lt;/em&gt; a correctness failure. A summary that arrives 90 seconds late is often worse than no summary — the user already left, the downstream job already timed out, the retry already double-charged them. Time belongs in your evals, and it belongs at the very bottom of the stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  Time is Tier 1 evidence
&lt;/h2&gt;

&lt;p&gt;If you've followed the tier doctrine behind &lt;a href="https://github.com/agent-eval" rel="noopener noreferrer"&gt;agent-eval&lt;/a&gt;, you know evidence ranks on an &lt;strong&gt;independence&lt;/strong&gt; axis — from evidence the agent can't forge, to opinion it shares a substrate with — not a cost axis. Three tiers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tier 1&lt;/strong&gt; — externally observable proof the agent can't fake: valid JSON, the file exists, the code compiled, tests passed, it &lt;strong&gt;finished within the deadline&lt;/strong&gt;, the output is non-empty.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tier 2&lt;/strong&gt; — statistical signal against a baseline the agent didn't author: embedding similarity to the task, length and repetition, whether the diff actually changed anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tier 3&lt;/strong&gt; — model-as-judge: a shared-substrate opinion. A signal, never a verdict.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Notice where "finished within the timeout" sits. It's Tier 1, right next to "valid JSON." That's not an accident. A wall-clock deadline is the most independent signal you have — the agent cannot argue with a stopwatch, cannot reason its way past it, cannot forge it. Physics grades this one. There is no more incorruptible ground truth in your whole eval suite than "the clock ran out."&lt;/p&gt;

&lt;p&gt;And crucially, Tier 1+2 are your &lt;strong&gt;real-time gate&lt;/strong&gt;: deterministic, ~$0, fast enough to sit in the hot path and &lt;em&gt;block a run&lt;/em&gt;. A timeout check is the purest expression of that — it's already in the hot path by definition. Tier 3, the judge, is the opposite: metered, slow, non-deterministic, offline-only. You would never put a model-as-judge on the critical path to decide whether a response was fast enough. The judge can't even see the clock.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "graded green" actually looks like
&lt;/h2&gt;

&lt;p&gt;Here's the trap. Most eval harnesses receive &lt;code&gt;(input, output)&lt;/code&gt; and score the output. The &lt;code&gt;output&lt;/code&gt; is whatever the agent finally returned — so by construction, timing information has already been discarded before grading begins. The eval literally cannot see that the run blew its deadline, because it only exists once the run is over.&lt;/p&gt;

&lt;p&gt;You have to grade the &lt;em&gt;run&lt;/em&gt;, not the &lt;em&gt;return value&lt;/em&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="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;RunResult&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&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;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ok&lt;/span&gt;&lt;span class="dl"&gt;"&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;T&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;elapsedMs&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;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;timeout&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;elapsedMs&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;deadlineMs&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;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;elapsedMs&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;error&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;withDeadline&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&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;work&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AbortSignal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&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;deadlineMs&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;RunResult&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;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;ctrl&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;AbortController&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="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&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;ctrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;deadlineMs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;value&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;work&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ok&lt;/span&gt;&lt;span class="dl"&gt;"&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="na"&gt;elapsedMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;started&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&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;elapsedMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;started&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;ctrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aborted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;timeout&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;elapsedMs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;deadlineMs&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;elapsedMs&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="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&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;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timer&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 the eval gate is trivial and deterministic — a Tier 1 check that runs in microseconds:&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;tier1TimingGate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RunResult&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&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="nl"&gt;pass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;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;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;timeout&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;pass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`deadline miss: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;elapsedMs&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;ms &amp;gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;deadlineMs&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;ms`&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&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;pass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`crashed after &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;elapsedMs&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;ms`&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;pass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A timed-out run never reaches Tier 2 or 3. There's nothing to embed, nothing to judge — the correct answer that arrives too late is not a correct answer. You short-circuit, you block the run, you fall back. No model was asked for its opinion, because no opinion was needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deadlines are per-budget, not per-agent
&lt;/h2&gt;

&lt;p&gt;The mistake I see next is a single global timeout. Deadlines are contextual: a background reindex job can take ten minutes; an interactive chat turn has maybe three seconds before the user perceives a stall. The deadline is a property of the &lt;em&gt;call site&lt;/em&gt;, not the agent. Thread it through as part of the task contract, and let every tool step inherit and decrement a shared budget — so a tool that eats 80% of the budget leaves the model no room to actually respond, and &lt;em&gt;that&lt;/em&gt; is a gradeable event too.&lt;/p&gt;

&lt;h2&gt;
  
  
  You can't grade time you didn't record
&lt;/h2&gt;

&lt;p&gt;All of this assumes you actually captured elapsed time per step — and here's where the eval half of the story needs its other half. agent-eval scores and gates the output; it can only enforce a Tier 1 timing gate if something recorded the timing. That's the job of trace capture. &lt;a href="https://github.com/agent-eval" rel="noopener noreferrer"&gt;AgentLens&lt;/a&gt; instruments the agent's trajectory — every model call and tool step, with resolved inputs, raw outputs, and start/stop timestamps — so "finished within the deadline" is a fact you can read off an unforgeable trace the agent didn't get to author, not a number you hope somebody logged.&lt;/p&gt;

&lt;p&gt;This pairing is the whole point. The trace is the substrate Tier 1+2 grade against: because AgentLens records when each step started and stopped independently of what the agent &lt;em&gt;claims&lt;/em&gt; it did, your timing gate has real ground truth. An agent can hallucinate that it "responded quickly." It cannot edit the timestamps in a trace it didn't write. That's the difference between an eval that catches the 80% of boring failures — stale, crashed, empty, &lt;em&gt;too slow&lt;/em&gt; — deterministically at Tier 1+2, and a dashboard of green checkmarks that quietly hides every run that limped across the finish line a minute late.&lt;/p&gt;

&lt;p&gt;Reserve the judge for the subjective 20% — tone, helpfulness, "did this actually answer the question" — and label it opinion, not evidence. But the stopwatch? That's evidence. Put it first.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>evaluation</category>
      <category>observability</category>
    </item>
    <item>
      <title>Your Agent's Confidence Score Is Not a Probability</title>
      <dc:creator>Saurav Bhattacharya</dc:creator>
      <pubDate>Wed, 29 Jul 2026 01:02:15 +0000</pubDate>
      <link>https://dev.to/saurav_bhattacharya/your-agents-confidence-score-is-not-a-probability-1jd8</link>
      <guid>https://dev.to/saurav_bhattacharya/your-agents-confidence-score-is-not-a-probability-1jd8</guid>
      <description>&lt;p&gt;Ask an agent how sure it is and it will happily tell you. "Confidence: 0.92." It looks like a probability. It renders nicely in a dashboard. Teams wire it into routing logic: high confidence, auto-approve; low confidence, send to a human. It feels rigorous.&lt;/p&gt;

&lt;p&gt;It is not rigorous. A self-reported confidence score is the agent grading its own homework, and it is one of the most seductive false signals in production agentic systems. If you are gating anything on it, you are trusting the defendant's opinion of their own alibi.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the number actually comes from
&lt;/h2&gt;

&lt;p&gt;When an LLM emits &lt;code&gt;confidence: 0.92&lt;/code&gt;, that number is not a calibrated posterior. It is another token sequence, generated by the same forward pass that produced the answer you are unsure about. It shares a substrate with the output it is describing. If the model hallucinated a file path, the same weights that invented the path will cheerfully assign it 0.9 confidence, because from the inside, a confident fabrication and a confident fact are indistinguishable.&lt;/p&gt;

&lt;p&gt;This is not a prompt-engineering problem you can fix with "be honest about your uncertainty." You can push the distribution around, but you cannot make a model's self-report into independent evidence, because there is no independent ground truth in the loop. The grader and the graded are the same network.&lt;/p&gt;

&lt;h2&gt;
  
  
  The independence axis, applied
&lt;/h2&gt;

&lt;p&gt;This is exactly why, when we build evals at &lt;a href="https://github.com/" rel="noopener noreferrer"&gt;agent-eval&lt;/a&gt;, we rank evidence on an &lt;strong&gt;independence axis&lt;/strong&gt; — independent to corruptible — not a cost axis of cheap to expensive. Three tiers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tier 1 — externally observable proof the agent can't forge.&lt;/strong&gt; The JSON parses. The file exists on disk. The code compiled. The tests passed. The call returned within the timeout. The output is non-empty. None of this can be faked by a confident model, because you are checking reality, not asking an opinion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tier 2 — statistical signal against a baseline the agent didn't author.&lt;/strong&gt; Embedding similarity between the output and the actual task. Length and repetition checks. Did the diff actually change anything. The agent didn't write the baseline, so it can't game the comparison.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tier 3 — model-as-judge.&lt;/strong&gt; A shared-substrate opinion. A signal, never a verdict.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A self-reported confidence score is not even Tier 3. Tier 3 at least lets a &lt;em&gt;separate&lt;/em&gt; model inspect an artifact. Self-confidence is the judged model judging itself in the same breath — maximally circular. It belongs at the very corruptible end of the axis.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two facts that fall out of this
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Tier 1+2 are the real-time gate.&lt;/strong&gt; They are deterministic, roughly free, and fast, so they can sit in the hot path and block a bad run before it ships. Tier 3 is offline-only: metered, slow, non-deterministic. A judge cannot live in your latency budget. And self-reported confidence, despite &lt;em&gt;looking&lt;/em&gt; cheap enough to gate on, is corruptible enough that gating on it is worse than gating on nothing — because it gives you false comfort.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A model judging another model's reasoning is circular.&lt;/strong&gt; Tier 1+2 can run over agent trajectories. Tier 3 cannot — if you let a judge grade the reasoning steps, judge and judged share a substrate and there is no independent ground truth. So Tier 3 may only inspect artifacts the judged agent didn't get to write. Self-confidence violates this rule harder than anything else: it is a claim &lt;em&gt;about&lt;/em&gt; the reasoning, authored &lt;em&gt;by&lt;/em&gt; the reasoner.&lt;/p&gt;

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

&lt;p&gt;Don't route on the agent's opinion of itself. Route on independent checks. Here is the shape of it in TypeScript:&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;AgentOutput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;answer&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;filePath&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;selfConfidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// present, and deliberately ignored for gating&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;GateResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;pass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;gate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AgentOutput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;GateResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Tier 1: unforgeable proof&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;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;pass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;tier&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;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;empty&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fileExists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filePath&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;pass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;tier&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;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hallucinated path&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="c1"&gt;// Tier 2: statistical signal vs a baseline the agent didn't author&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sim&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;cosineSim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sim&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.55&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;pass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;tier&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;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`off-task (sim=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;sim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="c1"&gt;// out.selfConfidence is never consulted. It's the defendant's alibi.&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;pass&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;tier&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;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cleared independent checks&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice &lt;code&gt;selfConfidence&lt;/code&gt; is present in the type and never read in the gate. That is the point. You can &lt;em&gt;log&lt;/em&gt; it, correlate it against outcomes offline, even discover it's anti-correlated with correctness — but it does not get a vote in the hot path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ship the 80%, then let a judge whisper about the rest
&lt;/h2&gt;

&lt;p&gt;Most production failures are boring and mechanical: stale data, a crash, malformed output, a hallucinated path, an empty result. Every one of those is caught at Tier 1+2 alone, deterministically, for about zero dollars. Reserve the model-as-judge for the roughly 20% subjective tail — tone, helpfulness, whether an explanation is actually clear — and label its output honestly as "opinion, not evidence." A judge that says 7/10 is a suggestion for a human, not a gate.&lt;/p&gt;

&lt;h2&gt;
  
  
  You can't gate what you can't see
&lt;/h2&gt;

&lt;p&gt;All of this assumes you can actually inspect what the agent did — the real inputs after resolution, the real tool outputs, the real intermediate steps. That is the other half of the workflow. &lt;a href="https://github.com/" rel="noopener noreferrer"&gt;AgentLens&lt;/a&gt; captures the &lt;strong&gt;trace&lt;/strong&gt;: every model and tool step, resolved inputs, raw outputs. agent-eval scores and gates the &lt;strong&gt;output&lt;/strong&gt;; AgentLens gives you the unforgeable, agent-didn't-author trace data for Tier 1+2 to score against — and the debugging surface for when a gate goes red and you need to know &lt;em&gt;why&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The pairing matters here specifically because the whole failure mode of this post is trusting the agent's narration of itself. AgentLens replaces the narration with the record. agent-eval judges the record, not the narration.&lt;/p&gt;

&lt;p&gt;Self-reported confidence is the narration. Stop routing on it. Route on proof.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>evaluation</category>
      <category>observability</category>
    </item>
    <item>
      <title>Right Tool, Wrong Arguments: The Agent Failure Your Evals Wave Through</title>
      <dc:creator>Saurav Bhattacharya</dc:creator>
      <pubDate>Sun, 26 Jul 2026 01:02:24 +0000</pubDate>
      <link>https://dev.to/saurav_bhattacharya/right-tool-wrong-arguments-the-agent-failure-your-evals-wave-through-2lf0</link>
      <guid>https://dev.to/saurav_bhattacharya/right-tool-wrong-arguments-the-agent-failure-your-evals-wave-through-2lf0</guid>
      <description>&lt;p&gt;Your agent picked the correct tool. It routed to &lt;code&gt;refund_order&lt;/code&gt; when the user asked for a refund. Your eval suite went green. And then it issued a $4,200 refund on order &lt;code&gt;#0&lt;/code&gt; because the argument extraction fumbled and defaulted the ID to zero.&lt;/p&gt;

&lt;p&gt;This is the failure mode nobody instruments: &lt;strong&gt;right tool, wrong arguments.&lt;/strong&gt; Most eval setups check whether the agent &lt;em&gt;chose&lt;/em&gt; the correct action, then wave the arguments through because validating them looks tedious. That gap is where real money leaks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tool selection is the easy 20%
&lt;/h2&gt;

&lt;p&gt;Routing to the right tool is a classification problem, and modern models are good at it. The hard, dangerous part is the &lt;em&gt;arguments&lt;/em&gt; — the resolved IDs, amounts, filters, and paths the agent synthesizes from messy context. Those are structured claims the agent authored, and they fail in boring, expensive ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A refund amount pulled from the wrong line item.&lt;/li&gt;
&lt;li&gt;A file path hallucinated from a plausible-looking directory that doesn't exist.&lt;/li&gt;
&lt;li&gt;A date filter off by a timezone, silently returning an empty set that reads as "no results."&lt;/li&gt;
&lt;li&gt;An &lt;code&gt;order_id&lt;/code&gt; that got coerced to &lt;code&gt;0&lt;/code&gt; or &lt;code&gt;null&lt;/code&gt; and still satisfied a loose schema.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are subjective. None of them need a model to grade them. They are &lt;strong&gt;Tier 1 problems&lt;/strong&gt; — externally observable proof the agent can't forge — and they're getting handed to a model-as-judge (or worse, to production) because teams conflate "did it call the right tool" with "did it call the tool right."&lt;/p&gt;

&lt;h2&gt;
  
  
  The independence axis, applied to tool calls
&lt;/h2&gt;

&lt;p&gt;The tier doctrine agent-eval is built on ranks evidence by &lt;strong&gt;independence&lt;/strong&gt;, not cost:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tier 1 — proof the agent can't forge.&lt;/strong&gt; The arguments parse as valid JSON, satisfy the tool's schema, the referenced &lt;code&gt;order_id&lt;/code&gt; actually exists, the amount is within the order total, the path resolves. Deterministic, ~$0, runs in the hot path, &lt;em&gt;can block the call before it executes.&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tier 2 — statistical signal against a baseline the agent didn't author.&lt;/strong&gt; The extracted amount is within a sane distribution for this merchant; the argument set actually changed from the previous turn; the resolved entity embeds close to the entity named in the user's request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tier 3 — model-as-judge.&lt;/strong&gt; "Given the conversation, does this refund &lt;em&gt;feel&lt;/em&gt; justified?" A signal, never a verdict, and &lt;strong&gt;offline only&lt;/strong&gt; — metered, slow, non-deterministic, no business sitting in your payment path.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The mistake is asking Tier 3 to do Tier 1's job. A judge model evaluating whether &lt;code&gt;order_id: 0&lt;/code&gt; is correct is circular reasoning: it shares a substrate with the agent that produced the argument, and it has no independent ground truth about your database. Whether order &lt;code&gt;0&lt;/code&gt; exists is not an opinion. &lt;strong&gt;Query the database.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Gate the arguments before the tool fires
&lt;/h2&gt;

&lt;p&gt;Tier 1 for tool calls is a validation layer that sits between the model's proposed call and execution. Here it is against a proposed refund:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RefundArgs&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;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;\d&lt;/span&gt;&lt;span class="sr"&gt;+$/&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;amountCents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;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;type&lt;/span&gt; &lt;span class="nx"&gt;ToolCall&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;gateRefund&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ToolCall&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OrderStore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Tier 1a: does it even parse to the contract?&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;RefundArgs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;safeParse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="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;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&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;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;tier&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;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;args_schema_invalid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;amountCents&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Tier 1b: does the referenced entity actually exist?&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;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;order&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;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;tier&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;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;order_not_found&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="c1"&gt;// Tier 1c: is the amount observably impossible?&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;amountCents&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;totalCents&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;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;tier&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;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;amount_exceeds_total&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="c1"&gt;// Tier 2: statistically weird, even if legal?&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;amountCents&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;merchant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;medianRefundCents&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;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;tier&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;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;amount_outlier&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;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every check here is proof or statistics, not opinion. Schema, existence, bounds, distribution. The agent cannot talk its way past &lt;code&gt;order_not_found&lt;/code&gt;, because the check consults a source the agent didn't get to write. That's the whole point of the independence axis: &lt;strong&gt;the gate is only worth anything if the agent couldn't author the evidence it's judged against.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the 80% you ship first. Malformed args, hallucinated IDs, empty result sets, out-of-bounds amounts — the failures that actually page you at 2am — are all caught at Tier 1+2, deterministically, before execution. The subjective ~20% ("was a refund the &lt;em&gt;right call&lt;/em&gt; socially?") is the only thing you route to a judge, clearly labeled opinion.&lt;/p&gt;

&lt;h2&gt;
  
  
  You can't gate what you can't see
&lt;/h2&gt;

&lt;p&gt;This all assumes you have the &lt;em&gt;resolved&lt;/em&gt; arguments — what the tool actually received after the model's output was parsed, defaulted, and coerced. Most logging captures the user prompt and the final response and nothing in between. When the refund fires on order &lt;code&gt;0&lt;/code&gt;, your logs show a happy path.&lt;/p&gt;

&lt;p&gt;This is where the two halves of the workflow lock together. &lt;strong&gt;AgentLens&lt;/strong&gt; captures the trace: every model step and tool step, the &lt;em&gt;resolved&lt;/em&gt; inputs the tool actually saw, and the raw outputs — unforged, agent-didn't-author records of what happened. &lt;strong&gt;agent-eval&lt;/strong&gt; scores and gates against that trace using the tier doctrine above. AgentLens gives Tier 1+2 something real to validate; agent-eval turns it into a red/green decision that can block the call.&lt;/p&gt;

&lt;p&gt;Without the trace, your evals are grading the story the agent tells about itself. With it, you're grading the arguments it actually passed — and stopping the &lt;code&gt;$4,200&lt;/code&gt; refund on order &lt;code&gt;0&lt;/code&gt; &lt;em&gt;before&lt;/em&gt; it clears.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Stop congratulating your agent for picking the right tool. Tool selection is classification; argument synthesis is where the risk lives. Validate the arguments as proof, not opinion — schema, existence, bounds — in the hot path, before execution. Reserve the judge for the genuinely subjective tail. And trace the resolved inputs, because you cannot gate what you never recorded.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>evaluation</category>
      <category>observability</category>
    </item>
    <item>
      <title>Stop Leaving Findings in the Judge: The Ratchet That Turns Opinions Into Gates</title>
      <dc:creator>Saurav Bhattacharya</dc:creator>
      <pubDate>Fri, 24 Jul 2026 01:02:25 +0000</pubDate>
      <link>https://dev.to/saurav_bhattacharya/stop-leaving-findings-in-the-judge-the-ratchet-that-turns-opinions-into-gates-4634</link>
      <guid>https://dev.to/saurav_bhattacharya/stop-leaving-findings-in-the-judge-the-ratchet-that-turns-opinions-into-gates-4634</guid>
      <description>&lt;p&gt;When a model-as-judge flags something real, most teams do the worst possible thing: they leave it in the judge. The finding lives on forever as a slow, metered, non-deterministic Tier 3 opinion — re-litigated on every run, at cost, with a different verdict each time. That's not an eval strategy. That's paying rent on a bug you already found.&lt;/p&gt;

&lt;p&gt;The senior move is a &lt;strong&gt;ratchet&lt;/strong&gt;: every recurring Tier 3 finding is a candidate to be promoted into a Tier 1 or Tier 2 check, where it becomes free, fast, deterministic, and able to block the run. This post is about how that ratchet works, and why it's the whole point of having a judge at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The independence axis, briefly
&lt;/h2&gt;

&lt;p&gt;Before promotion makes sense, you have to rank evidence the right way. &lt;code&gt;agent-eval&lt;/code&gt; ranks it on an &lt;strong&gt;independence axis&lt;/strong&gt; — from independent to corruptible — not a cost axis:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tier 1 — proof the agent can't forge.&lt;/strong&gt; Valid JSON, the file exists, it compiled, tests passed, it finished inside the timeout, the output is non-empty.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tier 2 — statistical signal vs a baseline the agent didn't author.&lt;/strong&gt; Embedding similarity to the task spec, length and repetition profiles, whether the diff actually changed anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tier 3 — model-as-judge.&lt;/strong&gt; A shared-substrate opinion. A signal, never a verdict.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two properties fall out of this and they drive everything below. Tier 1+2 are the &lt;strong&gt;real-time gate&lt;/strong&gt;: deterministic, ~$0, fast, so they can sit in the hot path and block a run. Tier 3 is &lt;strong&gt;offline-only&lt;/strong&gt;: metered, slow, non-deterministic, so it can't. And Tier 1+2 can legitimately run over agent &lt;em&gt;trajectories&lt;/em&gt;, while Tier 3 cannot — a model judging another model's reasoning is circular, because judge and judged share a substrate and there's no independent ground truth. So Tier 3 only gets to inspect artifacts the judged agent didn't write.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ratchet: promote findings down the tiers
&lt;/h2&gt;

&lt;p&gt;Here's the pattern. Your judge keeps flagging the same class of failure — say, "the summary references a section that doesn't exist in the source." That's a real defect. But it's expensive to catch this way, and the verdict wobbles.&lt;/p&gt;

&lt;p&gt;Ask: &lt;strong&gt;what unforgeable fact would have caught this?&lt;/strong&gt; In this case, "every cited section header appears verbatim in the source" is a string-membership check. That's Tier 1. You just promoted a subjective opinion into a deterministic gate.&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;Finding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;claim&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;// the judge's recurring complaint&lt;/span&gt;
  &lt;span class="nl"&gt;sourceText&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;// artifact the agent did NOT author&lt;/span&gt;
  &lt;span class="nl"&gt;citedSections&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="c1"&gt;// Tier 1 promotion: the finding is now an unforgeable-proof check.&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;citationsExist&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Finding&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;pass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;missing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;citedSections&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sourceText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&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;pass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="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;missing&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Tier 2 promotion: when membership is too rigid, drop to a&lt;/span&gt;
&lt;span class="c1"&gt;// baseline-relative signal the agent didn't get to author.&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;citationGroundedness&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Finding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="nx"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;b&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;=&amp;gt;&lt;/span&gt; &lt;span class="kr"&gt;number&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sourceText&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;scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;citedSections&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;src&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;scores&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&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;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scores&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;1&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;citationsExist&lt;/code&gt; is Tier 1: it's a fact about text the agent can't forge. When exact membership is too brittle — paraphrased citations, translated sources — you don't jump back to the judge, you drop to &lt;strong&gt;Tier 2&lt;/strong&gt;: &lt;code&gt;citationGroundedness&lt;/code&gt; scores similarity against a baseline (the source) the agent didn't write. Still deterministic. Still ~$0. Still allowed in the hot path. Only if &lt;em&gt;neither&lt;/em&gt; tier can express the property do you leave it with the judge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ship the 80%, meter the 20%
&lt;/h2&gt;

&lt;p&gt;This is where the ratchet pays off. Most failures in production are not subtle: stale outputs, crashes, malformed JSON, hallucinated file paths, empty responses. &lt;strong&gt;Every one of those is catchable at Tier 1+2 alone&lt;/strong&gt; — for free, in the hot path, blocking the run before a user ever sees it.&lt;/p&gt;

&lt;p&gt;The judge is for the ~20% subjective tail: tone, whether an explanation is actually helpful, whether a refactor is &lt;em&gt;tasteful&lt;/em&gt;. That work is real, but you run it offline, metered, and you label its output honestly: &lt;strong&gt;opinion, not evidence.&lt;/strong&gt; The ratchet steadily shrinks that 20% over time, because every recurring judge complaint that &lt;em&gt;can&lt;/em&gt; be expressed as a fact eventually gets promoted out.&lt;/p&gt;

&lt;p&gt;This is the line that separates a real eval layer from an "LLM-as-judge gives you a 7/10" tool. A 7/10 is not a gate and it's not reproducible. A promoted Tier 1 check is both.&lt;/p&gt;

&lt;h2&gt;
  
  
  You can't promote what you can't see
&lt;/h2&gt;

&lt;p&gt;The ratchet has a hard dependency: to promote a finding, you need the raw material — the exact source text, the resolved tool inputs, the actual output the agent produced. This is where the two halves of the workflow lock together.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;agent-eval&lt;/code&gt; scores and gates the agent's &lt;strong&gt;output&lt;/strong&gt; — the tiers, the drift checks, the hallucination checks above. But it can only score against unforgeable data if that data was captured faithfully. That's &lt;strong&gt;AgentLens&lt;/strong&gt;: it captures the &lt;strong&gt;trace&lt;/strong&gt; of how the agent got there — every model and tool step, the resolved inputs, the raw outputs. Two things follow. First, when a Tier 3 opinion recurs, you open the trace, find the exact artifact the agent &lt;em&gt;didn't&lt;/em&gt; author (the source document, the tool response), and that becomes the baseline your new Tier 1/2 check scores against. Second, the trace is itself agent-didn't-author data, which is exactly what Tier 1+2 need to run against without becoming circular.&lt;/p&gt;

&lt;p&gt;Without the trace, your judge is guessing and your gate has nothing trustworthy to grade. With it, every recurring opinion is one refactor away from becoming a free, deterministic check.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;A model-as-judge is not the top of your eval stack — it's the &lt;strong&gt;intake queue&lt;/strong&gt; for it. Its job is to surface recurring, real defects so you can promote them down the independence axis into checks that are cheaper, faster, and impossible for the agent to forge. &lt;code&gt;agent-eval&lt;/code&gt; runs the tiers and the gate; AgentLens gives you the unforgeable trace to build the next gate from. Leave a finding in Tier 3 forever and you're not evaluating — you're just paying, slowly, to be reminded of a bug you could have gated on day one.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>evaluation</category>
      <category>observability</category>
    </item>
    <item>
      <title>The Cold-Start Problem for Agent Evals: What to Gate on Day One With Zero Labeled Data</title>
      <dc:creator>Saurav Bhattacharya</dc:creator>
      <pubDate>Wed, 22 Jul 2026 01:02:08 +0000</pubDate>
      <link>https://dev.to/saurav_bhattacharya/the-cold-start-problem-for-agent-evals-what-to-gate-on-day-one-with-zero-labeled-data-4ck2</link>
      <guid>https://dev.to/saurav_bhattacharya/the-cold-start-problem-for-agent-evals-what-to-gate-on-day-one-with-zero-labeled-data-4ck2</guid>
      <description>&lt;p&gt;You just shipped an agent. It works in the demo. Now someone asks the reasonable question: "How do we know it keeps working?" And you reach for evals — and hit a wall. You have no labeled dataset. No golden outputs. No historical traces. Nothing to grade against.&lt;/p&gt;

&lt;p&gt;So the team stalls. "We'll add evals once we collect data." Meanwhile the agent runs in production, ungated, and the first time it silently breaks is the first time anyone notices.&lt;/p&gt;

&lt;p&gt;This is the cold-start problem, and the usual response — "just get an LLM to score the output 1-10" — is exactly the wrong instinct. You do not need labels to start gating. You need to understand which evidence you can trust on day one, and which you can't trust ever.&lt;/p&gt;

&lt;h2&gt;
  
  
  Independence, not cost
&lt;/h2&gt;

&lt;p&gt;Most eval discussions rank checks on a cost axis: cheap string matches at the bottom, expensive model-as-judge at the top, as if spending more buys you more truth. That's backwards. The axis that matters is &lt;strong&gt;independence&lt;/strong&gt;: can the agent forge this signal, or not?&lt;/p&gt;

&lt;p&gt;That reframing is the whole game for cold-start, because independent evidence needs zero labels. It's true or false about the world regardless of what your agent intended.&lt;/p&gt;

&lt;p&gt;Three tiers, ranked independent to corruptible:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tier 1 — externally observable proof the agent can't forge.&lt;/strong&gt; Did it produce valid JSON? Does the file it claims to have written exist? Did the code compile? Did the tests pass? Did it finish inside the timeout? Is the output non-empty?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tier 2 — statistical signal against a baseline the agent didn't author.&lt;/strong&gt; Is the output embedding-similar to the task it was given? Is the length sane, or did it collapse into repetition? Did the diff actually change anything?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tier 3 — model-as-judge.&lt;/strong&gt; A shared-substrate opinion. A signal, never a verdict.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On day one you have Tier 1 and Tier 2 completely for free. Neither needs a single labeled example, because neither asks "is this good?" — they ask "is this real?"&lt;/p&gt;

&lt;h2&gt;
  
  
  The day-one gate
&lt;/h2&gt;

&lt;p&gt;Here's a starter gate for an agent that's supposed to produce a code patch. No dataset required:&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;GateResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;pass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;coldStartGate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;patch&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;targetFile&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;runtime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;durationMs&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;timeoutMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&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;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;GateResult&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="na"&gt;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GateResult&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="c1"&gt;// Tier 1 — unforgeable facts about the world&lt;/span&gt;
  &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;tier&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;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;non-empty output&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;pass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;patch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;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="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;tier&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;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;target file exists&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;pass&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;fileExists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;targetFile&lt;/span&gt;&lt;span class="p"&gt;),&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="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;tier&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;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;patch applies + compiles&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;pass&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;applyAndCompile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;patch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;targetFile&lt;/span&gt;&lt;span class="p"&gt;),&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="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;tier&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;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;finished within timeout&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;pass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;durationMs&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timeoutMs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Tier 2 — statistical signal vs a baseline the agent didn't write&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;taskVec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;patchVec&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nf"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;patch&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="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;tier&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;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;patch is on-topic for the task&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;pass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;cosine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;taskVec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;patchVec&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.35&lt;/span&gt;&lt;span class="p"&gt;,&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="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;tier&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;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;diff changed something&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;pass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;*$/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;patch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;-+&lt;/span&gt;&lt;span class="se"&gt;]{3}&lt;/span&gt;&lt;span class="sr"&gt;.*$/gm&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="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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every check here is either true about the filesystem/compiler/clock, or it's a distance against the task string — which the agent received but did not author. There is nothing to label. And this catches the overwhelming majority of real failures: the stale run, the crash, the malformed output, the hallucinated file path, the empty response, the patch that wandered off into an unrelated file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Tier 3 stays out on day one (and after)
&lt;/h2&gt;

&lt;p&gt;The temptation is to skip all this and let a judge model read the patch and give it a score. Resist it — and not just because you have no labels.&lt;/p&gt;

&lt;p&gt;A model judging another model's work is &lt;strong&gt;circular&lt;/strong&gt;. Judge and judged share a substrate: the same training distribution, the same blind spots, the same confident wrongness. There's no independent ground truth in that loop. So Tier 3 is a signal about taste, never a verdict about correctness, and it may only inspect artifacts the judged agent didn't get to write — never the agent's own reasoning trace, which it can rationalize.&lt;/p&gt;

&lt;p&gt;There are two more hard constraints. &lt;strong&gt;Tier 1+2 are the real-time gate&lt;/strong&gt;: deterministic, effectively free, fast enough to block a run before a bad output escapes. &lt;strong&gt;Tier 3 is offline-only&lt;/strong&gt;: metered, slow, non-deterministic — it cannot sit in the hot path. You run it later, in batch, over the ~20% subjective tail that Tier 1+2 can't adjudicate, and you label its output "opinion, not evidence." Ship the 80% you can gate deterministically today; don't block your launch waiting for a judge you shouldn't trust in the loop anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two halves you actually need
&lt;/h2&gt;

&lt;p&gt;Gating output is only half the job. The other half is knowing &lt;em&gt;what happened&lt;/em&gt;, and this is where cold-start teams quietly cheat: they gate on the agent's self-report, which is exactly the forgeable thing Tier 1 is supposed to route around.&lt;/p&gt;

&lt;p&gt;This is why the eval layer and the trace layer ship as a unit. &lt;strong&gt;agent-eval&lt;/strong&gt; scores and gates the output — the tier logic above: evals, drift, hallucination checks. &lt;strong&gt;AgentLens&lt;/strong&gt; captures the trace of &lt;em&gt;how&lt;/em&gt; the agent got there: every model call and tool step, the resolved inputs, the raw outputs. The two connect at a specific seam: Tier 1+2 need unforgeable data to score against, and the agent must not be the one who wrote it. AgentLens gives you exactly that — the real file that got written, the actual exit code, the true wall-clock duration — instead of the agent's summary of what it thinks it did.&lt;/p&gt;

&lt;p&gt;Without the trace, your gate degrades into grading the agent's own press release. With it, "finished within timeout" and "target file exists" become facts, not claims.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start today
&lt;/h2&gt;

&lt;p&gt;You don't have a labeled dataset. You never will on day one. But you already have a filesystem, a compiler, a clock, and an embedding model — which means you already have a Tier 1+2 gate. Wire agent-eval to it, point AgentLens at your run to feed it unforgeable trace data, and gate the 80%. Collect the judge-tail labels &lt;em&gt;while&lt;/em&gt; you're already protected in production, not instead of protecting it.&lt;/p&gt;

&lt;p&gt;The cold-start problem was never about missing data. It was about asking the wrong tier for permission to launch.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>evaluation</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
