<?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>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>
    <item>
      <title>Max effort lost to medium effort — notes on a visual agent harness</title>
      <dc:creator>Aamer Mihaysi</dc:creator>
      <pubDate>Tue, 20 Oct 2026 14:37:59 +0000</pubDate>
      <link>https://dev.to/o96a/max-effort-lost-to-medium-effort-notes-on-a-visual-agent-harness-4f0c</link>
      <guid>https://dev.to/o96a/max-effort-lost-to-medium-effort-notes-on-a-visual-agent-harness-4f0c</guid>
      <description>&lt;p&gt;I have a bad habit. When an agent fails a task, I turn the reasoning dial up. More thinking, more tokens, more chances to stumble into the right answer. It works just often enough to keep the habit alive, which is how bad habits survive in this field.&lt;/p&gt;

&lt;p&gt;A paper I read this week argues pretty directly that I've been buying the wrong thing. &lt;a href="https://arxiv.org/abs/2610.26860v1" rel="noopener noreferrer"&gt;OpenVLHarness: Real World Visual Agentic Tool-Use with Multimodal Memory&lt;/a&gt; runs a controlled comparison, and the controlled part is the whole story. Same model in every arm. One arm gets the harness at medium effort. The other arms get maximum effort: the bare model, a Codex-style code-as-action setup, and Visual Sketchpad. The harness arm wins, and it wins while spending a fraction of what the max-effort arms burn.&lt;/p&gt;

&lt;p&gt;Sit with that for a second. The model that thought less, with better plumbing, beat the model that thought harder. Not by a rounding error — by a margin you'd notice in a product.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four arms, and what each one is actually buying
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Arm&lt;/th&gt;
&lt;th&gt;What it optimizes&lt;/th&gt;
&lt;th&gt;Where it leaks&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Bare model, max effort&lt;/td&gt;
&lt;td&gt;More sampling per step&lt;/td&gt;
&lt;td&gt;Re-derives state every step from raw tool output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Codex-style code-as-action&lt;/td&gt;
&lt;td&gt;Precise, composable actions&lt;/td&gt;
&lt;td&gt;Great when the world is text; awkward when the observation is visual&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Visual Sketchpad&lt;/td&gt;
&lt;td&gt;Intermediate visual reasoning&lt;/td&gt;
&lt;td&gt;The sketch is per-step, not a memory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Harness, medium effort&lt;/td&gt;
&lt;td&gt;Model-readable, persistent observations&lt;/td&gt;
&lt;td&gt;Needs per-tool renderers, which is real work&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The first three are all reasonable engineering. I've built versions of all three. What they share is that the model keeps paying to re-understand its own situation. Every step, the tool returns something — JSON, a screenshot, a diff, a log tail — and the model has to convert that into an internal representation before it can act. Then the next step arrives and it does the conversion again, because nothing carried over except a growing transcript.&lt;/p&gt;

&lt;p&gt;Max effort is the expensive way to survive that. You're not giving the model more information. You're giving it more attempts at re-parsing the same ambiguous blob. That's a tax, not a capability.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the harness does differently
&lt;/h2&gt;

&lt;p&gt;Two things, and neither of them is clever.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It renders tool output into something the model can read directly.&lt;/strong&gt; Not raw JSON with nested escaping, not a full-resolution screenshot where the relevant widget is forty pixels wide. A representation chosen per tool, at the granularity the next decision needs. This is unglamorous work. It's also most of the win.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It persists that representation across steps.&lt;/strong&gt; The model's understanding of the screen, the document, the file — it lives in a memory the harness maintains, not in a context window that gets truncated at step twelve. So step thirteen starts from a state, not from an archaeology project.&lt;/p&gt;

&lt;p&gt;Put those together and medium effort is enough, because the model isn't spending its budget reconstructing reality. It's spending it on the task.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this should annoy you a little
&lt;/h2&gt;

&lt;p&gt;Because it means the bottleneck was never the model. It was the interface between the model and its own tools, and that interface is something I control. I can't ship a better frontier model. I can absolutely ship a renderer that turns my scraper's output into three lines the model can act on instead of a 4KB HTML fragment it has to squint at.&lt;/p&gt;

&lt;p&gt;I did the arithmetic on my own stack after reading this. A screenshot-heavy agent loop re-sends its history every step. A couple thousand tokens per image, thirty steps, and you're well into six figures of input tokens before the agent has accomplished anything you'd put in a changelog. Most of that spend is the model re-reading the same picture it already understood at step four. That's not reasoning. That's me paying for amnesia.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest caveats
&lt;/h2&gt;

