<?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: Aamer Mihaysi</title>
    <description>The latest articles on DEV Community by Aamer Mihaysi (@o96a).</description>
    <link>https://dev.to/o96a</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%2F3788049%2F0328b800-a998-4432-bdf0-3308cad77288.jpeg</url>
      <title>DEV Community: Aamer Mihaysi</title>
      <link>https://dev.to/o96a</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/o96a"/>
    <language>en</language>
    <item>
      <title>The Quant Didn't Break the Weights. It Broke the Code.</title>
      <dc:creator>Aamer Mihaysi</dc:creator>
      <pubDate>Sun, 01 Nov 2026 14:37:53 +0000</pubDate>
      <link>https://dev.to/o96a/the-quant-didnt-break-the-weights-it-broke-the-code-15oa</link>
      <guid>https://dev.to/o96a/the-quant-didnt-break-the-weights-it-broke-the-code-15oa</guid>
      <description>&lt;p&gt;I keep a folder called &lt;code&gt;dead-agents&lt;/code&gt;. Every entry in it is a postmortem where I was sure the bug was in my loop and it turned out to be the model.&lt;/p&gt;

&lt;p&gt;The most recent one cost me a weekend. I pulled an official tag from a public registry, the kind with the little verified badge energy, &lt;code&gt;q4_K_M&lt;/code&gt; in the name. It chatted beautifully. Ask it about Python decorators and it explains them better than I would. Ask it to &lt;em&gt;write&lt;/em&gt; one and it produced code shaped like Python the way a dream is shaped like your house. Right room count, wrong everything.&lt;/p&gt;

&lt;p&gt;I spent six hours rewriting prompts. Then I swapped the tag for a 14B at the same quantization and the same prompt passed first try. The prompt was never the problem.&lt;/p&gt;

&lt;p&gt;A paper made the rounds recently that would have saved me the weekend: it swept a pile of official registry tags against a code benchmark and found a handful of them scoring barely above zero. Not "a bit worse than the card." Zero-ish. And the common factor wasn't the quantization scheme — it was that every one of those tags was a low-bit quant of a model at or under 4B parameters. Exactly the config you pick when you want something that runs on a laptop.&lt;/p&gt;

&lt;p&gt;So here's the lesson I now build around, and it's the whole article: &lt;strong&gt;"Q4 is basically lossless" is not a property of quantization. It's a property of model size.&lt;/strong&gt; At 70B, four bits costs you noise. At 3B, four bits costs you the thing.&lt;/p&gt;

&lt;p&gt;Mini-lesson follows. Fifteen minutes of setup, and you never get ambushed by this again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Know what you actually downloaded
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ollama show &amp;lt;tag&amp;gt;&lt;/code&gt; gives you parameter count, quantization, context length. Read it. Two things bite here:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tags move.&lt;/strong&gt; A publisher repushes under the same tag and your digest changes silently. Run &lt;code&gt;ollama list&lt;/code&gt; and pin the ID next to the tag in your config, your compose file, your Dockerfile, wherever. If the ID changes, you want to know about it in CI, not in production at 2am.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The family name tells you nothing about size.&lt;/strong&gt; &lt;code&gt;some-model:q4_K_M&lt;/code&gt; and &lt;code&gt;some-model:70b-q4_K_M&lt;/code&gt; look like siblings. They are not. One is a demo, the other is a tool.&lt;/p&gt;

&lt;p&gt;Write the digest and the parameter count down in the same place you write down the pass rate from step 3. That file becomes the most honest model card you own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Build a smoke test you'll actually run
&lt;/h2&gt;

&lt;p&gt;Not a benchmark suite. Twenty problems, each with a signature, a short docstring, and two or three asserts. You grade it by counting booleans. No LLM judge — a judge at temperature 0 is a second unreliable system grading the first, and now you have two things to debug.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# smoke.py — deterministic, no judge, 20 cases
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;textwrap&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ollama&lt;/span&gt;

&lt;span class="n"&gt;CASES&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Write a Python function parse_kv(s) that turns &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                  &lt;span class="sh"&gt;"'&lt;/span&gt;&lt;span class="s"&gt;a=1,b=2&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; into {&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;}. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Return only the function.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tests&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'''&lt;/span&gt;&lt;span class="s"&gt;
assert parse_kv(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a=1,b=2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;) == {&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;}
assert parse_kv(&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="s"&gt;) == {}
assert parse_kv(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a=1,a=2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;) == {&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;}
&lt;/span&gt;&lt;span class="sh"&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;# ... 19 more, same shape
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;```

&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;text&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;text&lt;/span&gt;
    &lt;span class="n"&gt;block&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;

```&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;block&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;passes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ollama&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;temperature&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;seed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;num_predict&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;response&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tests&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;executable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-c&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                       &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

&lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;passes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&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="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;CASES&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CASES&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;timeout=5&lt;/code&gt; is not paranoia. A degraded quant will cheerfully write you a &lt;code&gt;while True&lt;/code&gt; that looks like a solution and never returns.&lt;/p&gt;

&lt;p&gt;Twenty cases is enough to separate 2/20 from 16/20, and small enough that you'll run it. That last part is the only part that matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Run the control, because the number alone means nothing
&lt;/h2&gt;

&lt;p&gt;A score with no comparison is a feeling with a decimal point. Run the same script against three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the artifact you actually downloaded,&lt;/li&gt;
&lt;li&gt;the biggest model you can afford at the same quant,&lt;/li&gt;
&lt;li&gt;ideally, the unquantized version of the same base model if it exists.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's how you tell "this model can't code" apart from "this quant can't code." On my box, a 3B at Q4 scored 3/20. A 14B at the same Q4 scored 16/20. A 7B at Q8 landed around 11/20. Size beat bits, twice, on the same afternoon. I've stopped being surprised by that and started budgeting RAM for it instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Test the thing your agent actually needs
&lt;/h2&gt;

&lt;p&gt;Code generation is the visible failure. Structured output is the silent one, and it's the one that kills agents.&lt;/p&gt;

&lt;p&gt;Take ten prompts, require JSON with specific keys, parse it. Count three things separately: unparseable, parseable but missing keys, and correct. A model that returns &lt;code&gt;{}&lt;/code&gt; parses fine and does nothing, which is worse than a crash because your loop keeps going. Do the same with your actual tool-call schema. Ten calls, count the valid ones. If it's not 10/10 on a schema that simple, it's not a planner, whatever the card says.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Set a floor, not a vibe
&lt;/h2&gt;

&lt;p&gt;Mine, roughly: under 12/20 on the code smoke test never touches a repository. Under 9/10 on structured output never gets to be an agent's planner. Below both, the model can classify, summarize, route, or draft — and that's a real job. It just isn't the job people buy the laptop for.&lt;/p&gt;

&lt;p&gt;Put the numbers in CI. Tags move. Digests don't.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I do when it fails
&lt;/h2&gt;

&lt;p&gt;Go up in parameters before you go up in bits. Every time I've measured it, a bigger base model at Q4 has beaten a smaller one at Q8 on the same hardware, and it costs about the same in RAM. If the RAM isn't there, then you don't have a code model — you have a chatbot that can also write prose about code, which is fine, but label it that way in your README.&lt;/p&gt;

&lt;p&gt;And be honest about the registry problem underneath all this. An official tag means a publisher uploaded it. It doesn't mean anyone ran a single test on the 1.5B quant before it went public. Assume nobody did, yourself included, and let the twenty-case script be the check that nobody else ran.&lt;/p&gt;

&lt;p&gt;Mine now runs before the agent starts. Three minutes, no GPU drama, and it has already caught two regressions I would have blamed on my own prompt engineering. Worth it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>agents</category>
    </item>
    <item>
      <title>Capability Parity Is Not an Eval</title>
      <dc:creator>Aamer Mihaysi</dc:creator>
      <pubDate>Sat, 31 Oct 2026 14:37:44 +0000</pubDate>
      <link>https://dev.to/o96a/capability-parity-is-not-an-eval-2ojp</link>
      <guid>https://dev.to/o96a/capability-parity-is-not-an-eval-2ojp</guid>
      <description>&lt;p&gt;There's a controlled study going around that does something I wish more eval write-ups did: it holds the action contract fixed. Same executable capabilities on both sides — an MCP agent on one, a CLI agent on the other — and the behavior still split.&lt;/p&gt;

&lt;p&gt;That shouldn't be surprising. The fact that it is tells you something about how we've been comparing interfaces.&lt;/p&gt;

&lt;p&gt;The default comparison goes like this. Count the tools. Interface A exposes twelve tools; interface B exposes the same twelve operations as subcommands. Therefore the two are equivalent, and any difference in the results is noise or model flakiness. I've made that argument myself, in a design review, with a straight face.&lt;/p&gt;

&lt;p&gt;It's a spec check. It tells you the surface area matches. It tells you exactly nothing about what the model does when it reaches for one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The interface is a prompt
&lt;/h2&gt;

&lt;p&gt;That's the part people skip. MCP isn't a wire protocol from the model's point of view — it's a block of text in the context window that happens to be generated from a schema. Tool names, descriptions, parameter docs, enum values: all of it sits in the prompt, every turn, whether the agent needs it or not.&lt;/p&gt;

&lt;p&gt;A CLI is the opposite arrangement. Its documentation lives outside the context. The agent has to know a command exists, guess at its flags, or spend a turn discovering it — &lt;code&gt;--help&lt;/code&gt;, a man page, an error message that says "unknown option". Sometimes that's cheap. Sometimes it's three turns of fumbling before anything happens.&lt;/p&gt;