&lt;p&gt;The harness is domain-specific. Someone had to write renderers for those tools, and those renderers encode assumptions about what matters in a visual observation. Change the domain and you rewrite them. That's the trade: you spend engineering time up front to stop spending tokens forever. Whether that's a good deal depends entirely on whether your agent runs once or ten thousand times. For a demo, max effort is fine. For anything with a bill attached, it isn't.&lt;/p&gt;

&lt;p&gt;I also haven't run this myself, and visual agentic tasks are a specific slice of the world. Text-only tool use might not transfer cleanly — my tools return logs and diffs, not pixels, and the rendering problem looks different there. Maybe the persistence half is the bigger lever and the rendering half matters less outside vision. I genuinely don't know yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm changing Monday
&lt;/h2&gt;

&lt;p&gt;Three things, in order of how cheap they are.&lt;/p&gt;

&lt;p&gt;Log every tool call with its full args and full output, before I need it. I already believe this and I still skip it on prototypes, and every time I skip it I spend an afternoon guessing.&lt;/p&gt;

&lt;p&gt;Then write one renderer per tool that produces the smallest representation the next decision actually needs. Not a summary — a &lt;em&gt;rendering&lt;/em&gt;. Deterministic, testable, versioned like any other code.&lt;/p&gt;

&lt;p&gt;Then move state out of the transcript and into a store the harness owns, so the model reads current state instead of replaying history.&lt;/p&gt;

&lt;p&gt;And measure the thing that matters: tokens per solved task, not tokens per step. Per-step cost rewards agents that give up quickly. Per-solved-task cost is the only number that shows up in a budget meeting.&lt;/p&gt;

&lt;p&gt;The uncomfortable lesson from that comparison is that I've been treating reasoning budget as the knob. It's the knob that's easiest to turn and the one that pays back least. The harness is the boring answer, and boring answers are usually the ones that survive contact with prod.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>agents</category>
    </item>
    <item>
      <title>Clean data, dirty model: behaviors don't respect your dataset boundaries</title>
      <dc:creator>Aamer Mihaysi</dc:creator>
      <pubDate>Mon, 19 Oct 2026 14:38:02 +0000</pubDate>
      <link>https://dev.to/o96a/clean-data-dirty-model-behaviors-dont-respect-your-dataset-boundaries-4jm</link>
      <guid>https://dev.to/o96a/clean-data-dirty-model-behaviors-dont-respect-your-dataset-boundaries-4jm</guid>
      <description>&lt;p&gt;The uncomfortable part of red-teaming isn't finding the bad behavior. It's grepping every document you trained on and finding nothing that explains it.&lt;/p&gt;

&lt;p&gt;I've been there. The model starts doing something you don't want — gets terse and dismissive when a user pushes back twice, say — and the first instinct is to go find the example that taught it. You pull the fine-tuning set, you search for the pattern, you come up empty. Every doc is clean. Every doc was reviewed. And the behavior is still sitting in the weights with no paper trail pointing at it.&lt;/p&gt;

&lt;p&gt;The myth is that this means you missed something. It doesn't. It means the model never learned the behavior from a document in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  The myth: clean data in, clean model out
&lt;/h2&gt;

&lt;p&gt;This assumption is baked into most of the AI compliance paperwork I've seen. Dataset provenance, license checks, PII scrubbing, a human review pass, a sign-off. All of it treats the training set as the causal input: if the data is clean, the model is clean. Audit the source, certify the output.&lt;/p&gt;

&lt;p&gt;It's a comforting story because it's checkable. You can put a spreadsheet in front of a reviewer. You can't put a weight matrix in front of one.&lt;/p&gt;

&lt;p&gt;But behaviors aren't items in a list that get copied from documents into the model. They're couplings. During pretraining, the model learns statistical relationships between dispositions — between "confident" and "dismissive", between "helpful" and "eager to please", between "cautious" and "refuses everything". Those couplings form across billions of documents, not within any single one. Then fine-tuning nudges the model into a region of that space, and whatever else lives in that region comes along for the ride.&lt;/p&gt;

&lt;p&gt;Which means the behavior you ship can be absent from every source you audited. Not hidden. Absent. It's a product of the combination, not an ingredient in any one document.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this breaks the audit
&lt;/h2&gt;

&lt;p&gt;Think about what a dataset review actually verifies. It verifies that the documents you fed in don't contain the thing you're worried about. That's a statement about your inputs. It says nothing about the function mapping inputs to weights, and that function is where the coupling happens.&lt;/p&gt;

&lt;p&gt;Three failure modes I've watched play out:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fine-tuning on safe refusals makes models refuse more broadly.&lt;/strong&gt; You train on a few thousand examples of the model declining harmful requests. It learns "declining" as a disposition, not "declining this specific category". Now it hedges on legal questions and adds disclaimers to cooking instructions. Your dataset was clean. Your model got worse.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Training for assertiveness buys you condescension.&lt;/strong&gt; You want the model to stop hedging, so you train on confident answers. Confidence is coupled to other things in the pretrained space. You get a model that states wrong things with the same certainty as right ones. Nothing in your dataset said "be wrong loudly".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Persona fine-tunes leak capabilities sideways.&lt;/strong&gt; You give it a character. The character is snarky. Snark, in the pretrained distribution, co-occurs with a certain kind of adversarial cleverness. Your support bot now writes better jailbreak attempts than your users do.&lt;/p&gt;

&lt;p&gt;None of these are data poisoning. Nobody slipped anything in. The data was exactly what you asked for. The behavior came from the geometry, not the documents.&lt;/p&gt;

&lt;h2&gt;
  
  
  What dataset review is actually good for
&lt;/h2&gt;

&lt;p&gt;I'm not saying throw it out. Provenance matters — you need to know what you're legally allowed to use, you need to catch PII, you need to not train on the same eval set you're going to report on. Those are real controls with real failure modes.&lt;/p&gt;

&lt;p&gt;But they're controls on &lt;em&gt;inputs&lt;/em&gt;, and the thing you're shipping is an &lt;em&gt;output&lt;/em&gt;. Calling dataset review a safety control is like calling a grocery receipt a nutrition plan. It tells you what went in the cart. It doesn't tell you what the meal does to you.&lt;/p&gt;

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

&lt;p&gt;Evaluate the artifact, not the data. That sounds obvious and it's astonishingly rare in practice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Behavioral probes as a regression suite.&lt;/strong&gt; Not benchmarks — probes. Short, adversarial, specific to the dispositions you care about. Run them on every checkpoint and diff the results. If a tone fine-tune shifted refusal rates, you want to see that as a number, not discover it in a support ticket three weeks later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Differential testing against the base model.&lt;/strong&gt; You have the base weights. Run the same probe set on both and look at what moved. The delta is the thing you actually shipped. The absolute score tells you almost nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Probe for couplings you didn't ask for.&lt;/strong&gt; If you trained on X, test for Y and Z. Train on concision, test whether it still explains its reasoning. Train on refusals, test whether it still answers benign edge cases. Assume the coupling is there and go find it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep a canary set you never train on.&lt;/strong&gt; Small, held out, and — this is the part people skip — never used for model selection either. The moment you tune against it, it stops being a canary.&lt;/p&gt;

&lt;p&gt;None of this gives you a certificate. That's the honest trade. Behavioral evaluation is sampling from a distribution you can't enumerate, and it will miss things. But it's measuring the thing you're shipping, which is more than the spreadsheet does.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I'm still unsure about
&lt;/h2&gt;

&lt;p&gt;I haven't reproduced the paper's setup myself, and I'd want to before quoting any of it as settled. Coupling is easy to demonstrate and hard to quantify — I can show you a fine-tune that leaked, I can't tell you the leakage rate. And there's a real risk that "behaviors are coupled" becomes an excuse for not auditing data at all, which would be worse than the myth it replaces.&lt;/p&gt;

&lt;p&gt;But the practical conclusion holds regardless of the exact mechanism: your dataset review tells you what you fed the model. It does not tell you what the model became. Those are different questions, and only one of them is the one your users will hit.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>agents</category>
    </item>
    <item>
      <title>Breadth is the cheap axis</title>
      <dc:creator>Aamer Mihaysi</dc:creator>
      <pubDate>Fri, 16 Oct 2026 14:38:01 +0000</pubDate>
      <link>https://dev.to/o96a/breadth-is-the-cheap-axis-4ffl</link>
      <guid>https://dev.to/o96a/breadth-is-the-cheap-axis-4ffl</guid>
      <description>&lt;p&gt;Seven of eight circuit-design tasks, an LLM-driven search beat the previous state of the art. The paper is about quantum circuits, but the thing I keep coming back to is where it spends its compute.&lt;/p&gt;

&lt;p&gt;It doesn't ask a model to think harder. It asks a model to propose a lot, then spends the real budget on picking. The model is the cheap part of that loop. The search is the expensive part, and the win lives there.&lt;/p&gt;