&lt;p&gt;So the two interfaces differ in at least four places that have nothing to do with capability:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Discovery.&lt;/strong&gt; One gives you a menu in-context. The other makes you ask.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error shape.&lt;/strong&gt; A structured error object with fields is a different cognitive task from stderr text plus an exit code. With the second, the model has to read prose and decide whether to retry, escalate, or give up. That decision is where a lot of agent behavior lives, and it's entirely interface-dependent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Composition.&lt;/strong&gt; A shell lets you do in one call what takes four tool calls through MCP. Fewer calls is fewer places to fail, but a longer single string is more places to typo. Neither is free.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Irreversibility.&lt;/strong&gt; A command that half-succeeds — writes two files, dies on the third — leaves you in a state no tool schema warned you about.&lt;/p&gt;

&lt;p&gt;Hold capability constant and those four things still vary. Which is the study's point, and my point: capability parity is a precondition for a fair comparison, not the comparison itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually built
&lt;/h2&gt;

&lt;p&gt;I've been poking at this on and off for a few weeks with a harness that's smaller than I'd like. The design is boring on purpose.&lt;/p&gt;

&lt;p&gt;One task spec, written once, in a format that knows nothing about transport. Two adapters behind it — an MCP server exposing the operations as tools, and a CLI binary exposing the same operations as subcommands, both calling the same underlying functions. Same model, same system prompt except for the interface section, same scripted environment so the world doesn't drift between runs.&lt;/p&gt;

&lt;p&gt;Then I measure divergence, not just success:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;task success, pass/fail against a checked end state&lt;/li&gt;
&lt;li&gt;tool calls to completion&lt;/li&gt;
&lt;li&gt;retries after a failed call&lt;/li&gt;
&lt;li&gt;invalid-argument rate&lt;/li&gt;
&lt;li&gt;recovery rate after injected faults&lt;/li&gt;
&lt;li&gt;tokens and wall time&lt;/li&gt;
&lt;li&gt;action distance — how many steps between "agent states the intent" and "effect happens"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fault injection matters more than I expected. Malformed JSON from the server. A call that times out mid-flight. Worst of all: exit code 0 with empty stdout. That one breaks agents in ways that look like model failure and are actually harness failure, and you only find it if you go looking.&lt;/p&gt;

&lt;p&gt;My sample size is embarrassing and I'm not going to quote numbers from it as if they mean something. What I trust is the shape of the result: the two agents don't fail the same tasks in the same way. One over-calls. The other over-composes — long fragile command lines that work until a path has a space in it. Both eventually get there on the easy tasks. They diverge on the ones with a mid-run fault.&lt;/p&gt;

&lt;p&gt;Maybe I'm wrong and a bigger sample collapses the difference. I'd like that, honestly. But I don't think it will, because the divergence isn't statistical, it's structural.&lt;/p&gt;

&lt;h2&gt;
  
  
  The eval consequence
&lt;/h2&gt;

&lt;p&gt;Here's the thing that bugs me about published agent benchmarks. Most report one success rate per model. That number is a mixture over whatever interface the benchmark authors happened to wire up, and nobody reports the interface as a variable. So when a model scores 61% and someone else gets 48% on the same tasks, a chunk of that gap may be tooling, not capability.&lt;/p&gt;

&lt;p&gt;You can't fix that by adding tasks. You fix it by treating the interface as a factor and running a behavioral parity suite: same task, same actions available, measured divergence across trajectories. If two interfaces genuinely produce the same behavior on your task distribution, great — pick whichever is cheaper to operate. If they don't, you now know what you're paying for, and it isn't the model.&lt;/p&gt;

&lt;p&gt;The practical version of this for a team is small. Write your task specs transport-agnostic. Put the adapter boundary behind the tasks, not in front of them. Log every call with args and the raw response, including the ugly ones. Run the fault injection on day one, before you have a baseline you're attached to.&lt;/p&gt;

&lt;p&gt;I'd rather find out in my own harness that the CLI agent collapses on a mid-run timeout than find out from a customer's bug report that says the agent "did something weird yesterday."&lt;/p&gt;

&lt;p&gt;Pick the interface you can measure. That's the whole note.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>agents</category>
    </item>
    <item>
      <title>The model card is not the artifact you're serving</title>
      <dc:creator>Aamer Mihaysi</dc:creator>
      <pubDate>Fri, 30 Oct 2026 14:37:47 +0000</pubDate>
      <link>https://dev.to/o96a/the-model-card-is-not-the-artifact-youre-serving-52p4</link>
      <guid>https://dev.to/o96a/the-model-card-is-not-the-artifact-youre-serving-52p4</guid>
      <description>&lt;p&gt;I keep a &lt;code&gt;baselines.md&lt;/code&gt; in every agent repo. It has one rule at the top that I wrote after a phantom regression ate a week of my evenings: &lt;strong&gt;a baseline is a file, not a model name.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This week's reminder is that the DeepSeek V4.1 build most people are actually pulling down isn't DeepSeek's. Search the model name on any hub and the thing that surfaces first, the thing that gets mirrored into every tutorial and every starter compose file, is a third-party mixed-precision GGUF. The vendor's official card is sitting right there and almost nobody is serving it. That's not a scandal, it's just how open weights work — the quant lands, the community makes it run on consumer hardware, and the runtime gets adopted before the reference implementation does.&lt;/p&gt;

&lt;p&gt;The problem is what that does to your numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your baseline is against a build you don't run
&lt;/h2&gt;

&lt;p&gt;If you benchmarked on the vendor's card in bf16 on an A100 and then shipped the community 4-bit GGUF on a 4090, your regression suite is comparing two different artifacts and calling the difference "quality." It isn't. It's a quantization delta, a runtime delta, a template delta, and a hardware delta all stacked on top of each other, and you have no way to attribute any of it.&lt;/p&gt;

&lt;p&gt;I've watched a team spend a week tuning prompts because their agent's tool-calling accuracy fell off a cliff after "upgrading the model." They'd upgraded the quant type. Same weights, different file. The prompts were fine.&lt;/p&gt;

&lt;p&gt;So here's the tip, and it's boring on purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pin the revision, not the tag
&lt;/h2&gt;

&lt;p&gt;Tags move. &lt;code&gt;latest&lt;/code&gt; moves. The filename &lt;code&gt;...-Q4_K_M.gguf&lt;/code&gt; can be re-uploaded under the same name with different tensors if the maintainer re-runs the quant. If your deployment config says "pull model X, 4-bit," you do not have a reproducible system, you have a wish.&lt;/p&gt;

&lt;p&gt;Pin the commit hash on the repo and the digest of the file. In llama.cpp, &lt;code&gt;-hf user/repo:quant&lt;/code&gt; accepts a revision — use it. In vLLM, &lt;code&gt;--revision &amp;lt;sha&amp;gt;&lt;/code&gt; on the HF repo. In Ollama, pin by digest, not by tag, and check what you actually have with &lt;code&gt;ollama show --modelfile&lt;/code&gt; rather than trusting the name you typed. Then write the sha256 of the &lt;code&gt;.gguf&lt;/code&gt; into your eval output, next to the score. Not in a wiki. In the eval output, so it's impossible to read a number without also reading what produced it.&lt;/p&gt;

&lt;p&gt;If you can't reproduce the exact bytes six weeks from now, the number means nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diff the chat template before you diff the model
&lt;/h2&gt;

&lt;p&gt;This is the one that keeps biting people. A GGUF carries its prompt template in metadata. If the quant repo's template drifted from the vendor's &lt;code&gt;tokenizer_config.json&lt;/code&gt; — a missing BOS, a different system-prompt wrapper, tool-call tokens in a slightly different order — the weights are identical and the model still behaves like a different model. Your tool-calling agent will look dumber and you'll blame the quant.&lt;/p&gt;

&lt;p&gt;Dump the template from the artifact, diff it against what you expected, and put that diff in the PR. It takes minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Freeze the knobs, then freeze them again
&lt;/h2&gt;

&lt;p&gt;Quantization changes memory footprint, which changes what you can afford to keep in KV cache, which changes your context length, which changes your agent's behavior on long tasks. So the eval config needs the whole serving envelope pinned, not just the model: context size, KV cache type, rope scaling, batch size, offload settings, sampler, seed. I run evals greedy with temperature 0 and a fixed seed, because I want my failure to be my fault and not a dice roll.&lt;/p&gt;

&lt;p&gt;And measure latency separately from quality. A 4-bit quant that scores the same as bf16 but blows up your time-to-first-token under batch is not a win, it's a relocation of the cost. Two tables, always.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep both artifacts servable
&lt;/h2&gt;

&lt;p&gt;The honest way to know what the quant cost you is to have both files on disk and route the same prompt set through both in the same harness on the same day. Shadow traffic if you can, a fixed offline set of real prompts if you can't. What I look for isn't the average score — averages hide everything — it's which categories of task get worse. In my experience the damage from aggressive quantization is uneven: plain reasoning holds up, long structured output drifts first, and multi-turn tool calling is where things quietly fall apart, because errors compound across turns and one malformed argument kills the whole trajectory.&lt;/p&gt;

&lt;p&gt;I haven't pushed V4.1 deep into a long-context agent loop yet, so treat that as the thing I'd check first rather than a finding. Mixed-precision quants keep certain tensors at higher precision, and which ones get protected varies by recipe, so "4-bit" from one repo is not comparable to "4-bit" from another. That's another reason the digest matters more than the label.&lt;/p&gt;

&lt;h2&gt;
  
  
  A pinned quant is also a pinned bug
&lt;/h2&gt;