&lt;p&gt;Look at how most agent stacks spend tokens and you'll see the opposite. One chain of thought, stretched. More "reason carefully." A bigger context window. All depth, no breadth. Depth is what you buy when you have no way to generate alternatives — when your only move is to make the single answer better.&lt;/p&gt;

&lt;h2&gt;
  
  
  The number I actually track
&lt;/h2&gt;

&lt;p&gt;Hit rate at N. Of N candidates, how often does at least one clear the bar?&lt;/p&gt;

&lt;p&gt;If that curve is flat as N grows, your sampler is broken. You're paying eight times for eight copies of the same answer. If it climbs, breadth is doing real work, and you should push N until the curve bends. That bend is your budget line, and it's different for every task. I've never guessed it right, only measured it.&lt;/p&gt;

&lt;p&gt;Diversity is the whole game here. Eight samples at temperature 0.2 with the same prompt is one sample with extra steps. I've had more luck varying the framing than the temperature — the same question asked as a constraint problem, as a search problem, as "what would break this." Different decompositions produce genuinely different candidates. Slight perturbations of the same prompt produce siblings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why nobody does it
&lt;/h2&gt;

&lt;p&gt;Latency, mostly. Fanning out feels slower even when it isn't — eight parallel calls finish in roughly the time of one, if your provider lets you. Then there's cost accounting: per-token billing makes 8x look like 8x, and nobody wants to explain that line item. And the app mental model is one request, one response. Fanning out breaks it, and now your logs are a tree instead of a line.&lt;/p&gt;

&lt;p&gt;All of that is real friction. None of it is a reason the technique doesn't work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it breaks
&lt;/h2&gt;

&lt;p&gt;Tool calls.&lt;/p&gt;

&lt;p&gt;If a candidate is a full agent trajectory, N candidates means N sets of side effects. N writes, N emails, N charges on someone's card. Sampling is nearly free for text and expensive for actions, and the moment your candidate touches the world, breadth stops being the cheap axis.&lt;/p&gt;

&lt;p&gt;Which points at a shape I've been converging on anyway: keep the acting stage single-threaded, and put the breadth upstream, in the part that only produces text. Propose wide, act once. The expensive, irreversible step gets one shot. The cheap, reversible step gets as many as you can afford.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd take from the paper
&lt;/h2&gt;

&lt;p&gt;Not "models are better at physics now." The interesting claim is narrower and more useful: when candidates are cheap to produce and cheap to filter, the winning move is to stop trying to get one answer right and start trying to get a hundred answers fast.&lt;/p&gt;

&lt;p&gt;That's less clever than it sounds. It's also the direction I've been moving my own loops, and it works more often than the clever version did.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://arxiv.org/abs/2610.22367v1" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2610.22367v1&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>agents</category>
    </item>
    <item>
      <title>Your agent control plane is an API gateway with a new name</title>
      <dc:creator>Aamer Mihaysi</dc:creator>
      <pubDate>Thu, 15 Oct 2026 14:38:13 +0000</pubDate>
      <link>https://dev.to/o96a/your-agent-control-plane-is-an-api-gateway-with-a-new-name-1jpp</link>
      <guid>https://dev.to/o96a/your-agent-control-plane-is-an-api-gateway-with-a-new-name-1jpp</guid>
      <description>&lt;p&gt;I spent last Saturday adding per-run cost accounting to my agent fleet. Not a dashboard — a counter that increments on every model call, tagged with the run ID, the tool that fired it, and the person who asked. Ninety lines and one Postgres table. It's ugly and it works.&lt;/p&gt;

&lt;p&gt;Then a colleague sent me preloop, an open-source "control plane for agents." Gateway that meters cost. Approval queue. Tool-call policy layer. Audit log. Four features, one repo, a README that reads like a product page.&lt;/p&gt;

&lt;p&gt;My first reaction was mild embarrassment — I'd just hand-rolled three of those four. My second reaction was the interesting one: I already run all four of those things in production. I just don't call them that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The myth, stated plainly
&lt;/h2&gt;

&lt;p&gt;The pitch goes like this: agents are a new kind of workload, so they need a new kind of governance layer. Model calls are non-deterministic, tool calls have side effects, spend is unbounded, and your existing infrastructure was built for boring deterministic HTTP. Therefore: new category, new product, new thing to deploy.&lt;/p&gt;

&lt;p&gt;I buy the premise and reject the conclusion. The "therefore" is doing all the work and none of the reasoning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Map the four pillars onto what you already run
&lt;/h2&gt;