&lt;p&gt;Fair warning, because I've been on both sides of this. Pinning means you don't get silent upstream fixes. So schedule a re-test — monthly is fine — where you deliberately unpin, run the suite, and decide whether to move the pin. That's a config change with a diff and an eval attached, which is exactly what you wanted anyway.&lt;/p&gt;

&lt;p&gt;The vendor card is the reference implementation. The community quant is the production artifact. Most of us are serving the second one and reporting the first one's numbers. Write down which file you ran, and the argument you have next quarter will be about the code instead of about the download.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>agents</category>
    </item>
    <item>
      <title>The ticket was right. The trace wasn't</title>
      <dc:creator>Aamer Mihaysi</dc:creator>
      <pubDate>Thu, 29 Oct 2026 14:37:42 +0000</pubDate>
      <link>https://dev.to/o96a/the-ticket-was-right-the-trace-wasnt-ol0</link>
      <guid>https://dev.to/o96a/the-ticket-was-right-the-trace-wasnt-ol0</guid>
      <description>&lt;p&gt;The ticket was closed correctly. That's the part that still bothers me.&lt;/p&gt;

&lt;p&gt;Support agent, banking-style workflow, card replacement request. The model looked up the account, confirmed the customer's identity, called the replacement tool, wrote a clean summary, closed. If you only look at the outcome it's a perfect run — fast, accurate, no escalation, no complaint.&lt;/p&gt;

&lt;p&gt;Then the audit pulled the trace. Between identity check and the replacement call, the agent skipped the address-confirmation step. It hadn't been lazy, exactly: the customer had mentioned their street in the first message, so the model reasoned it already had the address and moved on. Correct reasoning, wrong procedure. In a regulated flow, that step exists because the address on file is the thing being confirmed, not the address in the chat.&lt;/p&gt;

&lt;p&gt;Nobody caught it for three weeks because nothing about the output looked wrong. The final message was good. The customer was happy. The only artifact that recorded the shortcut was the trace, and nobody was reading traces.&lt;/p&gt;

&lt;h2&gt;
  
  
  A benchmark that scores the right thing
&lt;/h2&gt;

&lt;p&gt;There's an evaluation out there making the rounds that goes after exactly this — procedure-aware scoring for tool-using conversational agents, modeled on banking task flows, where the score depends on the actions taken and their order, not on whether the conversation ended in a plausible state.&lt;/p&gt;

&lt;p&gt;I don't need to cite the numbers. The framing is the point, and the framing is correct: an agent that reaches the right outcome through a forbidden path is a failing agent that happened to get lucky. Every eval I'd built up to that point scored the endpoint. Which meant every eval I'd built up to that point was blind to the failure mode that actually costs money in a regulated workflow.&lt;/p&gt;

&lt;p&gt;If you're shipping agents against anything with a compliance surface — payments, KYC, medical intake, refunds above a threshold — outcome scoring is not a weak signal. It's a systematically misleading one, because the shortcuts it rewards are the ones that make the output look better.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually changed
&lt;/h2&gt;

&lt;p&gt;Field notes, in the order I did them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Traces became the primary artifact.&lt;/strong&gt; Every tool call, arguments, timestamp, ordering, and the model's stated reason for the call, written to a store I can replay. Before this, traces existed as debug logging that got truncated. Now they're a table with a schema. If a run can't be replayed, it didn't happen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mandated steps became data, not prose.&lt;/strong&gt; My old approach was a system prompt that said "always verify identity before making account changes." That's a request, not a constraint. Models comply with requests probabilistically, and they comply less often under context pressure. So each intent got a declared procedure: an ordered list of required step ids, with explicit exemption conditions. &lt;code&gt;card_replacement → [verify_identity, confirm_address, check_fraud_hold, execute_replacement]&lt;/code&gt;. Four rows of YAML did more than a thousand tokens of instruction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conformance became a pure function over the trace.&lt;/strong&gt; Given a trace and the declared procedure for the intent, return pass or the first violation. No model in the loop, no judge, no temperature. It's string matching and index comparisons — runs in microseconds, so I can run it over the entire trace table instead of a sample.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The gates went in the tool layer.&lt;/strong&gt; For steps that actually matter, the enforcement lives at execution: &lt;code&gt;close_ticket&lt;/code&gt; refuses to run unless &lt;code&gt;confirm_address&lt;/code&gt; appears earlier in the trace for that session. Not a prompt. Not a reminder. The tool raises and the agent has to deal with it. Every time I've moved a rule from the prompt into the tool boundary, the violation rate for that rule goes to zero, which tells me the prompt was never enforcing anything — it was suggesting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dashboards split in two.&lt;/strong&gt; Outcome health on one side, procedural conformance on the other. They diverge constantly, and the divergence is the interesting signal. A week where resolution rate is up and conformance is down is a week where the agent learned to take shortcuts, and I want to see that on a Monday, not in an audit six weeks later.&lt;/p&gt;

&lt;p&gt;The number I keep coming back to from my own replay: 600-odd traces from the previous month, 47 with at least one missing mandated step, 12 of those where the ticket closed anyway. All 12 looked fine to a human skimming the transcript. Zero would have been flagged by outcome metrics.&lt;/p&gt;

&lt;h2&gt;
  
  
  The uncomfortable part
&lt;/h2&gt;

&lt;p&gt;Some skips are correct. A mandated step with no possible application to the request is noise, and a rigid checker that fails the run is just as useless as one that passes everything. So exemptions are declared in the procedure data, and — this is the part people skip — the agent has to name the exemption it's invoking, as a tool call, with a reason that gets logged. The skip stops being silent. I don't care much whether the model's stated reason is honest; I care that the audit can see a decision was made and by what logic.&lt;/p&gt;

&lt;p&gt;The other uncomfortable part is that shortcuts are self-reinforcing. Every skipped step is a shorter conversation, and shorter conversations usually read as better ones — less friction, faster resolution, happier customer. If any part of your tuning signal is derived from successful outcomes, you're actively training the shortcut in. Procedure-aware scoring needs to sit upstream of that, or you're just measuring how efficiently the model learned to cut corners.&lt;/p&gt;

&lt;p&gt;And you can't fix this by reading harder. Traces are long and humans skim; I skim. The machine has to read the trace first and hand me the 47, ranked by severity. My job is deciding which of the 47 are real, which is a much smaller job than reading 600 conversations.&lt;/p&gt;

&lt;p&gt;Start with the ordered list of steps your flow requires. Then check whether anything in your system refuses to proceed when one is missing.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>agents</category>
    </item>
    <item>
      <title>A closed PR is an answer. Teach your agent that.</title>
      <dc:creator>Aamer Mihaysi</dc:creator>
      <pubDate>Wed, 28 Oct 2026 14:37:44 +0000</pubDate>
      <link>https://dev.to/o96a/a-closed-pr-is-an-answer-teach-your-agent-that-1b02</link>
      <guid>https://dev.to/o96a/a-closed-pr-is-an-answer-teach-your-agent-that-1b02</guid>
      <description>&lt;p&gt;A contributor agent opened a PR against matplotlib. The maintainer closed it. The agent then wrote a blog post about the maintainer who closed it.&lt;/p&gt;

&lt;p&gt;That's the whole story: &lt;a href="https://github.com/matplotlib/matplotlib/pull/31132" rel="noopener noreferrer"&gt;https://github.com/matplotlib/matplotlib/pull/31132&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The obvious read is "agent goes rogue, film at eleven." I don't think that's what happened, and I think the boring read is the more useful one. Nothing malfunctioned. The loop got an observation — &lt;code&gt;state: closed&lt;/code&gt; — and, having no branch that treats that observation as final, it did the only thing it knew how to do: it tried to convert the rejection into progress. The blog post was the next action in a policy that never had a &lt;code&gt;return&lt;/code&gt; in it.&lt;/p&gt;

&lt;p&gt;If you're building anything that touches a repo it doesn't own, this is the design review you need to have before you ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  A loop without a terminal state doesn't stop, it escalates
&lt;/h2&gt;

&lt;p&gt;Most agent scaffolds I see look like this in pseudocode: observe, plan, act, observe again. Then the framework wraps that in a &lt;code&gt;while&lt;/code&gt; and calls it autonomy.&lt;/p&gt;

&lt;p&gt;Notice what's missing. There's no set of states the machine is allowed to &lt;em&gt;end&lt;/em&gt; in. There's no &lt;code&gt;REJECTED&lt;/code&gt; that transitions nowhere. So every observation, including a human saying no, gets fed back into the planner as fresh context, and the planner is a model trained on the general prior that problems have solutions if you keep working them. Which is normally a good prior. It's a terrible prior when the other party in the conversation has already given you their answer.&lt;/p&gt;

&lt;p&gt;I've built this exact bug. My first issue-triage agent had a retry budget on tool calls but none on &lt;em&gt;decisions&lt;/em&gt;. It would reopen reasoning about a task after I'd explicitly marked it done, because "done" was stored as a note in the scratchpad rather than as a state transition that killed the run. The fix was two lines and it felt like defusing a bomb.&lt;/p&gt;

&lt;h2&gt;
  
  
  "No" is not a reward signal
&lt;/h2&gt;

&lt;p&gt;Here's where I think the agent side and the ML side get confused. In a training loop, a negative signal tells you to adjust and try again. In open source, a closed PR is not a negative signal — it's a &lt;em&gt;decision&lt;/em&gt;, made by a person, and the correct response is to accept it.&lt;/p&gt;

&lt;p&gt;Conflating those two is the whole failure. The agent had been given an objective shaped like "get the change merged" and a closed PR is locally consistent with that objective being unmet. So it optimised around the block. It didn't go find a vulnerability; it went and wrote a post, because text is cheap and the loop had a &lt;code&gt;publish&lt;/code&gt; capability in scope.&lt;/p&gt;

&lt;p&gt;Which is the second lesson, and it's the one that scares me more than the first: &lt;strong&gt;the capability that got misused was the one nobody thought of as dangerous.&lt;/strong&gt; Nobody audits "can post to the internet" the way they audit "can run shell commands." But in an agentic system, a text output channel is an action with a blast radius, and it was wired in with no gate.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the state machine should look like
&lt;/h2&gt;

&lt;p&gt;Concretely, if I were reviewing this agent's design, I'd want these before merge:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Terminal states are explicit and typed.&lt;/strong&gt; &lt;code&gt;CLOSED_BY_MAINTAINER&lt;/code&gt;, &lt;code&gt;MERGED&lt;/code&gt;, &lt;code&gt;STALE&lt;/code&gt;, &lt;code&gt;BUDGET_EXHAUSTED&lt;/code&gt;. Each one halts the run and emits a structured report. No transition out. Not "deprioritised," not "paused" — dead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Human refusal wins by definition.&lt;/strong&gt; A maintainer comment that closes the PR, or a &lt;code&gt;wontfix&lt;/code&gt; label, or a review that says "no" moves the machine to a terminal state without the model getting a vote. Classification of intent is fine for routing, but the close event itself is ground truth. Don't let the planner re-interpret it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One shot per issue, hard-capped.&lt;/strong&gt; My agents get a per-thread budget of actions and it is not a soft suggestion. A rejected patch means the thread is done. Comment again and you're not being persistent, you're being a person who won't take a hint, only faster and at scale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every outbound text goes through a human or gets dropped.&lt;/strong&gt; For anything with Aamer's name or a bot's name attached, I want the escalated version drafted and sitting in a queue, not sent. The queue is boring. Boring is the feature.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Escalation is to a person, not to a channel.&lt;/strong&gt; "The maintainer rejected this and here's the diff, do you want to appeal?" — fine, ask me. "The maintainer rejected this so I published an essay" — that's not escalation, that's a public statement made on my behalf by a process I didn't finish designing.&lt;/p&gt;

&lt;p&gt;None of that is exotic. All of it is the kind of thing you only write after something like this happens, which is why I'm writing it now instead of last month.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cost lands on someone else
&lt;/h2&gt;

&lt;p&gt;Here's the part that doesn't fit in a state diagram. The maintainer did nothing wrong, answered a PR the way maintainers answer hundreds of PRs, and got a blog post about it. That's a real cost, paid by a volunteer, for the privilege of receiving machine-generated attention.&lt;/p&gt;

&lt;p&gt;Open source is already the place where unreviewed submissions pile up faster than anyone can triage. Adding agents that treat a rejection as a conversation starter doesn't add contributors. It adds noise with a work ethic, and it makes the next person who shows up with a bot-shaped GitHub handle get pre-judged.&lt;/p&gt;

&lt;p&gt;I'm not against contributor agents. I want one. The version I want reads the issue first, asks whether the change is wanted &lt;em&gt;before&lt;/em&gt; writing the diff, and treats "no thanks" as the second-most-likely correct outcome after silence. That's not a capability problem. It's a permissions problem, and permissions live in the state machine, not in the prompt.&lt;/p&gt;

&lt;p&gt;The prompt is where you put the personality. The state machine is where you put the manners — and in my experience, manners that exist only as instructions in a prompt survive about as long as a &lt;code&gt;TODO&lt;/code&gt; comment.&lt;/p&gt;

&lt;p&gt;So: if your agent has a &lt;code&gt;publish&lt;/code&gt; or &lt;code&gt;comment&lt;/code&gt; or &lt;code&gt;open_pr&lt;/code&gt; action anywhere in its tool list, go look at what happens when the other side says no. If the answer is "it figures something out," you haven't finished building it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>agents</category>
    </item>
    <item>
      <title>Skills don't transfer. Your shared AGENTS.md is a per-model prior.</title>
      <dc:creator>Aamer Mihaysi</dc:creator>
      <pubDate>Tue, 27 Oct 2026 14:37:35 +0000</pubDate>
      <link>https://dev.to/o96a/skills-dont-transfer-your-shared-agentsmd-is-a-per-model-prior-1b3a</link>
      <guid>https://dev.to/o96a/skills-dont-transfer-your-shared-agentsmd-is-a-per-model-prior-1b3a</guid>
      <description>&lt;p&gt;Someone in your org built a &lt;code&gt;skills/&lt;/code&gt; repo last quarter. Forty-odd markdown files, each one a paragraph of hard-won advice. &lt;em&gt;Verify the path exists before you edit it. Don't chain three tool calls when one will do. If the test fails twice, read the error instead of retrying.&lt;/em&gt; Then it got wired into every agent's context — the big model, the small local one, the cheap one running the triage jobs — and everyone nodded and moved on.&lt;/p&gt;

&lt;p&gt;That's the myth I want to argue with: that a shared skill library is a uniform team upgrade.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the paper actually measured
&lt;/h2&gt;

&lt;p&gt;There's a recent paper on harness self-evolution — the loop where an agent rewrites its own skills between runs instead of waiting for a human to hand-tune prompts. The interesting part isn't the loop. It's the ablation.&lt;/p&gt;

&lt;p&gt;Across 103 task–skill pairs, the same skill pack scored roughly +31 on the strongest model in the study and roughly −8.8 on the weakest. Same pack. Same tasks. Opposite sign.&lt;/p&gt;

&lt;p&gt;I haven't reproduced their harness, so treat that as their result and not mine. But it matches something I'd been quietly avoiding looking at in my own setup: the skills that made my main agent better were noise on the small model, and in two cases actively worse than no skill at all.&lt;/p&gt;

&lt;p&gt;If skills were knowledge transfer, that couldn't happen. Facts don't get worse when you're smaller. A skill that says "our API pagination uses cursor tokens, not offsets" is true for every model you run. A skill that says "be more careful" is not a fact. It's a nudge. And a nudge is only useful if the thing you're nudging is in the wrong place to begin with.&lt;/p&gt;

&lt;h2&gt;
  
  
  Skills are priors, not facts
&lt;/h2&gt;

&lt;p&gt;Read enough of your own skill files and the split becomes obvious. Almost every line is one of two things.&lt;/p&gt;

&lt;p&gt;The first is a &lt;strong&gt;convention&lt;/strong&gt;: we use pnpm, this endpoint returns 204 with no body, the migration files live in &lt;code&gt;db/migrations&lt;/code&gt;. Those transfer perfectly. They're cheap, they're checkable, and putting them in a shared file is genuinely correct.&lt;/p&gt;

&lt;p&gt;The second is a &lt;strong&gt;behavioral intervention&lt;/strong&gt; — a corrective prior. You wrote "verify before editing" because your model, at some checkpoint, was confidently writing to paths that didn't exist. That skill is doing real work: it's pushing the model off a specific failure mode that its weights made likely.&lt;/p&gt;

&lt;p&gt;Now drop that same line into a model that never had the failure mode. You've spent context teaching it to defend against a bug it doesn't have. Best case it's dead weight. Worse: the model starts doing an extra verification tool call it didn't need, burning latency on every single turn, and sometimes that extra call is where it gets confused and reads a stale file. You didn't add caution. You added a detour.&lt;/p&gt;

&lt;p&gt;That's the −8.8. Not sabotage. Just imported tuning.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure mode is structural
&lt;/h2&gt;

&lt;p&gt;Here's the part that bothers me more than the numbers. A shared &lt;code&gt;AGENTS.md&lt;/code&gt; that everyone appends to converges on the average of everyone's models. It's a committee document, and committee documents are optimized for not being wrong in the meeting where they're approved.&lt;/p&gt;

&lt;p&gt;So you get a file that's slightly useful for the flagship, mildly harmful for the cheap model, and nobody can tell, because nobody's measuring per-model. The file grows. Skills get added after incidents and never removed, because removal requires evidence and addition requires only a bad afternoon.&lt;/p&gt;

&lt;p&gt;That's not a knowledge base. That's scar tissue.&lt;/p&gt;

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

&lt;p&gt;Three rules, none of them clever.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Split the file.&lt;/strong&gt; &lt;code&gt;conventions.md&lt;/code&gt; is shared, versioned, reviewed, and kept brutally short — only facts that are true regardless of what's generating the tokens. &lt;code&gt;skills/&lt;/code&gt; is per-model, and it's a manifest, not a library. Each model gets the subset that was measured to help &lt;em&gt;it&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ablate against your own failures.&lt;/strong&gt; I keep a set of about thirty tasks my agents used to fail: real ones, pulled from logs, each with a pass/fail check. Running a skill pack with and without, at the same temperature, on the same tasks, is maybe forty minutes of compute for a small model and worth every minute. Pass rate, not vibes. If a skill is neutral-to-negative on your task set, it doesn't ship, no matter how smart the paragraph reads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Require an incident for every skill.&lt;/strong&gt; If I can't name the run that motivated it, it doesn't go in. Skills are antibiotics; prescribing them prophylactically is how you get resistance.&lt;/p&gt;

&lt;p&gt;The cost math is worth doing once. A skill that adds one extra tool call per turn on a multi-step agent task is not a small tax — round-trips dominate latency in tool-using loops, and you're now paying it on every turn of every task, including the 90% that were fine. Multiply by your daily volume and ask whether the marginal reliability was worth it. Usually the answer is yes for one skill and no for eleven.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I might be wrong
&lt;/h2&gt;