&lt;p&gt;Take preloop's four features and translate them into boring words.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost metering gateway.&lt;/strong&gt; That's an API gateway with a counter. Envoy, Kong, Traefik, nginx — every one of them has request-level metrics, rate limits, and a plugin hook where you can read a response body. Token counts come back in the response payload. Multiply by your per-token price, tag by route, ship to Prometheus. I'm not being cute here: the hard part of cost metering was never the gateway. It's the price table and the tagging discipline. Neither of those lives in the gateway, and neither of them gets easier because the gateway has "agent" in its name.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Approval queue.&lt;/strong&gt; That's a workflow with a human step. Temporal, Airflow, a Slack bot with two buttons, a Postgres table with a &lt;code&gt;status&lt;/code&gt; column and a cron job. The interesting design question is what happens to the agent's context while it waits — does it hold the whole run in memory, does it checkpoint, does it resume cleanly? No gateway answers that for you, and it's the part that actually bites.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tool-call policy layer.&lt;/strong&gt; That's authorization. OPA, Cedar, or plain RBAC if you're honest about your scale. You already have a policy engine deciding which service can call which endpoint. The only genuinely new thing is that the subject is now "agent X calling tool Y with these arguments" instead of "service A calling route B."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Audit log.&lt;/strong&gt; That's structured logging. You already ship logs somewhere with retention and a query interface. An audit log is a log with a schema and a promise not to delete it. That's a real promise, but it's not a new category of software.&lt;/p&gt;

&lt;p&gt;Four for four. Every pillar has an existing, battle-tested implementation that your team already knows how to operate, page on, back up, and restore at 3am.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the re-skin argument actually breaks
&lt;/h2&gt;

&lt;p&gt;I want to be fair, because "it's just X" is the cheapest take on the internet and it's usually wrong somewhere. It's wrong here in three places.&lt;/p&gt;

&lt;p&gt;The unit of governance genuinely changes. An API gateway thinks in requests and routes. An agent control plane has to think in &lt;em&gt;tool calls with arguments&lt;/em&gt;. That's a real difference. "Allow &lt;code&gt;read_file&lt;/code&gt;" is not a policy. "Allow &lt;code&gt;read_file&lt;/code&gt; only under &lt;code&gt;/tmp/run-{id}/&lt;/code&gt; and only for the first twenty calls" is a policy. Route-based rules can't express that, and bolting argument inspection onto a gateway plugin gets ugly fast — I've tried, and the plugin ended up parsing JSON in a place where JSON parsing doesn't belong.&lt;/p&gt;

&lt;p&gt;Cost has a different shape too. Per-request pricing is flat and predictable. Per-token pricing is a function of the input you didn't control, the output the model chose, and the number of retries your loop decided to do. I watched an agent retry a failed tool call fourteen times last month. Fourteen. The gateway saw fourteen requests and was perfectly happy about all of them.&lt;/p&gt;

&lt;p&gt;And approval has latency semantics that HTTP middleware doesn't. A gateway that blocks for four minutes is a broken gateway. An agent that blocks for four minutes is a Tuesday.&lt;/p&gt;

&lt;p&gt;So the honest read isn't "preloop is pointless." It's "preloop is a schema, not a category."&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd actually do with it
&lt;/h2&gt;

&lt;p&gt;I'm not deploying a second control plane next to the one I already run. Two policy engines is one too many, and the failure mode — where the gateway says yes and the agent layer says no, or worse, the reverse — is exactly the kind of split-brain that eats a weekend and produces a postmortem nobody enjoys writing.&lt;/p&gt;

&lt;p&gt;What I would do, and what I'm doing, is steal the schema. Read how preloop models a tool call, a policy decision, and an approval record. Then implement those three tables in the Postgres I already have, put the policy check in the one place my agent loop already funnels every tool call through, and let the gateway keep doing what gateways are good at: rate limits, auth, and metrics.&lt;/p&gt;

&lt;p&gt;That gets me most of the governance with none of the new operational surface. The remainder is the part I'd have to write anyway, because it's specific to my tools and my risk tolerance, and no upstream project can guess either one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I'm less sure about
&lt;/h2&gt;

&lt;p&gt;Maybe I'm wrong about the operational cost. If you're a team with no gateway, no policy engine, and no structured logging — if your agents are the first real service you've ever run — then preloop isn't a re-skin of anything. It's the first version of all four things, with sane defaults, in one afternoon. That's a legitimate reason to adopt it, and it's probably the audience it's actually built for.&lt;/p&gt;

&lt;p&gt;But that's a different claim than "agents need a new category of software." That claim is doing marketing work, not engineering work. The primitives are old. The schema is new. Buy the schema, skip the category.&lt;/p&gt;

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