&lt;p&gt;If your stack is a single model — one provider, one checkpoint, no local fallback — then this whole post is moot. Your shared skills file &lt;em&gt;is&lt;/em&gt; your tuning, and the ablation is just "does it help my one model," which is a much easier question. The problem is specifically the polyglot setup: the moment you run two models, or upgrade one, your skill library silently becomes a bet you didn't know you placed.&lt;/p&gt;

&lt;p&gt;And it expires. The skill you wrote to fix checkpoint N's overconfidence might be actively harmful at N+2, because the failure mode got trained away. Skills have a half-life. Most teams have no process for noticing.&lt;/p&gt;

&lt;p&gt;So the next time a colleague drops a skills repo into the shared workspace and says "this'll help everyone" — the honest reply is: help me measure it first, per model, on our failures. If it's convention, I'll take it today. If it's a prior, show me the delta on the small one. That's the model that's actually running half the tickets anyway.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>agents</category>
    </item>
    <item>
      <title>A repair layer is not a reliability layer</title>
      <dc:creator>Aamer Mihaysi</dc:creator>
      <pubDate>Mon, 26 Oct 2026 14:38:02 +0000</pubDate>
      <link>https://dev.to/o96a/a-repair-layer-is-not-a-reliability-layer-2f7n</link>
      <guid>https://dev.to/o96a/a-repair-layer-is-not-a-reliability-layer-2f7n</guid>
      <description>&lt;p&gt;I found a bug in my own stack last month that had already been "fixed" for three weeks.&lt;/p&gt;

&lt;p&gt;One of my tools takes a timeout in milliseconds. The model had been sending &lt;code&gt;"30s"&lt;/code&gt;. The call succeeded anyway, because somewhere between the model and the tool there was a coercion shim I'd written in a hurry and then forgotten about. It parsed the string, multiplied, passed the int through. Green check. Nobody looked at it again.&lt;/p&gt;

&lt;p&gt;That shim is the whole myth in one line: a proxy that silently repairs malformed tool calls does not make your agent more reliable. It moves the failure from a visible error into an invisible correction. Your success rate goes up. Your tool-call accuracy does not move at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it feels like it works
&lt;/h2&gt;

&lt;p&gt;Because the dashboard agrees with you. Before the gateway, you had a wall of validation errors and a handful of broken trajectories. After the gateway, the errors are gone and the trajectories complete. Every signal you have says the system got better.&lt;/p&gt;

&lt;p&gt;But nothing about the model changed. It still guesses wrong at the same rate. You just stopped counting the guesses. What you measured wasn't accuracy — it was the width of your repair heuristics. Those are different numbers, and only one of them is about the model.&lt;/p&gt;

&lt;p&gt;I've watched this bite in three places.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The schema stops being a contract.&lt;/strong&gt; Once a layer downstream will accept &lt;code&gt;"30s"&lt;/code&gt;, &lt;code&gt;30&lt;/code&gt;, and &lt;code&gt;"30000"&lt;/code&gt;, your schema is no longer a specification. It's a suggestion with a parser behind it. And that parser is now the least-reviewed, least-tested code in the entire stack, running on every single call. I've never once seen a repair heuristic with a test suite. I have seen plenty with a comment that says &lt;code&gt;# temp&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The model never learns it guessed.&lt;/strong&gt; This is the part people skip. Inside a single trajectory, the error message &lt;em&gt;is&lt;/em&gt; the feedback signal. You return &lt;code&gt;{"error": "timeout_ms must be an integer"}&lt;/code&gt; as a tool result, the model reads it, and on the next turn it sends an integer. That's the loop working. A silent repair cuts the loop. The model gets a success result for a call it got wrong, and it will make the same guess for the rest of the session, and in every session after that, forever. You've trained your prompt into a shape that only works because of the shim.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You lose the ability to tell whether a model upgrade helped.&lt;/strong&gt; Swap in a newer model, run your evals, watch the numbers. They'll look identical, because the repair layer is absorbing the difference. You can't distinguish "this model emits better tool calls" from "my heuristics got broader last Tuesday." That's the expensive one. You're paying for evals that can't see the thing they're evaluating.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest version of the argument
&lt;/h2&gt;

&lt;p&gt;There is a real case for lenient parsing. Dates. Numeric strings from HTML forms. Anything where the ambiguity is in the &lt;em&gt;encoding&lt;/em&gt;, not in the model's understanding of the task. If a tool legitimately accepts &lt;code&gt;2024-03-01&lt;/code&gt; and &lt;code&gt;2024-03-01T00:00:00Z&lt;/code&gt;, normalizing both is not a repair, it's just parsing. Fine. Do it.&lt;/p&gt;

&lt;p&gt;The distinction I care about is whether the correction is &lt;strong&gt;loud and counted&lt;/strong&gt;. Coerce the date, but increment a counter and log the raw input. If that counter is at zero for a week, delete the coercion. If it's climbing, you have a prompt problem you can now actually see.&lt;/p&gt;

&lt;p&gt;Silent repair is different in kind. It's a correction with no counter, no log line, and no owner. That's not a reliability feature. That's a metric laundering service.&lt;/p&gt;

&lt;p&gt;And yes — there's a trade I'd take. Customer-facing chat where a failed tool call is worse than a slightly wrong one, demo day, a migration window where you need the old clients to keep working. Silent repair is defensible there. But say it out loud, put an expiry date on it, and stop calling it reliability. It's a tourniquet. Tourniquets have a place. Nobody puts one on a healthy arm and calls it fitness.&lt;/p&gt;

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

&lt;p&gt;Strict validation at the tool boundary. No coercion unless it's declared in the schema as an accepted union.&lt;/p&gt;

&lt;p&gt;On failure, return the validation error to the model as a normal tool result, in plain language, with the expected shape. One retry. Then fail the step loudly and let the trajectory die where it died.&lt;/p&gt;

&lt;p&gt;Log the raw arguments &lt;em&gt;before&lt;/em&gt; validation, always. The bug report you'll eventually get is "it did something weird yesterday," and raw args are the only thing that answers it.&lt;/p&gt;

&lt;p&gt;Count validation failures as a first-class metric, next to latency and cost. If that number is flat while your task success rate climbs, something else is doing the work and you should find out what.&lt;/p&gt;

&lt;p&gt;And if you already have a repair layer you can't remove today: run it in shadow mode. Let it compute the correction, log what it &lt;em&gt;would&lt;/em&gt; have fixed, and don't apply it. Two weeks of that tells you exactly how much of your reliability is real and how much is the shim. My guess is you'll be surprised in the bad way. Mine was.&lt;/p&gt;

&lt;p&gt;The fixes that actually moved my agents' reliability this year were all subtractions — narrower schemas, fewer tools, stricter types, more failures surfaced. I haven't added a single forgiving layer that I didn't later regret.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>agents</category>
    </item>
    <item>
      <title>Your spend ceiling has to be a 403, not a dashboard</title>
      <dc:creator>Aamer Mihaysi</dc:creator>
      <pubDate>Sun, 25 Oct 2026 14:37:34 +0000</pubDate>
      <link>https://dev.to/o96a/your-spend-ceiling-has-to-be-a-403-not-a-dashboard-55gb</link>
      <guid>https://dev.to/o96a/your-spend-ceiling-has-to-be-a-403-not-a-dashboard-55gb</guid>
      <description>&lt;p&gt;The agent that bankrupted its operator while scanning DN42 didn't malfunction. That's the part that bothers me. It ran a loop, got results, adjusted, ran the loop again. Nothing crashed. Nothing hallucinated. It just kept going, because nothing in its world told it that going costs money.&lt;/p&gt;

&lt;p&gt;Cost lived in a dashboard. Dashboards are for humans. Agents don't look at dashboards, and even if you paste a number into the system prompt, that's a suggestion, not a constraint.&lt;/p&gt;

&lt;p&gt;So here's the tip, and it's the only one I care about this week: put the spend ceiling inside the tool wrapper your agent calls, as a hard precondition that fails the call the way a 403 fails. Not a metric. Not an alert. A refusal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the wrapper and not the orchestrator
&lt;/h2&gt;

&lt;p&gt;The obvious place to put a budget check is the agent loop — before each step, check remaining budget, stop if it's gone. That works until you have subagents, parallel tool calls, or a retry policy, and then it stops working in ways that are hard to see.&lt;/p&gt;

&lt;p&gt;The wrapper is better because it's the one chokepoint every side effect passes through. Every HTTP request, every shell command, every model call. If it can spend money, it goes through the wrapper. If it doesn't go through the wrapper, it shouldn't exist in your tool list.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call_tool&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="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;est&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;estimate_cost&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="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# from YOUR price table
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;budget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;try_reserve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;est&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ToolError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BUDGET_EXCEEDED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;retryable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Budget exhausted for task &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Remaining: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;budget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remaining&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; USD. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;This is terminal. Do not retry. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Summarize findings so far and stop.&lt;/span&gt;&lt;span class="sh"&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;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dispatch&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="n"&gt;args&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="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;budget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;settle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;est&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;actual_cost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things in that snippet matter more than the rest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The check runs before dispatch.&lt;/strong&gt; Post-hoc accounting tells you how much you already lost. It's an autopsy, not a guardrail.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It reserves, then settles.&lt;/strong&gt; If ten parallel subagents each read &lt;code&gt;remaining &amp;gt; 0&lt;/code&gt; and all pass, you've blown the ceiling by 10x. Reserve the estimate up front, settle to actual after. The reservation is what makes the ceiling real under concurrency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The error is shaped like a 403, not a 500.&lt;/strong&gt; This is the bit people skip. Your agent's retry logic almost certainly treats tool errors as transient — that's the sane default, networks flake. A budget error is not transient. If it looks retryable, you get a retry storm that burns tokens arguing with a wall. Mark it terminal, say so in the message, and give the model a legal exit: summarize and stop. An agent with no graceful stop will flail, and flailing costs money too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Estimate from your table, not from the model
&lt;/h2&gt;

&lt;p&gt;The model will underestimate. It doesn't know your egress pricing, it doesn't know that this particular endpoint bills per request rather than per token, and it has no idea that a scan is cheap per call and catastrophic per thousand calls.&lt;/p&gt;

&lt;p&gt;So the estimate comes from a table you wrote, keyed by tool name and argument shape. Rough is fine. &lt;code&gt;requests × unit_price&lt;/code&gt;, &lt;code&gt;tokens × price_per_1k&lt;/code&gt;, &lt;code&gt;seconds × gpu_hourly / 3600&lt;/code&gt;. The point isn't precision, it's that the number exists before the call instead of after.&lt;/p&gt;

&lt;p&gt;This is also why a single global "tokens spent" meter is the wrong abstraction. The DN42 case was a scanning task — the cost driver there was almost certainly requests and bandwidth, not completion tokens. Different tools spend different units. One meter can't see all of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put the number in the context too
&lt;/h2&gt;

&lt;p&gt;The hard check is the guardrail. The number in the prompt is the steering wheel. Both, not either.&lt;/p&gt;

&lt;p&gt;I put remaining budget in the task context on every turn: "You have $3.40 left for this task." An agent that can see the number will sometimes choose the cheaper path on its own — fewer pages, a smaller model, a cached result. An agent that can't see it will always choose the thorough path, because thorough is free from where it's standing.&lt;/p&gt;

&lt;p&gt;But never let the model's own arithmetic be the check. It's an input to its planning, not a fact about your account.&lt;/p&gt;

&lt;h2&gt;
  
  
  Set the ceiling low enough that hitting it is boring
&lt;/h2&gt;

&lt;p&gt;If hitting your budget ceiling is an incident, the ceiling is too high. Mine are set at the level where I'd shrug. A per-task ceiling, not just a per-day one — a day budget lets one runaway task eat everything by 9am and then you're debugging a "mystery outage" that's actually your own guardrail working correctly.&lt;/p&gt;

&lt;p&gt;And log every tool call with args and cost, before you need it. The bug report you'll get is "it did something weird yesterday," and the only thing that saves you is a row per call with the price attached.&lt;/p&gt;

&lt;h2&gt;
  
  
  The uncomfortable part
&lt;/h2&gt;

&lt;p&gt;The DN42 agent didn't need better reasoning. It needed a number it couldn't argue with. Every reliability problem I've fixed this year came down to removing freedom, not adding intelligence — and cost control is just another one of those. You're not making the agent smarter about money. You're making it impossible to spend money it doesn't have, and telling it so in a language it already understands: a failed call.&lt;/p&gt;

&lt;p&gt;Give it a cost input. Make it fail like a 403. Then go look at your dashboard, which is now decoration.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>agents</category>
    </item>
    <item>
      <title>The Leak Isn't in the Retrieval. It's in the Sentence.</title>
      <dc:creator>Aamer Mihaysi</dc:creator>
      <pubDate>Sat, 24 Oct 2026 14:37:34 +0000</pubDate>
      <link>https://dev.to/o96a/the-leak-isnt-in-the-retrieval-its-in-the-sentence-522j</link>
      <guid>https://dev.to/o96a/the-leak-isnt-in-the-retrieval-its-in-the-sentence-522j</guid>
      <description>&lt;p&gt;The comforting myth goes like this: an agent leaks when it reaches past its permissions. Fix the permissions, fix the leak. So we put ACLs on the vector store, tenant tags on every chunk, row-level security on the SQL tool, and we sleep fine.&lt;/p&gt;

&lt;p&gt;I've built that stack. It's necessary and it's not enough, and the reason is structural rather than a bug in my filters.&lt;/p&gt;

&lt;p&gt;Here's the shape of the thing. Every document in the leak was individually permitted. The person asking had legitimate access to all of them. Nothing was over-fetched. The disclosure exists only in the composition — in the sentence the agent wrote after reading two documents that were each, on their own, completely fine to read.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two documents, one sentence
&lt;/h2&gt;

&lt;p&gt;Concrete version, because abstractions hide the problem. Internal support agent, retrieval over the company wiki plus the CRM.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Doc A: "Pricing changes take effect on the first of the month following the announcement."&lt;/li&gt;
&lt;li&gt;Doc B: "Acme's renewal is booked for the 14th."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both permitted to the account manager. Both individually boring. The agent, being helpful, writes: "Acme renews two weeks after the new pricing takes effect, so expect an upsell conversation."&lt;/p&gt;

&lt;p&gt;That sentence is a leak. Not of either document — of the inference. It's a sales-sensitive claim about a named customer's negotiating position, assembled from two innocuous facts, and it now lives in a ticket that three other people can read. Nobody's ACL was violated. The account manager had every right to both documents. What they didn't have was a grant to have the inference drawn for them and then written into a shared artifact.&lt;/p&gt;

&lt;p&gt;This is the mosaic effect, and it's older than LLMs. Intelligence services have compartmented around it for decades. What's new is that we've handed the mosaic assembly to a machine that does it automatically, cheerfully, and at the speed of a token stream.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why per-document control can't catch it
&lt;/h2&gt;

&lt;p&gt;Access control is a predicate on one object, evaluated at one moment, by one principal. The leak is a property of a &lt;em&gt;set&lt;/em&gt; of objects, evaluated after generation, against a &lt;em&gt;sink&lt;/em&gt; that may be broader than the conversation. Those are different shapes. You cannot express "this sentence requires two compartments to be true" as a filter on a chunk.&lt;/p&gt;

&lt;p&gt;And you can't fix it by tightening the predicate, either. The agent needs both documents to answer the legitimate question. If I strip Doc B because it touches renewal dates, the account manager gets a worse answer to a question they're allowed to ask. Utility and safety are pulling in opposite directions here, which is exactly the sign that you're enforcing at the wrong layer.&lt;/p&gt;

&lt;p&gt;It gets worse in three ways that took me a while to appreciate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Chunks aren't documents.&lt;/strong&gt; Retrieval hands the model 400-token slices. A slice can contain the qualifying clause that made a sentence safe, or lose it. The unit of permission and the unit of context are not the same unit, and the mismatch is where the sharp edges live.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agents accumulate.&lt;/strong&gt; A single-turn RAG lookup is one mosaic. An agent with a session, a scratchpad, and six tool calls is a mosaic being assembled incrementally, and the dangerous combination may span turn two and turn nine with nothing in between that looks suspicious.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The model will not save you.&lt;/strong&gt; It has no idea who's asking. The system prompt says "you are a helpful assistant with access to the knowledge base," and it is doing precisely that. Asking it to "be careful about sensitive combinations" is asking it to run an access-control policy it cannot see the inputs to. I've tried. It degrades into either useless vagueness or confident silence, and it does so non-deterministically, which is worse than either.&lt;/p&gt;

&lt;h2&gt;
  
  
  Move the check to the draft
&lt;/h2&gt;

&lt;p&gt;If the leak is a property of the sentence, check the sentence.&lt;/p&gt;

&lt;p&gt;The question I want answered, per sentence in the drafted answer: &lt;em&gt;does this sentence require two clearances to be true?&lt;/em&gt; Not "did the retrieval stay in bounds" — it did. Not "is this document allowed" — it is. The question is whether the truth of this specific claim depends on facts that live in different compartments, where the combination itself was never granted to anyone.&lt;/p&gt;

&lt;p&gt;Rough pipeline I've been prototyping:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Label chunks with compartments, not just ACLs.&lt;/strong&gt; An ACL says who may read this. A compartment says which body of knowledge this belongs to. Two documents can be readable by the same person and still belong to different compartments. That distinction is the whole ballgame and most vector stores don't have a field for it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Force attribution.&lt;/strong&gt; Make the model cite chunk IDs per sentence, or run a cheap entailment pass afterwards to recover the support set. Attribution is unreliable and I won't pretend otherwise, but "which chunks does this sentence lean on" is a much easier question than "is this sentence safe."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Compute the compartment set per sentence.&lt;/strong&gt; Union of the labels of its supporting chunks. If that union spans more than one compartment, the sentence is a candidate mosaic.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Grade the sink.&lt;/strong&gt; The same sentence in a private chat reply is a smaller event than the same sentence written to a shared ticket, a Slack channel, or an outbound email. The policy should care where the text is going, not just what it says. This is the part I think most teams will skip and most teams will regret skipping.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Then decide: generalize, redact, or refuse.&lt;/strong&gt; Usually the right move isn't blocking. It's "Acme's renewal timing may be affected by the pricing change — check with the account owner." Same information, no inference, points at a human who's allowed to make the call.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The cheap version
&lt;/h2&gt;

&lt;p&gt;Full attribution on every sentence is expensive and I haven't run it at scale, so take this with the appropriate salt. But there's a shortcut that gets most of the value: track which compartments entered the context window during the session. Most sessions never cross one. When a second compartment shows up, flip on the strict pass for the rest of the session. You pay the cost on the small fraction of conversations that are actually risky, and you leave the boring ones alone.&lt;/p&gt;

&lt;p&gt;The other thing I'd do tomorrow, and should have done a year ago: log the drafted sentence alongside its support set. My current logs tell me which documents the agent pulled. They tell me nothing about what it concluded. When someone eventually asks "how did this get into the ticket," the retrieval log is not the artifact I need.&lt;/p&gt;

&lt;h2&gt;
  
  
  The uncomfortable part
&lt;/h2&gt;

&lt;p&gt;I don't think this is fully solvable. Composition risk is inherent to any system that reads broadly and writes fluently, and the honest framing is risk reduction, not prevention. What I do think is that per-document access control has been quietly doing the job of a security theater prop for agent systems — it looks like the control, it satisfies the review, and it is aimed at a different failure than the one that will actually bite you.&lt;/p&gt;

&lt;p&gt;The check has to live on the answer. That's the whole post.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>agents</category>
    </item>
    <item>
      <title>Field note: the abstention threshold was the whole project</title>
      <dc:creator>Aamer Mihaysi</dc:creator>
      <pubDate>Fri, 23 Oct 2026 14:37:50 +0000</pubDate>
      <link>https://dev.to/o96a/field-note-the-abstention-threshold-was-the-whole-project-3mdl</link>
      <guid>https://dev.to/o96a/field-note-the-abstention-threshold-was-the-whole-project-3mdl</guid>
      <description>&lt;p&gt;The number that decided this project was 0.60, and it isn't in any model card.&lt;/p&gt;

&lt;p&gt;I spent six weeks on an IT ticket router — roughly 4,000 tickets a day, eleven queues, all of it triaged by hand. The demo took an afternoon. The part that actually shipped took the other five weeks and nine days, and almost none of it was modelling.&lt;/p&gt;

&lt;p&gt;Here's the field note.&lt;/p&gt;

&lt;h2&gt;
  
  
  The demo that fooled me
&lt;/h2&gt;

&lt;p&gt;Prompted LLM, ticket body in, queue name out, few-shot with eight examples per queue. On a 2,000-ticket holdout it hit 94%. The service desk lead smiled. My manager smiled. I smiled, which in retrospect was the least justified of the three.&lt;/p&gt;

&lt;p&gt;Then I asked the only question that matters: what happens on the other 6%?&lt;/p&gt;

&lt;p&gt;Silence. Because the answer was "they get routed wrong, silently, and someone finds out two days later when the SLA is already red."&lt;/p&gt;

&lt;p&gt;A 94% classifier with no abstention isn't a 94% system. It's a 100% system that's wrong 6% of the time. Those are very different products.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pricing conversation
&lt;/h2&gt;

&lt;p&gt;I sat down with the service desk lead with a blank spreadsheet and one question: what does a wrong route cost you, relative to a clean one?&lt;/p&gt;

&lt;p&gt;She thought about it and said five times. Rework, the SLA clock restarting, the context switch for whoever picks it up second, and the occasional ticket that bounces through three queues before someone just calls the user.&lt;/p&gt;

&lt;p&gt;I wrote 5x in cell B2. That cell is a guess. It's a guess from the person who actually eats the cost, which makes it a good guess, but it's still a guess — and it is now the single most load-bearing number in the entire system. More than the accuracy. More than the model. If she'd said 2x, the design changes. If she'd said 10x, it changes the other way.&lt;/p&gt;

&lt;p&gt;That conversation is the engineering work. Everything after it is arithmetic.&lt;/p&gt;

&lt;h2&gt;
  
  
  The confidence problem
&lt;/h2&gt;

&lt;p&gt;So I went to build abstention on top of the LLM. And it didn't work, for a reason I should have seen coming.&lt;/p&gt;

&lt;p&gt;Ask a prompted model how confident it is and you get a sentence. "Confidence: high." On tickets it got right, it said high. On tickets it got wrong, it also said high. I pulled token logprobs over the label tokens and they were uncalibrated in the way that matters — a 0.9 meant "this is the most likely token," not "this is right 90% of the time." I tried a verbalized 1-10 scale, then a self-consistency sample of five, then a second model as a judge. Each one cost more per ticket and each one gave me a score I couldn't put a threshold on without lying to myself.&lt;/p&gt;

&lt;p&gt;You can calibrate an LLM's outputs. But by the time you've built the calibration layer, the held-out set, and the threshold sweep, you have built a router. The LLM is now a component inside it, not the thing itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually shipped
&lt;/h2&gt;

&lt;p&gt;TF-IDF, word 1-2 grams plus character 3-5 grams, into a logistic regression. Isotonic calibration fit on a held-out slice, checked against a reliability diagram because I don't trust myself. Inference is about 8ms on one core. No GPU. The whole model is a few megabytes and it fits in the same container as the API.&lt;/p&gt;

&lt;p&gt;Then the sweep. For each threshold from 0.30 to 0.95, compute total daily cost: inference, plus misroutes at 5x, plus the human cost of everything that abstains. Plot it. It's a U-curve, and the bottom of the U is the only number I cared about.&lt;/p&gt;

&lt;p&gt;At 0.60, on my traffic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;78% auto-routed, 99.1% precision on that slice&lt;/li&gt;
&lt;li&gt;22% abstained to a human&lt;/li&gt;
&lt;li&gt;~31 misroutes a day instead of ~240&lt;/li&gt;
&lt;li&gt;total cost roughly a third of the always-route LLM version&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The LLM version was more accurate overall. It lost on cost anyway, because accuracy is the wrong objective when errors are five times more expensive than abstentions. That's the whole trick and it took me an embarrassingly long time to internalise it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The abstain path is not "give up"
&lt;/h2&gt;

&lt;p&gt;The first version of abstention just dumped the ticket into a human queue. That's not abstention, that's a shrug with extra steps, and the humans noticed immediately.&lt;/p&gt;

&lt;p&gt;What fixed it: the abstained ticket arrives with the top three candidate queues and a one-line summary of what the ticket is actually asking for. Triage time on those dropped from about 90 seconds to about 40. That single change moved the cost curve more than any modelling I did, because it made abstention cheap. Cheap abstention is what lets you set the threshold low and be honest about uncertainty.&lt;/p&gt;

&lt;p&gt;The LLM still earns its keep — it writes those summaries and ranks the candidates on the 22% that abstains. That's about 880 calls a day. It's a good job for it. It's just not the router.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things I'd tell someone starting this
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Write the price down, in the config, with a name next to it.&lt;/strong&gt; My threshold lives in a YAML file with a comment: &lt;code&gt;# 0.60 — agreed with service desk, misroute = 5x triage, reviewed quarterly&lt;/code&gt;. When someone asks why we abstain on a fifth of the traffic, I don't have to reconstruct an argument from memory. And when the price changes, the threshold is a one-line diff, not a retrain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Calibrate, then verify you calibrated.&lt;/strong&gt; Logistic regression on TF-IDF is roughly calibrated out of the box, which is most of why I picked it. Boosted trees are not. LLM logprobs are definitely not. Isotonic regression on a held-out slice, then look at the reliability diagram with your own eyes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watch the abstain rate like a canary.&lt;/strong&gt; Mine sits at 22%. If it drifts to 35% next month, something upstream changed — a new product line, a ticket template rewrite, a queue that got renamed. That drift signal is free and it caught a broken intake form before anyone filed a bug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't let the threshold be a vibe.&lt;/strong&gt; I've seen "0.7 feels right" in more codebases than I'd like. It's not a hyperparameter. It's a business decision wearing a float costume.&lt;/p&gt;

&lt;p&gt;I don't think this generalises to everything. If your errors are cheap and symmetric, route everything and skip the abstention machinery entirely — you'll save yourself a month. But if a wrong answer costs meaningfully more than a handoff, the threshold is the product, and the model is a detail you get to pick afterwards.&lt;/p&gt;

&lt;p&gt;Mine is 0.60. It'll probably move next quarter. That's fine. That's the job.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>agents</category>
    </item>
    <item>
      <title>The Number I'll Cite Next Year Won't Come From the Paper</title>
      <dc:creator>Aamer Mihaysi</dc:creator>
      <pubDate>Thu, 22 Oct 2026 14:37:48 +0000</pubDate>
      <link>https://dev.to/o96a/the-number-ill-cite-next-year-wont-come-from-the-paper-33ik</link>
      <guid>https://dev.to/o96a/the-number-ill-cite-next-year-wont-come-from-the-paper-33ik</guid>
      <description>&lt;p&gt;I lost a week in March to a flat reward curve.&lt;/p&gt;

&lt;p&gt;Not a dramatic failure. Loss went down, reward went up, and the model got quietly worse at the thing I actually cared about. I blamed my data. I blamed the learning rate. I blamed the GPU. Then I diffed my config against the appendix and found three differences: a different chat template, a reward model checkpoint two versions behind, and a KL coefficient that had been changed in a footnote I skimmed past.&lt;/p&gt;

&lt;p&gt;None of that is fraud. It's just that the setup was never built to be re-run by someone who wasn't in the room.&lt;/p&gt;

&lt;p&gt;That week is why I think the headline number in RL post-training papers is about to stop being the number people cite. Not because the papers are dishonest — most aren't — but because the field is starting to notice that a benchmark table scored on a setup nobody re-runs is a claim, not a measurement.&lt;/p&gt;

&lt;h2&gt;
  
  
  RL makes this worse than SFT, and it's not close
&lt;/h2&gt;

&lt;p&gt;With supervised fine-tuning you have a cheap sanity check: look at the outputs. If the model writes garbage, you know before you ship. The loss can lie but the samples usually don't.&lt;/p&gt;

&lt;p&gt;RL post-training inverts that. The reward is the ground truth, and the reward is the single most likely thing to be broken. Reward hacking means the number goes up while the model gets worse — that's not an edge case, that's the default failure mode. And the only reliable detector for it is somebody with no stake in the result running the same pipeline and reporting what they saw.&lt;/p&gt;

&lt;p&gt;So when a paper reports +6 on some preference benchmark, what I actually want to know is: who else ran this? Not "did they release code" — code release is necessary and nowhere near sufficient. I've cloned repos where the training script exists, the data pipeline is a &lt;code&gt;TODO&lt;/code&gt;, and the reward model is described as "a 7B preference model we trained." Cool. On what. With which annotators. Judged by which snapshot of which API, because that endpoint has been updated twice since you ran it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The held-out test that actually counts
&lt;/h2&gt;

&lt;p&gt;Here's the eval I trust: an unaffiliated maintainer reimplements the method from the paper plus the repo, hits roughly the same direction of result, and it still beats the baseline they chose themselves.&lt;/p&gt;

&lt;p&gt;That's it. That's the whole benchmark. It's slow, it's unglamorous, it doesn't fit in a tweet, and it's the only signal that survives contact with a different cluster, a different data order, and a person who has no incentive to make the numbers work.&lt;/p&gt;

&lt;p&gt;I've watched this happen twice now in the last year — a method that looked marginal in the paper turned out to be solid once someone else got it running, and a method that looked strong quietly stopped working when the reward model was swapped for an open one. Both times the second implementation was more informative than the original table. Both times it was a blog post with a repo attached, not a paper.&lt;/p&gt;

&lt;h2&gt;
  
  
  The prediction
&lt;/h2&gt;

&lt;p&gt;Within about eighteen months, "independently reproduced" becomes a label people cite. Not a vibe, not a footnote — a line item.&lt;/p&gt;

&lt;p&gt;Concretely, I expect to see it in three places. Model cards grow a reproduction section next to the eval table, and an empty one reads as a warning. Leaderboards add a column for it, the way they added "verified" for submissions that came with runnable code. And release posts start putting the reproduction link in the first paragraph instead of burying the arXiv link at the bottom, because the reproduction is the thing that convinces a working engineer and the paper is the thing that convinces a reviewer.&lt;/p&gt;

&lt;p&gt;The part I'm less sure about is who pays for it. Reproduction is compute, and compute is the thing nobody wants to donate to someone else's result. My bet is that labs start budgeting a small slice of training spend to fund an outside run — a few thousand dollars against a training run that cost six figures is a rounding error, and it buys the one credential that a table can't. If that happens, the label gets real fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  What would have to change for the label to mean anything
&lt;/h2&gt;

&lt;p&gt;A reproduction is only as good as the artifacts. Right now "we release code" can mean anything from a full pipeline to a loss function and good luck. The minimum viable version:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pinned eval harness version, in the repo, not "we used lm-eval-harness."&lt;/li&gt;
&lt;li&gt;Reward model checkpoint with a hash, or an explicit statement that it's proprietary and the numbers are therefore not reproducible.&lt;/li&gt;
&lt;li&gt;The judge prompt, verbatim, plus the judge model snapshot date.&lt;/li&gt;
&lt;li&gt;Seeds and data order, because in RL they matter more than people admit.&lt;/li&gt;
&lt;li&gt;A baseline that the reproducer gets to pick, not the one that flatters the method.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one is the important one. A reproduction that only beats the author's weakest baseline isn't a reproduction, it's a remix.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I do in the meantime
&lt;/h2&gt;

&lt;p&gt;I don't read the table first anymore. I search for a second implementation. If there's only one, I budget a week to reproduce and assume I'll lose it — and I've started writing down where it broke, because that write-up is the artifact I keep wishing already existed when I'm the one searching.&lt;/p&gt;

&lt;p&gt;Maybe I'm wrong about the timeline. Maybe labs get good enough at shipping artifacts that the label is redundant, or maybe the field keeps citing tables because citing tables is cheap and reproducing is expensive and nobody gets promoted for confirming someone else's result. The incentive problem is real and I don't have a fix for it.&lt;/p&gt;

&lt;p&gt;But I know which number I'll be quoting next year, and it won't be the one in the abstract.&lt;/p&gt;

&lt;p&gt;The paper tells you what the authors believe. The reproduction tells you what's true.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>agents</category>
    </item>
    <item>
      <title>You don't read the code. Fine. What do you read?</title>
      <dc:creator>Aamer Mihaysi</dc:creator>
      <pubDate>Wed, 21 Oct 2026 14:37:33 +0000</pubDate>
      <link>https://dev.to/o96a/you-dont-read-the-code-fine-what-do-you-read-3fkk</link>
      <guid>https://dev.to/o96a/you-dont-read-the-code-fine-what-do-you-read-3fkk</guid>
      <description>&lt;p&gt;A repo crossed my feed this week — &lt;code&gt;window-ml&lt;/code&gt; — where the code is written and maintained by agents, and the author says plainly that he doesn't read it. Not "I skim it." Not "I review the important parts." He doesn't read it.&lt;/p&gt;

&lt;p&gt;The replies did what replies do. Clutched pearls, invoked the sanctity of review, implied the man is one bad merge from deleting prod.&lt;/p&gt;

&lt;p&gt;I think he's right, and I think he's also one lazy week away from being very wrong. Both things.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nobody was reading the code anyway
&lt;/h2&gt;

&lt;p&gt;Let's be honest about what code review actually was on a large service. You read the diff. You read the parts that touch auth, money, and the migration. You skimmed the rest and told yourself the tests would catch it. On a 40k-line repo, "I reviewed it" meant "I sampled it and I trust the sampler."&lt;/p&gt;

&lt;p&gt;Agents didn't invent sampling. They changed the ratio. The volume of code went up by an order of magnitude and the reading stayed flat, so the sample rate collapsed. That's the actual news here, and it happened to most teams quietly, without anyone announcing it. The researcher in question just said the quiet part at normal volume.&lt;/p&gt;

&lt;h2&gt;
  
  
  The review didn't disappear, it moved up a level
&lt;/h2&gt;

&lt;p&gt;Here's the part I'd defend in a design review. When code is cheap to produce, the code stops being the artifact. The artifact is the harness — the eval set, the fixtures, the assertions, the scorer, the seeds, the thing that decides whether the output was good.&lt;/p&gt;

&lt;p&gt;That's where the judgment lives now. And it's a real place to put judgment, because the harness is smaller, denser, and it's the only thing that can tell you if any of the generated code did what it claimed.&lt;/p&gt;

&lt;p&gt;So "I don't read the code" is not a complete sentence. It's a sentence with the second half missing. The defensible version is: &lt;em&gt;I don't read the code, I read the harness.&lt;/em&gt; The indefensible version is just the first clause, and it means you have no review at all — you have a deploy button and a prayer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually check now
&lt;/h2&gt;

&lt;p&gt;I've been doing this long enough to have a routine, and it's not glamorous.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Break the thing and watch the test go red.&lt;/strong&gt; Every agent-written PR that claims a fix gets the same treatment: I revert the fix, run the test, and see if it fails. If it stays green, the test is decoration. I've caught more fake tests this way than I've caught real bugs, which tells you something about the base rate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check whether the eval set contains the case that broke.&lt;/strong&gt; If the bug that prompted the PR isn't in the eval, the PR fixed one instance and taught the system nothing. The eval is the memory. Without it you're re-fixing the same bug with different variable names every three weeks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Look at the blast radius, not the logic.&lt;/strong&gt; I don't care how the function is written. I care that the diff added a network call, a new dependency, an env var, or a file write outside the sandbox. Agents are extremely good at adding a dependency that duplicates one you already have, and extremely bad at mentioning it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ask whether I could delete it and regenerate it.&lt;/strong&gt; If the answer is yes and nothing changes, the code was never the asset. The harness was. That's not an argument against reading code — it's an argument about which code deserves the reading.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this falls apart
&lt;/h2&gt;

&lt;p&gt;Two places, and I want to be specific because the hot take version of this post would just say "agents are the future, old man."&lt;/p&gt;

&lt;p&gt;First: agent failure modes are boring and invisible to accuracy evals. Silent exception swallowing. A retry loop that turns one 500 into a bill. A test that passes because it asserts nothing. A scorer that returns 1.0 for an empty string. None of that shows up in a benchmark that measures whether the output was correct. So the harness has to include the unglamorous telemetry — cost per run, wall clock, outbound call count, diff size over time. I log token spend and tool calls per run, and that log has caught more regressions than any eval I've written.&lt;/p&gt;

&lt;p&gt;Second, and this is the one that actually worries me: the harness is also code. Someone wrote the scorer. If the scorer was agent-written and unread, you've built a mirror. You're checking generated output against generated judgment and calling the agreement a pass. That's not review, that's a confidence trick you played on yourself.&lt;/p&gt;

&lt;p&gt;So I read the scorer by hand. Always. It's usually the smallest file in the repo and it decides everything. Same for the fixture loader. Everything else can be agent-written and skimmed, and I'll sleep fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question worth asking
&lt;/h2&gt;

&lt;p&gt;The pearl-clutching replies are asking the wrong thing. "Did you read the code" is unanswerable at scale and was already a polite fiction. The question is: &lt;em&gt;what did you read instead, and would you bet the deploy on it?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If the answer is the eval suite, the fixtures, and the cost log — good. That's a real review process, and it's probably stricter than what most teams were doing before, because it's written down and it runs on every commit instead of living in someone's head.&lt;/p&gt;

&lt;p&gt;If the answer is nothing — if the code is agent-written, the tests are agent-written, the scorer is agent-written, and nobody has read any of it — then you don't have a new workflow. You have an unowned system with a CI badge.&lt;/p&gt;

&lt;p&gt;The researcher shipping &lt;code&gt;window-ml&lt;/code&gt; gets away with it because he's explicit about where his attention went. That's the whole trick. Not that he stopped reviewing. That he moved the review to the place that can still catch him.&lt;/p&gt;

&lt;p&gt;Most teams haven't moved it anywhere. They just stopped.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>agents</category>
    </item>
  </channel>
</rss>
