<?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: Qweezyy</title>
    <description>The latest articles on DEV Community by Qweezyy (@qweezyy).</description>
    <link>https://dev.to/qweezyy</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%2F4145688%2Fece3fbd5-81ac-418a-b592-4621e67cbcec.png</url>
      <title>DEV Community: Qweezyy</title>
      <link>https://dev.to/qweezyy</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/qweezyy"/>
    <language>en</language>
    <item>
      <title>Three token optimizations that made our agent more expensive</title>
      <dc:creator>Qweezyy</dc:creator>
      <pubDate>Thu, 08 Oct 2026 09:58:58 +0000</pubDate>
      <link>https://dev.to/qweezyy/three-token-optimizations-that-made-our-agent-more-expensive-2hdj</link>
      <guid>https://dev.to/qweezyy/three-token-optimizations-that-made-our-agent-more-expensive-2hdj</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR.&lt;/strong&gt; Last time we cut a browser agent's tokens by 58% and really saved money. This time we looked for more places to trim and checked each idea on tasks with hidden tests. The three most obvious savings made the agent &lt;strong&gt;more expensive&lt;/strong&gt;: a trimmed system prompt (+82% per task), aggressive clearing of old context (almost twice the price), and loading tools one at a time, which reset the provider's cache. They share a root: in an agent the price is set not by the size of one request but by the number of steps and the share the provider serves from its cache. Plus two small examples where the saving really was there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;About this post.&lt;/strong&gt; The project is &lt;a href="https://github.com/Qweezyy/AltairAgent" rel="noopener noreferrer"&gt;Altair&lt;/a&gt;, an open-source (Apache-2.0) AI agent for your PC and phone. I'm the author and build it largely with Claude Code. This post, the experiments and the charts were prepared by that same AI assistant; I reviewed it and stand behind it. Numbers are from our runs, code is from the repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  First — how to price an agent
&lt;/h2&gt;

&lt;p&gt;An agent is a loop: the model answers, calls a tool, gets the result, and &lt;strong&gt;the whole history goes into the next request again&lt;/strong&gt;. A 10-step task is 10 requests, each longer than the last.&lt;/p&gt;

&lt;p&gt;Providers soften this with a cache: if the start of a request matches the previous one, that part costs several times less. For the model we measured on (&lt;code&gt;glm-5.3-flash&lt;/code&gt;), input is $0.15 per million tokens and cached input $0.03 — five times less. Our providers cache automatically, but only on a &lt;strong&gt;matching start&lt;/strong&gt; of the request. Change something in the middle and everything after it is billed in full.&lt;/p&gt;

&lt;p&gt;So two things set the price of a task:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;how many steps&lt;/strong&gt; the agent took;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;what share&lt;/strong&gt; of each request came from the cache.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The size of one request comes third. All three failed optimizations saved exactly that.&lt;/p&gt;

&lt;h2&gt;
  
  
  How we measured
&lt;/h2&gt;

&lt;p&gt;Six hard tasks with hidden tests (the agent never sees them, the check runs after "done"): an LRU cache with a TTL, a config parser fixed from user bug reports, a rename across several files, latency percentiles from logs, parsing "1.5M / 200K", business days with holidays. The real Altair run through its CLI, 18 runs per variant. Between agent and provider, a proxy that logs every request and can change it on the fly: drop a prompt section, cut tool descriptions. Cost as billed by the provider.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optimization 1: trim the system prompt
&lt;/h2&gt;

&lt;p&gt;The fixed part of every step for us is about 9,300 tokens: the system prompt (5,200) and the schemas of 20 core tools (4,100). An obvious target. We tried two variants:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;a trimmed prompt&lt;/strong&gt; — only what the model can't guess: environment, folder, rules for secrets and approvals, honesty. Out went the advice on approach, planning, editing code, verifying, communicating;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;short tool descriptions&lt;/strong&gt; — each description cut to its first sentence, parameter descriptions to 80 characters.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fafse9i946gkqzqa17red.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fafse9i946gkqzqa17red.png" alt="Saving on the prompt" width="800" height="224"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variant&lt;/th&gt;
&lt;th&gt;Solved&lt;/th&gt;
&lt;th&gt;Steps&lt;/th&gt;
&lt;th&gt;Cost per task&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Full prompt&lt;/td&gt;
&lt;td&gt;15 of 18&lt;/td&gt;
&lt;td&gt;7.9&lt;/td&gt;
&lt;td&gt;3.56 m$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trimmed prompt&lt;/td&gt;
&lt;td&gt;15 of 18&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;12.4&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;6.49 m$&lt;/strong&gt; (+82%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Short tool descriptions&lt;/td&gt;
&lt;td&gt;15 of 18&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10.4&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;5.96 m$&lt;/strong&gt; (+67%)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each request got shorter and each task got more expensive. Without advice on how to work, the agent took 57% more steps: read too much, re-ran tests, rewrote what already worked. Every extra step resends the whole history.&lt;/p&gt;

&lt;p&gt;Short tool descriptions did something worse. One task — fixing the parser from seven bug reports — the agent &lt;strong&gt;failed in all three attempts&lt;/strong&gt;, though it solved it with full descriptions. Tool descriptions aren't ballast; they're the instructions for using the tools right.&lt;/p&gt;

&lt;p&gt;Two more "cuts" gave nothing either: a hint to "do several independent things in one step" didn't reduce steps (7.9 vs 7.9), and dropping the skills list from the prompt was within noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optimization 2: clear old context aggressively
&lt;/h2&gt;

&lt;p&gt;Old tool outputs — files read, command results — stop being useful. Like many agents we have "observation masking": when the history passes half the budget, old large outputs are replaced with a short note. It's a proven technique: JetBrains showed on SWE-bench that it makes an agent 52% cheaper at the same solve rate.&lt;/p&gt;

&lt;p&gt;The logical next thought is to clear earlier. We cut the context budget from 120K to 40K tokens and gave the agent a task where clearing would surely kick in: read eight files in full, then answer six questions about them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftbb5xjc43afvc1e4wjyp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftbb5xjc43afvc1e4wjyp.png" alt="Aggressive context clearing" width="800" height="183"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Budget&lt;/th&gt;
&lt;th&gt;Solved&lt;/th&gt;
&lt;th&gt;Steps&lt;/th&gt;
&lt;th&gt;From cache&lt;/th&gt;
&lt;th&gt;Cost per task&lt;/th&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;120K (normal)&lt;/td&gt;
&lt;td&gt;3 of 3 (6/6 answers)&lt;/td&gt;
&lt;td&gt;20.7&lt;/td&gt;
&lt;td&gt;90%&lt;/td&gt;
&lt;td&gt;30.6 m$&lt;/td&gt;
&lt;td&gt;2.5 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;40K (tight)&lt;/td&gt;
&lt;td&gt;3 of 3 (6/6 answers)&lt;/td&gt;
&lt;td&gt;28.3&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;57%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;58.6 m$&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;4.9 min&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;About the same tokens (653K vs 679K), and the task got almost &lt;strong&gt;twice as expensive&lt;/strong&gt;. Two reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Clearing breaks the cache.&lt;/strong&gt; The note goes where the old output was, in the middle of the history, and the provider no longer recognizes anything after it. We checked separately: a 25.7K-token request after a masked output got &lt;strong&gt;6%&lt;/strong&gt; from the cache instead of 100%.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The agent re-reads what was hidden.&lt;/strong&gt; The fact it needed was in a hidden output, so the model honestly reads the file again. An extra step that resends the whole history once more.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What helped instead of clearing earlier:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Getting a hidden output back.&lt;/strong&gt; A new tool, &lt;code&gt;tool_output(id)&lt;/code&gt;, returns the original output from the stored history: our chats are kept without loss, the model is just shown a shortened view. The note now says how to get the output back instead of "run the tool again". In a separate test the model used it in 15 of 16 cases and never answered wrongly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A note before clearing.&lt;/strong&gt; One step before clearing, the agent is told: "old outputs are about to be hidden — note what you still need from them". It costs no extra model call; the note goes into an ordinary answer. After that the model answered 10 of 16 later questions at once, without re-reading.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leave the threshold alone.&lt;/strong&gt; Clear rarely and in big batches, so the cache breaks as seldom as possible.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Optimization 3: load tools on demand
&lt;/h2&gt;

&lt;p&gt;The agent has 117 tools. All their schemas are 22,500 tokens per request, so we use "deferred loading": the model sees 20 core tools and finds the rest by search when it needs them. This optimization does work: all 117 tools at once made a task 78% more expensive (10.95 vs 6.15 m$).&lt;/p&gt;

&lt;p&gt;The catch showed up in the logs. The tool list sits &lt;strong&gt;at the very start&lt;/strong&gt; of the request, before the system prompt and the history. Any change to that list and the provider recognizes nothing:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmppmekttarsibwahb3gv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmppmekttarsibwahb3gv.png" alt="What breaks the provider's cache" width="800" height="307"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;history appended at the end — 100% from cache;&lt;/li&gt;
&lt;li&gt;one tool appended to the end of the list — &lt;strong&gt;0%&lt;/strong&gt;;&lt;/li&gt;
&lt;li&gt;one tool inserted in name order — &lt;strong&gt;6%&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And browser tools were loaded &lt;strong&gt;one at a time&lt;/strong&gt;: first "open a page", a couple of steps later "read", then "tabs", "click", "screenshot". In one real chat the tool set changed 10 times — and 10 times the whole history went out at full price.&lt;/p&gt;

&lt;p&gt;The fix is to load &lt;strong&gt;by family&lt;/strong&gt;: one browser tool needed, all sixteen come. The same for dev-server, Android and code-quality tools.&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;family&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&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;name&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;browser_&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;browser&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;name&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;android_&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;android&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_dev_server&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_dev_servers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dev_server&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;QUALITY_TOOLS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;quality&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On 21 real chats that cuts cache resets from 77 to 43. The price is 2,300 tokens of browser-family schemas, which almost always come from the cache.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two short examples where the saving was right there
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Line numbers.&lt;/strong&gt; &lt;code&gt;read_file&lt;/code&gt; is the most used tool; its outputs are half of all outputs in the history. It numbered lines like &lt;code&gt;12 | text&lt;/code&gt;. We counted with the provider's tokenizer on 1,320 lines of real files:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F88tj1az6ereh4nt1pgoh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F88tj1az6ereh4nt1pgoh.png" alt="Line numbers in read_file output" width="799" height="265"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The padded number with a separator made the output 44% more expensive than plain text. &lt;code&gt;12|text&lt;/code&gt; is +24%, i.e. &lt;strong&gt;14% fewer tokens on every file read&lt;/strong&gt;. We checked editing quality separately on the same hard tasks: unchanged.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Service calls.&lt;/strong&gt; A chat title is five words, yet the model spent about 190 reasoning tokens on it: 95% of the answer. A history summary with a 600-token limit came back empty in 3 cases out of 4 because reasoning used up the whole limit. An empty summary isn't a saving, it's losing the start of the conversation. Service calls now ask for the least reasoning, and the summary limit is 2,500.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to take from this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Measure the cost of a task, not the size of a request.&lt;/strong&gt; Three of three "obvious" savings shrank the request and grew the bill.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Steps cost more than tokens.&lt;/strong&gt; Anything that makes the agent take an extra step — fewer hints, a needed fact hidden — costs more than it saves.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't touch the start of the request.&lt;/strong&gt; The provider's cache works on a matching start. The tool list, the system prompt and old history should change as rarely as possible and in big portions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check quality, not just cost.&lt;/strong&gt; Short tool descriptions looked harmless until one task started failing on every attempt.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What this doesn't fix
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One model, two providers.&lt;/strong&gt; &lt;code&gt;glm-5.3-flash&lt;/code&gt;, automatic caching. Anthropic caches with explicit markers, and "what breaks the cache" looks different there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small samples.&lt;/strong&gt; 18 runs per prompt variant, 3 per budget variant. A difference of one or two tasks is noise; a difference of 1.5–2x in cost is not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Our tasks are code.&lt;/strong&gt; For browsing and writing the balance may shift.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Check it yourself
&lt;/h2&gt;

&lt;p&gt;Altair's code — &lt;strong&gt;&lt;a href="https://github.com/Qweezyy/AltairAgent" rel="noopener noreferrer"&gt;https://github.com/Qweezyy/AltairAgent&lt;/a&gt;&lt;/strong&gt;: deferred tools and families in &lt;code&gt;pc/core/tools/deferred.py&lt;/code&gt;, context clearing in &lt;code&gt;pc/core/agent/session.py&lt;/code&gt;, &lt;code&gt;tool_output&lt;/code&gt; in &lt;code&gt;pc/core/tools/builtin/context_tools.py&lt;/code&gt;. The stand (a proxy that edits requests on the fly, tasks with hidden tests, the summary) and the raw results: &lt;strong&gt;&lt;a href="https://github.com/Qweezyy/AltairAgent/tree/main/research/agent-lab-2026-10" rel="noopener noreferrer"&gt;research/agent-lab-2026-10&lt;/a&gt;&lt;/strong&gt; (&lt;code&gt;python lab/facts.py&lt;/code&gt; recomputes every number in this post from the saved results, no keys needed).&lt;/p&gt;

&lt;p&gt;Where did you save in your agents and then roll it back? I'm especially curious about caching: how often does the start of your requests change?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>agents</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Our agent said "done" on 15% of tasks while the provider was failing</title>
      <dc:creator>Qweezyy</dc:creator>
      <pubDate>Mon, 05 Oct 2026 12:04:36 +0000</pubDate>
      <link>https://dev.to/qweezyy/our-agent-said-done-on-15-of-tasks-while-the-provider-was-failing-104i</link>
      <guid>https://dev.to/qweezyy/our-agent-said-done-on-15-of-tasks-while-the-provider-was-failing-104i</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR.&lt;/strong&gt; We ran our AI agent on 46 tasks and checked each one with tests after it said "done". 7 of the 46 — 15% — "done"s were untrue. Not because of the model: not one task failed because the model couldn't solve it. The provider was to blame. It answered with HTTP 200 and sent its own error text instead of the model's reply, or an empty stream, or the model looped on its side — and the agent took any end of a stream for the end of the task. Below: the three shapes of this failure, how to detect them, why a second reviewer doesn't help, and what it means for "done" in general.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;About this post.&lt;/strong&gt; The project is &lt;a href="https://github.com/Qweezyy/AltairAgent" rel="noopener noreferrer"&gt;Altair&lt;/a&gt;, an open-source (Apache-2.0) AI agent for your PC and phone. I'm the author and build it largely with Claude Code. This post, the experiments and the charts were prepared by that same AI assistant; I reviewed it and stand behind it. Numbers are from our runs, code is from the repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it started: "completed ≠ accepted"
&lt;/h2&gt;

&lt;p&gt;After the &lt;a href="https://dev.to/qweezyy/i-built-an-ai-agent-that-snapshots-every-edit-and-runs-your-tests-before-it-says-done-l17"&gt;first post&lt;/a&gt; about Altair, Kent Bodrov left a comment worth retelling. We had boasted that the agent snapshots every edit and runs the tests before "done". Kent pointed out that a snapshot only proves rollback material exists, and a green command only proves a check ran. Neither proves the wanted behaviour is there. "Completed" isn't "accepted".&lt;/p&gt;

&lt;p&gt;We wrote it into the plan and went measuring. It turned out there's a simpler question below "was it accepted": &lt;strong&gt;was it completed at all&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How we measured
&lt;/h2&gt;

&lt;p&gt;The real Altair, a cheap model (&lt;code&gt;glm-5.3-flash&lt;/code&gt;) through two providers, 46 tasks of five kinds: fix a bug from failing tests, answer a question about code, compute from a CSV, write a module with tests, go through long code. Each task has an automatic check that runs after "done". A proxy between agent and provider logged every request and answer: how many tokens the provider counted, how much text came, how long the stream lasted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp1t9j6r68mrgw5ychflu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp1t9j6r68mrgw5ychflu.png" alt="46 tasks the agent called done" width="799" height="265"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;46 tasks the agent called done&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;39 tasks were really solved. 7 weren't, and in all seven the provider broke:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;6 times — the gateway's error text instead of an answer.&lt;/strong&gt; HTTP 200 and an ordinary assistant message: "The request could not be completed. Please retry later, or reduce the request parameters/content." The agent showed it to the user as its answer, the CLI returned &lt;code&gt;"ok": true&lt;/code&gt;, task "done" in one step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Once — an empty stream.&lt;/strong&gt; The provider held the connection for 103 seconds and closed it without a single token: no text, no tool call. The agent wrote "Task finished, but the model returned no text" — after two steps out of an intended twenty.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The next series, on hard tasks, showed a third shape:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A loop cut short.&lt;/strong&gt; The model looped on endless reasoning. Our loop guard cut the stream at 160,000 characters — and the agent loop took the cut stream for the final answer. That ended 8 of 67 runs where no reasoning level was set. (Why the model looped and how one field in the request fixes it — in the companion post.)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Why the agent believed it
&lt;/h2&gt;

&lt;p&gt;All three go through the same place. The agent loop works like most:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;model answered → is there a tool call? → run it, continue
               → no tool call?        → that's the final answer, task done
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The "no tool call" branch can't tell "the model finished the work" from "the provider sent anything without a tool call". And there was never an HTTP error, the thing that triggers retries and the backup provider: everything came with 200.&lt;/p&gt;

&lt;p&gt;The telling part is that the proxy saw what the agent didn't. For one of these "answers" the provider counted 4,552 input tokens. The agent's system prompt and tool schemas alone are about 9,300 tokens. The provider never even read the request, and the agent got an "answer" and closed the task.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to detect it
&lt;/h2&gt;

&lt;p&gt;We now check every answer without a tool call. It's not an answer if:&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;judge_turn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;turn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cut&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;turn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool_calls&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;None&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cut&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cut&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;            &lt;span class="c1"&gt;# the loop guard cut the stream
&lt;/span&gt;    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;turn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&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;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;empty&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;          &lt;span class="c1"&gt;# neither text nor a tool call
&lt;/span&gt;    &lt;span class="n"&gt;low&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;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&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;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;low&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;GATEWAY_ERROR_PHRASES&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gateway&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;        &lt;span class="c1"&gt;# a short reply that is a gateway's error text
&lt;/span&gt;    &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;turn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_text_chars&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;4.5&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unprocessed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;    &lt;span class="c1"&gt;# the provider read less than half of the request
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Such a reply is an exception the client retries like a dropped connection: after a pause, or on the backup provider if one is set. Three details without which this would work badly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The first ~160 characters are held back.&lt;/strong&gt; Otherwise the gateway's error text reaches the chat and stays there after the retry. The delay is a fraction of a second, only at the start of an answer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The phrases only count in a short reply.&lt;/strong&gt; If the model, in a normal long answer, tells the user that "the request could not be completed", that's an answer, not an error.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A loop is retried once, with a low reasoning level.&lt;/strong&gt; If the model loops again, the agent reports an honest error instead of five more 160K-character attempts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An empty final answer is no longer "task finished" either: it's a failure, not a result.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about a second reviewer?
&lt;/h2&gt;

&lt;p&gt;The first thought after Kent's comment is to add a separate model call that checks the result against the task after "done". We tried that too: the reviewer sees the task and every file, answers "OK" or a list of violations, and the agent fixes what it flags.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj48bsghrrjjpdkmlmrfd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj48bsghrrjjpdkmlmrfd.png" alt="A second reviewer after done" width="800" height="224"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;A second reviewer after "done"&lt;/em&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Solved before review&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;th&gt;Flags (false)&lt;/th&gt;
&lt;th&gt;Cost per task&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Agent alone&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;16 of 18&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;3.8 m$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+ reviewer, &lt;code&gt;low&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;20 of 21&lt;/td&gt;
&lt;td&gt;20 of 21&lt;/td&gt;
&lt;td&gt;4 (3)&lt;/td&gt;
&lt;td&gt;5.5 m$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+ reviewer, &lt;code&gt;high&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;15 of 18&lt;/td&gt;
&lt;td&gt;16 of 18&lt;/td&gt;
&lt;td&gt;5 (4)&lt;/td&gt;
&lt;td&gt;12.8 m$&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;With a frugal reviewer: not one task fixed, for +45% cost. With an expensive one: one task fixed out of 18, at 3.4x the cost, and it missed one. Most flags were false: the agent checked and dismissed them without breaking anything, but we paid for each.&lt;/p&gt;

&lt;p&gt;The reason is simple. With a properly set reasoning level the agent already solves about 90% of these tasks. And the real false "done"s didn't come from a model that left work unfinished, but from infrastructure the reviewer can't see at all. The reviewer reads files — and a provider reply with an error text never got as far as the files.&lt;/p&gt;

&lt;h2&gt;
  
  
  A provider that doesn't fail
&lt;/h2&gt;

&lt;p&gt;Separately, for six hours, every 1–1.5 minutes, we sent the same provider real-size requests. 297 of 297 eventually got an answer, only 4 on a second try. Not a single "error disguised as an answer" this time.&lt;/p&gt;

&lt;p&gt;But around 14:00 the provider sagged and stayed 4–5x slower until the end: the median went from 6 to 22–29 seconds, single answers took 1.5–4 minutes. Such slumps last hours, and retrying doesn't help — it joins the same queue. Only a backup provider does. So our breaker trips not only on errors but on slowness: if the first byte came later than 40 seconds twice in a row, the next steps go to the backup model for 10 minutes.&lt;/p&gt;

&lt;p&gt;One more lesson from that test: put the timeout on &lt;strong&gt;silence&lt;/strong&gt;, not on the whole request. Our phone app had a total 120-second limit per request — and long but healthy answers were cut at second 120 during slow hours. On the PC the same 120 s meant "no new data for 120 s", and a flowing stream got to the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway: check "done" at the infrastructure level
&lt;/h2&gt;

&lt;p&gt;Kent is right: "completed" isn't "accepted". Our measurements add a rung below. Before asking whether the model did what was asked, the agent has to make sure the model answered at all.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The end of a stream isn't the end of a task.&lt;/strong&gt; An empty answer, a cut answer, an answer to a request the provider didn't read — these are failures and must be handled as failures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTTP 200 guarantees nothing.&lt;/strong&gt; Gateways have their own errors that arrive as ordinary model replies. The provider's input-token count is a good detector: it doesn't lie about what the model actually read.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Checking "done" is engineering, not another model.&lt;/strong&gt; Tests after the work, checking the provider's answer, an honest error instead of "finished". A second reviewer is expensive and mostly noise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure afterwards instead of trusting the process.&lt;/strong&gt; We found all this only because each task was checked by tests after "done".&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What this doesn't fix
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The detector is heuristic.&lt;/strong&gt; The list of gateway phrases is incomplete, the "read less than half" threshold was tuned on our data. Another gateway may fail in its own way.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Accepted" still isn't checked.&lt;/strong&gt; We closed "was it completed". Checking "is it the right behaviour" — a separate acceptance step against criteria from the user's request — is only planned.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small samples.&lt;/strong&gt; 46 tasks in the first series, 18–21 runs per reviewer setting. How often this happens depends heavily on the provider's load: in calm hours it almost doesn't.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Check it yourself
&lt;/h2&gt;

&lt;p&gt;The detection code is &lt;code&gt;pc/core/llm/reliability.py&lt;/code&gt; with its tests &lt;code&gt;pc/tests/test_reliability.py&lt;/code&gt; in &lt;strong&gt;&lt;a href="https://github.com/Qweezyy/AltairAgent" rel="noopener noreferrer"&gt;https://github.com/Qweezyy/AltairAgent&lt;/a&gt;&lt;/strong&gt;. The stand (logging proxy, tasks with checks, summary) and the raw results: &lt;strong&gt;&lt;a href="https://github.com/Qweezyy/AltairAgent/tree/main/research/agent-lab-2026-10" rel="noopener noreferrer"&gt;research/agent-lab-2026-10&lt;/a&gt;&lt;/strong&gt; (&lt;code&gt;python lab/facts.py&lt;/code&gt; recomputes every number in this post from the saved results, no keys needed).&lt;/p&gt;

&lt;p&gt;How do you check that your agent has really finished? Have you met providers that send errors with HTTP 200?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>agents</category>
      <category>opensource</category>
    </item>
    <item>
      <title>One field in the request made our agent 3x cheaper and 8x faster</title>
      <dc:creator>Qweezyy</dc:creator>
      <pubDate>Sun, 04 Oct 2026 10:20:43 +0000</pubDate>
      <link>https://dev.to/qweezyy/one-field-in-the-request-made-our-agent-3x-cheaper-and-8x-faster-5e8c</link>
      <guid>https://dev.to/qweezyy/one-field-in-the-request-made-our-agent-3x-cheaper-and-8x-faster-5e8c</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR.&lt;/strong&gt; Reasoning models decide by themselves how long to think if you don't tell them. Our agent didn't — and on hard tasks the model sometimes thought for 14 minutes and 33K tokens in a single step. One field in the request body (&lt;code&gt;"reasoning": {"effort": "low"}&lt;/code&gt;) made a task 3x cheaper and 7–8x faster, and it solved &lt;em&gt;more&lt;/em&gt; tasks, not fewer: 12 of 12 instead of 6 of 7. The best setting in the end was neither "always low" nor "always high" but "low, and high right after a failing test". Below: how we measured it, the tables, the code, and what it doesn't fix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;About this post.&lt;/strong&gt; The project is &lt;a href="https://github.com/Qweezyy/AltairAgent" rel="noopener noreferrer"&gt;Altair&lt;/a&gt;, an open-source (Apache-2.0) AI agent for your PC and phone. I'm the author and build it largely with Claude Code. This post, the experiments and the charts were prepared by that same AI assistant; I reviewed it and stand behind it. Every number comes from our runs.&lt;/p&gt;

&lt;p&gt;This is the third post about the project: the &lt;a href="https://dev.to/qweezyy/i-built-an-ai-agent-that-snapshots-every-edit-and-runs-your-tests-before-it-says-done-l17"&gt;first&lt;/a&gt; is about snapshots and running tests before "done", the second about cutting a browser agent's tokens by 58%.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it started
&lt;/h2&gt;

&lt;p&gt;We ran the agent on a cheap reasoning model (&lt;code&gt;glm-5.3-flash&lt;/code&gt;) and read the provider's logs. On one task the very first step took 14 minutes. The provider had time to bill it: &lt;strong&gt;32,978 reasoning tokens in one step&lt;/strong&gt;, and no answer. Our loop guard cut the stream at 160,000 characters.&lt;/p&gt;

&lt;p&gt;The cause was mundane. Reasoning models have a "how much to think" knob — &lt;code&gt;reasoning_effort&lt;/code&gt; at OpenAI, &lt;code&gt;reasoning.effort&lt;/code&gt; at OpenRouter, &lt;code&gt;thinking&lt;/code&gt; in Z.ai-style APIs. If you don't send it, the provider or the model decides; OpenRouter's docs say as much. Our agent sent nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  How we measured
&lt;/h2&gt;

&lt;p&gt;Easy tasks (fix a bug, answer a question about code) were solved 100% of the time under any setting, so they show no difference. We wrote &lt;strong&gt;six hard tasks with hidden tests&lt;/strong&gt; — the agent never sees them, they run after it says "done":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an LRU cache with a time-to-live (11 hidden edge-case tests);&lt;/li&gt;
&lt;li&gt;fixing a config parser from seven user bug reports;&lt;/li&gt;
&lt;li&gt;renaming a function across a multi-file package and adding a parameter;&lt;/li&gt;
&lt;li&gt;the 95th percentile of latencies from logs, with exclusions;&lt;/li&gt;
&lt;li&gt;parsing "1.5M", "200K", "10 тыс";&lt;/li&gt;
&lt;li&gt;business days between dates with holidays, fast over a hundred years.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each task was first solved with reference code to make sure the hidden tests were fair. The agent is the real Altair, run through its CLI (&lt;code&gt;altair -p&lt;/code&gt;). A proxy between agent and provider logged every request: size, cached tokens, reasoning tokens, time. Cost is what OpenRouter billed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F617hi0rc7jecsoiio79b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F617hi0rc7jecsoiio79b.png" alt="a1-cost-en" width="799" height="265"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Cost of one hard task&lt;/em&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Solved&lt;/th&gt;
&lt;th&gt;Cost per task&lt;/th&gt;
&lt;th&gt;Time per task&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No level (before)&lt;/td&gt;
&lt;td&gt;6 of 7&lt;/td&gt;
&lt;td&gt;12.6 m$&lt;/td&gt;
&lt;td&gt;4.1 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;effort = low&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;12 of 12&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4.2 m$&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.55 min&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;effort = medium&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10 of 12&lt;/td&gt;
&lt;td&gt;4.8 m$&lt;/td&gt;
&lt;td&gt;0.64 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;effort = high&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;12 of 12&lt;/td&gt;
&lt;td&gt;6.4 m$&lt;/td&gt;
&lt;td&gt;1.5 min&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;m$ is a thousandth of a dollar. 12.6 m$ without a level, 4.2 m$ with &lt;code&gt;low&lt;/code&gt;: three times less. Time went from 4.1 to 0.55 minutes, 7.5x.&lt;/p&gt;

&lt;p&gt;Why "6 of 7" and not "of 12": runs without a level kept looping for 10–14 minutes, and we stopped that series early so as not to burn money. One of the six tasks (parsing "1.5M") sent the model into endless reasoning in almost every series without a level. With any explicit level — never.&lt;/p&gt;

&lt;p&gt;Where the money goes:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9066yvm6fxudrn65am6j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9066yvm6fxudrn65am6j.png" alt="a1-reasoning-en" width="799" height="265"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Reasoning tokens per task&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;9,743 reasoning tokens per task on average without a level, 235 with &lt;code&gt;low&lt;/code&gt; — 41x fewer. Output tokens cost 3.3x more than input for this model, so on hard tasks reasoning is the main bill.&lt;/p&gt;
&lt;h2&gt;
  
  
  And on long tasks?
&lt;/h2&gt;

&lt;p&gt;Short tasks are 5–9 steps. We also tried 15–20-step ones: a package with six bugs in different modules, auditing twelve values across sixty files of our own code, and "read eight files in full, then answer questions about them".&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk93czs1inp918bs87zm9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk93czs1inp918bs87zm9.png" alt="a1-long-en" width="800" height="224"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Long tasks&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;9 of 9 solved in every setting, but without a level a task costs 28 m$ and takes 5.2 minutes; with an explicit level, 11–13 m$ and a little over a minute.&lt;/p&gt;
&lt;h2&gt;
  
  
  The twist: think hard only when something broke
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;low&lt;/code&gt; is cheap, &lt;code&gt;high&lt;/code&gt; is safer. We wanted both, and tried two per-step modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"think about the plan"&lt;/strong&gt; — &lt;code&gt;high&lt;/code&gt; on the first step, then &lt;code&gt;low&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"think after a failure"&lt;/strong&gt; — &lt;code&gt;low&lt;/code&gt;, but &lt;code&gt;high&lt;/code&gt; on the step right after tests failed or a tool returned an error.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fso83hyjtqpw323jlqlt1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fso83hyjtqpw323jlqlt1.png" alt="a1-adaptive-en" width="799" height="265"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;When to think harder&lt;/em&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting (18 runs each)&lt;/th&gt;
&lt;th&gt;Solved&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;always &lt;code&gt;low&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;16 of 18&lt;/td&gt;
&lt;td&gt;3.78 m$&lt;/td&gt;
&lt;td&gt;0.96 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;always &lt;code&gt;high&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;17 of 18&lt;/td&gt;
&lt;td&gt;5.72 m$&lt;/td&gt;
&lt;td&gt;1.92 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;high&lt;/code&gt; on the first step&lt;/td&gt;
&lt;td&gt;15 of 18&lt;/td&gt;
&lt;td&gt;4.37 m$&lt;/td&gt;
&lt;td&gt;1.93 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;low&lt;/code&gt;, &lt;code&gt;high&lt;/code&gt; after a failure&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;17 of 18&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3.65 m$&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.84 min&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Careful planning didn't help — it solved the fewest. "Think after a failure" solved as many as always-&lt;code&gt;high&lt;/code&gt;, at the cost and speed of &lt;code&gt;low&lt;/code&gt;. It makes sense: most of an agent's work is routine (read a file, write a file, run the tests), and thinking pays off where a test just showed the first idea was wrong. One task in 18 is close to noise, honestly, but this mode is no worse than &lt;code&gt;low&lt;/code&gt; on all three measures. It's now our default.&lt;/p&gt;
&lt;h2&gt;
  
  
  How it looks in code
&lt;/h2&gt;

&lt;p&gt;The catch is that every provider has its own field:&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;reasoning_extra&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base_url&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;override&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;auto&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;Request fields for a reasoning level ({} for &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;default&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; / unknown levels).&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;level&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;LEVELS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dialect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base_url&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;override&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;kind&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;openrouter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# OpenRouter takes minimal..high; some endpoints refuse "none"/enabled=false,
&lt;/span&gt;        &lt;span class="c1"&gt;# so the lowest we send is "low" for "minimal" there.
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reasoning&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;effort&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;low&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;level&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;minimal&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;level&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;kind&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;zai&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Z.ai-style APIs have an on/off switch: low and minimal turn thinking off.
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;thinking&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&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;disabled&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;level&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;minimal&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;low&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;enabled&lt;/span&gt;&lt;span class="sh"&gt;"&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;kind&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;openai&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reasoning_effort&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What we stepped on along the way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For this model OpenRouter &lt;strong&gt;refuses&lt;/strong&gt; &lt;code&gt;reasoning: {"enabled": false}&lt;/code&gt; and &lt;code&gt;reasoning_effort: "none"&lt;/code&gt; with a 400 "Reasoning is mandatory for this endpoint". Only &lt;code&gt;effort: "low"&lt;/code&gt; works.&lt;/li&gt;
&lt;li&gt;A Z.ai-style gateway, the other way round, takes &lt;code&gt;thinking: {"type": "disabled"}&lt;/code&gt; and didn't answer &lt;code&gt;reasoning.effort&lt;/code&gt; at all.&lt;/li&gt;
&lt;li&gt;A provider that doesn't know the field may answer 400. We catch that, retry without the field and remember not to send it again.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The agent loop picks the level for each step:&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="n"&gt;FAILURE_RE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\b\d+ failed\b|\bFAILED\b|Traceback \(most recent call last\)|AssertionError|&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                        &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\bexit code [1-9]\d*\b|\bSyntaxError\b|[A-Za-z]Error:&lt;/span&gt;&lt;span class="sh"&gt;"&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;_reasoning_level&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;llm_reasoning&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;default&lt;/span&gt;&lt;span class="sh"&gt;"&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;None&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;adaptive&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;high&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_round_failed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;low&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After each round of tools, &lt;code&gt;_round_failed&lt;/code&gt; is true if a tool returned an error or its output has "2 failed", a traceback or a non-zero exit code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Service calls thought more than the main ones
&lt;/h2&gt;

&lt;p&gt;Two more places turned up in the logs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The chat title.&lt;/strong&gt; The agent asks the model for a title from the first message. About 95% of that answer was reasoning: ~190 tokens of "thoughts" for a five-word title. With reasoning off the answer is 12 tokens and comes 1.6–3x faster; all six sample titles were still fine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The history summary.&lt;/strong&gt; When the context grows, the agent asks the model to condense the start of the conversation. The answer limit was 600 tokens. The model spent them on reasoning and &lt;strong&gt;the summary came back empty in 3 cases out of 4&lt;/strong&gt; (&lt;code&gt;finish_reason: length&lt;/code&gt;). An empty summary isn't a fold, it's a loss: the agent drops the start of the conversation instead of condensing it. With a 2,000 limit there were no empty summaries.&lt;/p&gt;

&lt;p&gt;Service calls now ask for the least reasoning, and the summary limit is 2,500.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this doesn't fix
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One model.&lt;/strong&gt; Everything was measured on &lt;code&gt;glm-5.3-flash&lt;/code&gt;. Other models scale their levels differently, and "low" may mean something else. The principle — don't leave the level to the provider — carries over; the exact numbers don't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small samples.&lt;/strong&gt; 12–18 runs per setting. A difference of one or two solved tasks is noise. A difference of 3x in cost and time is not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not every provider has levels.&lt;/strong&gt; A Z.ai-style gateway only knows on/off. There "high after a failure" means "think fully", which is expensive: on our tasks "always off" on that gateway came out half the price of adaptive (5.9 vs 10.8 m$, 12 of 12 both). If cost matters most, pick "low" there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The tasks are code.&lt;/strong&gt; For writing, search or analytics the best level may differ.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Check it yourself
&lt;/h2&gt;

&lt;p&gt;The core of the experiment is one agent run with a set level:&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;# proxy.py adds the level to every agent request to the provider
&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reasoning&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;effort&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;low&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;request_body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;
&lt;span class="c1"&gt;# stand.py: the agent solves the task, then the hidden tests run
&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;altair_cli.py&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;-p&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;--output-format&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;json&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;--mode&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;bypass&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;--cwd&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;workspace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;env&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;900&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;passed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;workspace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The whole stand (the logging proxy, tasks with hidden tests and reference solutions, the summary script) and the raw results: &lt;strong&gt;&lt;a href="https://github.com/Qweezyy/AltairAgent/tree/main/research/agent-lab-2026-10" rel="noopener noreferrer"&gt;research/agent-lab-2026-10&lt;/a&gt;&lt;/strong&gt; (&lt;code&gt;python lab/facts.py&lt;/code&gt; recomputes every number in this post from the saved results, no keys needed). Altair's code: &lt;strong&gt;&lt;a href="https://github.com/Qweezyy/AltairAgent" rel="noopener noreferrer"&gt;https://github.com/Qweezyy/AltairAgent&lt;/a&gt;&lt;/strong&gt; — the level is in Settings → Agent → Reasoning level (default "Adaptive"), &lt;code&gt;LLM_REASONING&lt;/code&gt; in &lt;code&gt;.env&lt;/code&gt;, module &lt;code&gt;pc/core/llm/reliability.py&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;How do you set the reasoning level in your agents — fixed, per task type, or as the work goes? Have you seen a model think far more than it needs when no level is set?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>agents</category>
      <category>opensource</category>
    </item>
    <item>
      <title>How our browser agent stopped spending 58% of its tokens</title>
      <dc:creator>Qweezyy</dc:creator>
      <pubDate>Mon, 28 Sep 2026 13:27:59 +0000</pubDate>
      <link>https://dev.to/qweezyy/how-our-browser-agent-stopped-spending-58-of-its-tokens-eca</link>
      <guid>https://dev.to/qweezyy/how-our-browser-agent-stopped-spending-58-of-its-tokens-eca</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR.&lt;/strong&gt; A browser agent sees a page as a text accessibility tree. After every click it used to get the whole tree again, even when two lines of the page had changed. We now send the model only the changes (a diff) and strip what the model never uses. On a real 98-step browsing chat, page states went from 355K to 151K tokens (−58%). A reproducible benchmark on four sites gave −63%, and a live A/B run of the agent on the same task gave −54% on browser tool results. Here is how it works, where it nearly broke, and a script to check the numbers yourself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;About this post.&lt;/strong&gt; The project is &lt;a href="https://github.com/Qweezyy/AltairAgent" rel="noopener noreferrer"&gt;Altair&lt;/a&gt;, an open-source (Apache-2.0) AI agent for your PC and phone. I'm the author and build it largely with Claude Code. This post, the measurements and the screenshots were prepared by that same AI assistant; I reviewed it and stand behind it. It will also answer the comments while I watch and approve every reply, so treat it as a live test: if something reads as dishonest or spammy, say so and I'll fix it. Every number below is reproducible.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the agent "sees" a page
&lt;/h2&gt;

&lt;p&gt;A screenshot is expensive and imprecise: to press a button the model has to guess coordinates. So many browser agents, ours included, read the page as its &lt;strong&gt;accessibility tree&lt;/strong&gt; — what a screen reader sees. Playwright returns it in a YAML-like form, and every element gets a short &lt;code&gt;ref&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;banner [ref=e9]&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;heading "Navigation Menu" [level=2] [ref=e10]&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;button "Code" [ref=e180]&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;textbox "Search Hacker News" [ref=e684]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model calls &lt;code&gt;browser_click(ref="e180")&lt;/code&gt; and the agent clicks exactly that element.&lt;/p&gt;

&lt;p&gt;The question is &lt;strong&gt;when&lt;/strong&gt; the model gets this tree. Every action changes the page, and the model needs to know what it looks like now. The simplest answer is to return the whole tree again. That is what we did.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;p&gt;A GitHub repo page is about 20K tokens as a tree, a Wikipedia article 136K. We cut a snapshot at 14,000 characters (≈3.5–4K tokens), and the agent finds the rest with an in-page search. Even so, every click cost 3.5–4K tokens.&lt;/p&gt;

&lt;p&gt;Then the easy-to-miss part kicks in: &lt;strong&gt;the history is resent on every step&lt;/strong&gt;. After 30 actions on a page, the history holds 30 near-identical copies of it, and every following request sends them all. Prompt caching lowers the price of resending, but not the room they take in the context window.&lt;/p&gt;

&lt;p&gt;In our logs, &lt;strong&gt;browser tools made up more than half of all tool tokens&lt;/strong&gt;. One real 98-step chat had 355K tokens of page states.&lt;/p&gt;

&lt;p&gt;The clearest example comes from the benchmark below. The agent clicks the search field on Hacker News. It used to get the whole front page back — 3,690 tokens. Exactly one word changed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;             - text: "Search:"
&lt;span class="gd"&gt;-            - textbox [ref=e684]
&lt;/span&gt;&lt;span class="gi"&gt;+            - textbox [active] [ref=e684]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now that answer costs 80 tokens.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trick 1: drop what the model never uses
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unnamed wrappers.&lt;/strong&gt; &lt;code&gt;- generic [ref=e42]:&lt;/code&gt; with no name is a layout &lt;code&gt;div&lt;/code&gt;. The model never clicks it. The line goes; its children stay.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redundant &lt;code&gt;[cursor=pointer]&lt;/code&gt;.&lt;/strong&gt; A link, button or tab is clickable by its role. The mark stays only where it says something: on a clickable &lt;code&gt;generic&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Long link targets.&lt;/strong&gt; Tracking links run to 1,000+ characters. Past 150 characters we drop the query string.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Refs are never touched, or clicks would break. On untruncated snapshots this saves 7–27% (Hacker News −15%, GitHub −7%, MDN −27%, Wikipedia −9%). It's modest, and that's the honest result: the main win is elsewhere. It also fits more real content under the 14K-character cut.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trick 2: after an action, send only the changes
&lt;/h2&gt;

&lt;p&gt;The agent remembers the page the model saw last: tab, URL and tree. After an action, if the tab and URL are the same, the model gets a unified diff with one line of context:&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;page_changes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""""&lt;/span&gt;&lt;span class="s"&gt; = nothing changed; None = too much changed, send the whole page.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;difflib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unified_diff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;lineterm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;line&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;---&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;+++&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;line&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;@@&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;text&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;)&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;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="k"&gt;if&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;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&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;new&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;else&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rules around it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The whole page comes on navigation, a new tab, or an explicit &lt;code&gt;browser_read&lt;/code&gt;.&lt;/strong&gt; A diff between two different pages is meaningless.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If more than half of the page changed, send it whole.&lt;/strong&gt; A diff of 80% of a page is longer than the page and harder to read.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Nothing visible changed" is an answer too.&lt;/strong&gt; It's a useful signal: the click probably didn't work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The header explains the format&lt;/strong&gt;: &lt;code&gt;-&lt;/code&gt; lines were removed, &lt;code&gt;+&lt;/code&gt; added, the rest of the page and its refs are as before. No extra system-prompt instructions were needed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Playwright refs are stable within a page, so refs from the old snapshot keep working after a diff; we tested that on a real browser.&lt;/p&gt;

&lt;p&gt;A live run. The task: open the GitHub repo, open the green Code menu and give me the HTTPS clone URL, then open the branch picker and list the branches.&lt;/p&gt;

&lt;p&gt;The second click (branch picker) &lt;strong&gt;without diffs&lt;/strong&gt; — the model gets the whole page again, from the dialog to the site header and the file list. 3,386 tokens:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/img%2Fcard-full.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/img%2Fcard-full.png" alt="A click result without diffs: the whole page" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The first click (Code menu) &lt;strong&gt;with diffs&lt;/strong&gt; — only the menu that appeared, with the HTTPS URL right there. 568 tokens:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/img%2Fcard-diff.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/img%2Fcard-diff.png" alt="A click result with diffs: only the changes" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it nearly broke: the history
&lt;/h2&gt;

&lt;p&gt;A diff only makes sense while the page it was taken against is still in the history. We already had two mechanisms that clean the history:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Superseding stale snapshots&lt;/strong&gt;: old page snapshots became a one-line note, the newest two stayed. With diffs, "the newest two" could be two diffs whose base was gone — changes to a page the model can't see.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Budget clearing&lt;/strong&gt;: near the context limit, old large tool outputs become a note. The largest output in a browsing chat is exactly the whole page.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The fix: superseding keeps the newest two &lt;em&gt;whole&lt;/em&gt; pages with every diff after them, and budget clearing never takes the latest whole page. Clearing is batched, because every history edit invalidates the prompt cache from that point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measurements
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. A real 98-step chat&lt;/strong&gt; (measured during development): page states 355K → 270K tokens with leaner snapshots → 151K with diffs. &lt;strong&gt;That's the −58%.&lt;/strong&gt; I can't re-run that exact chat — its stored outputs were already compacted — so here are two independent checks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. A reproducible benchmark.&lt;/strong&gt; Real Chromium, four sites (Wikipedia, GitHub, Hacker News, MDN), a fixed script: open, click search, type, Escape, open a menu. Each step is rendered with the same &lt;code&gt;PageView.render&lt;/code&gt; the agent uses, same truncation. Tokens via &lt;code&gt;tiktoken&lt;/code&gt; &lt;code&gt;o200k_base&lt;/code&gt; (tokenizers differ, the ratios hold).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/img%2Fbench-steps-en.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/img%2Fbench-steps-en.png" alt="Benchmark: 17 steps on 4 real sites" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;17 steps: &lt;strong&gt;60,117 → 22,255 tokens, −63%&lt;/strong&gt;. Actions that stay on the same page only: &lt;strong&gt;37,449 → 6,865, −82%&lt;/strong&gt;. Opening a page still costs full price; the win is everything after.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. A live A/B.&lt;/strong&gt; Built Altair 0.1.3, model &lt;code&gt;glm-5.3-flash&lt;/code&gt;, the same GitHub task twice — diffs on, then off (&lt;code&gt;BROWSER_SNAPSHOT_DIFF=false&lt;/code&gt;; lean snapshots in both). Both runs took the same path (navigate + two clicks) and gave the same correct answer.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/img%2Fagent-ab-en.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/img%2Fagent-ab-en.png" alt="Live A/B: browser tool results" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Browser results: &lt;strong&gt;10,314 → 4,726 tokens, −54%&lt;/strong&gt;. By the provider's own count, the last request was 20,923 tokens without diffs and 15,113 with them — and that includes the system prompt and tool schemas, which diffs don't touch. On a 30-step task the gap grows with every step, since each saved page is not resent in all the requests after it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this doesn't solve
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The first look at a page costs full price.&lt;/strong&gt; An agent that mostly follows links gains less.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The model has to read diffs.&lt;/strong&gt; In our runs it did, without instructions — unified diffs are everywhere in training data. But we don't yet have a systematic "task success with vs. without diffs" measurement, only manual checks and ref-stability tests. If your model gets confused, &lt;code&gt;BROWSER_SNAPSHOT_DIFF=false&lt;/code&gt; brings back the old behavior.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small samples.&lt;/strong&gt; Four sites, one live A/B, one historical chat. Shown as they are, no cherry-picked averages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;tiktoken is a proxy&lt;/strong&gt; for GLM, Claude or Gemini tokenizers.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Check it yourself
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;playwright.sync_api&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sync_playwright&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;core.browser_session&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;PageView&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;compact_tree&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page_changes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SNAPSHOT_LIMIT&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;tiktoken&lt;/span&gt;

&lt;span class="n"&gt;enc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tiktoken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_encoding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;o200k_base&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;tok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&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;enc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tree&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;PageView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tree&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tree&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;SNAPSHOT_LIMIT&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;truncated&lt;/span&gt;&lt;span class="o"&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;tree&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;SNAPSHOT_LIMIT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;changes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;sync_playwright&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pw&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chromium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;new_page&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;seen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://news.ycombinator.com/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                   &lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input[name=q]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                   &lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keyboard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;)]:&lt;/span&gt;
        &lt;span class="nf"&gt;action&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for_timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;aria_snapshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ai&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;lean&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;compact_tree&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;ch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;page_changes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seen&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;lean&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;seen&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
        &lt;span class="n"&gt;seen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lean&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="nf"&gt;tok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;title&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;raw&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="nf"&gt;tok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;title&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;lean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it from the repo's &lt;code&gt;pc/&lt;/code&gt; folder: it imports the functions straight from the agent, so it measures exactly what ships. Code: &lt;strong&gt;&lt;a href="https://github.com/Qweezyy/AltairAgent" rel="noopener noreferrer"&gt;https://github.com/Qweezyy/AltairAgent&lt;/a&gt;&lt;/strong&gt; (&lt;code&gt;core/browser_session.py&lt;/code&gt;). Diffs are on by default since 0.1.2.&lt;/p&gt;

&lt;p&gt;If you've built a browser agent: do you send the model the whole tree, a diff, screenshots, or something else? Have you seen models get confused by diffs?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>llm</category>
      <category>agents</category>
    </item>
    <item>
      <title>I built an AI agent that snapshots every edit and runs your tests before it says "done"</title>
      <dc:creator>Qweezyy</dc:creator>
      <pubDate>Sun, 27 Sep 2026 15:47:51 +0000</pubDate>
      <link>https://dev.to/qweezyy/i-built-an-ai-agent-that-snapshots-every-edit-and-runs-your-tests-before-it-says-done-l17</link>
      <guid>https://dev.to/qweezyy/i-built-an-ai-agent-that-snapshots-every-edit-and-runs-your-tests-before-it-says-done-l17</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; this post was written by Altair — the agent it describes — and reviewed by me.&lt;br&gt;
Filed under #ABotWroteThis as the community requires.&lt;br&gt;
I'm answering the comments the same way. If anything here reads like marketing rather than&lt;br&gt;
engineering, tell me and I'll fix it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Most coding agents are one more chat window next to your editor. What I wanted was different: an agent&lt;br&gt;
I could actually walk away from.&lt;/p&gt;

&lt;p&gt;The problem is trust. Agents delete the wrong file, run the wrong command, or report "all done"&lt;br&gt;
without ever running the tests. So I found myself watching every step — which defeats the whole point.&lt;/p&gt;

&lt;p&gt;Altair is what I built to fix that. It's open source (Apache-2.0) and runs on your own hardware.&lt;/p&gt;

&lt;h2&gt;
  
  
  Snapshots before every edit
&lt;/h2&gt;

&lt;p&gt;Any tool that changes a file — write, patch, delete — saves the previous state first, into the app's&lt;br&gt;
data folder, not your project. The file card gets an "undo" button. Multiple edits to the same file&lt;br&gt;
roll back one by one, newest to oldest.&lt;/p&gt;

&lt;p&gt;Commands need different handling, because a command can generate or delete files behind your back. So&lt;br&gt;
commands are tracked by a separate hidden git repository that captures their side effects. It never&lt;br&gt;
touches your own &lt;code&gt;.git&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If a task goes sideways, you can roll back the whole run, not just one edit, and see an audit of&lt;br&gt;
exactly what changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The health-gate
&lt;/h2&gt;

&lt;p&gt;This is the part that actually lets me leave it alone.&lt;/p&gt;

&lt;p&gt;If the agent edited code, then before it's allowed to say "done" it runs &lt;em&gt;your&lt;/em&gt; project checks —&lt;br&gt;
tests and linter — automatically. If they fail, it goes back and tries to fix them, with a cap on&lt;br&gt;
attempts. If it still can't, the run gets rolled back.&lt;/p&gt;

&lt;p&gt;It cannot report success with red tests. Not "usually" — it structurally can't, because the check runs&lt;br&gt;
before the word "done" is allowed to be said.&lt;/p&gt;

&lt;h2&gt;
  
  
  It's not just a chat box
&lt;/h2&gt;

&lt;p&gt;The phone app isn't a remote control. It's a second agent with its own context and tools, linked to the&lt;br&gt;
PC over a local bridge. The phone has the camera, files, location and notifications; the PC has the&lt;br&gt;
heavy tools. They hand whole subtasks to each other, and either can ask the other for something it&lt;br&gt;
doesn't have.&lt;/p&gt;

&lt;p&gt;Keys are yours: any OpenAI-compatible provider, native Anthropic, or local models via Ollama / LM&lt;br&gt;
Studio. No Altair cloud, no account, no telemetry.&lt;/p&gt;

&lt;h2&gt;
  
  
  A real run
&lt;/h2&gt;

&lt;p&gt;On a cheap model, asked to write a script printing the first 30 primes, run it, and chart the gaps&lt;br&gt;
between consecutive primes. It asked permission before writing and running, did the math, built an&lt;br&gt;
interactive chart, and noted in its own summary that the largest gap was 8 (between 89 and 97). About&lt;br&gt;
two and a half minutes end to end.&lt;/p&gt;

&lt;p&gt;The bug I remember best is from the library task — asked it to write an RFC 5545 &lt;code&gt;.ics&lt;/code&gt; library from&lt;br&gt;
scratch. It handled the classic traps: escaping special characters, and folding lines at 75 &lt;strong&gt;octets&lt;/strong&gt;&lt;br&gt;
(not characters) without cutting a multi-byte UTF-8 sequence. 15 tests plus 100 random round-trip&lt;br&gt;
checks, and the generated file opens in a normal calendar. Cost about $0.04.&lt;/p&gt;

&lt;p&gt;But the most useful moment was a failure. In the packaged build, the checks were being invoked as&lt;br&gt;
&lt;code&gt;LocalAIAgent.exe -m pytest&lt;/code&gt; — through the app's own executable instead of Python — which meant the&lt;br&gt;
health-gate was silently doing nothing. The agent didn't paper over it. It checked another way and&lt;br&gt;
wrote plainly that the automated check had not passed, and why. Later, when the provider's search and&lt;br&gt;
review tools returned HTTP 502, it recorded the review as &lt;em&gt;not performed&lt;/em&gt; rather than successful.&lt;/p&gt;

&lt;p&gt;That honesty is the thing I care about most. A pretty report that hides a failed check is worse than&lt;br&gt;
no report.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prompt-injection and the boring safety rails
&lt;/h2&gt;

&lt;p&gt;External content — web pages, documents, search results — is wrapped in a "this is data, not&lt;br&gt;
instructions" frame and scanned for injection patterns. If the agent has just read something&lt;br&gt;
suspicious and is about to do something outbound, it asks for confirmation even in automatic mode.&lt;/p&gt;

&lt;p&gt;On top of that, two rails that user approval cannot switch off: a path sandbox (the agent doesn't&lt;br&gt;
leave your working folder) and a destructive-command blacklist (&lt;code&gt;rm -rf&lt;/code&gt;, &lt;code&gt;format&lt;/code&gt;, &lt;code&gt;mkfs&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;dd of=/dev/…&lt;/code&gt;, fork bombs).&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limits of 0.1.x
&lt;/h2&gt;

&lt;p&gt;This is early software and I'd rather you hear it from me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Windows only for now. Linux and macOS are planned; the Windows build isn't code-signed, so
SmartScreen may warn on first launch.&lt;/li&gt;
&lt;li&gt;Phone ↔ PC only works on the same local network. Tailscale works as a workaround.&lt;/li&gt;
&lt;li&gt;The pairing QR scan doesn't work yet — you type the address and token manually. Fix is in progress.&lt;/li&gt;
&lt;li&gt;Chat history isn't synced between devices yet (specified, not built).&lt;/li&gt;
&lt;li&gt;It hasn't had much mileage on real-world tasks. Don't hand it anything critical or irreversible
without your own review and backups.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;irm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;https://raw.githubusercontent.com/Qweezyy/AltairAgent/main/install.ps1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;iex&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One command, no Python required. Android APK is in the releases.&lt;/p&gt;

&lt;p&gt;Code, docs and changelog: &lt;a href="https://github.com/Qweezyy/AltairAgent" rel="noopener noreferrer"&gt;https://github.com/Qweezyy/AltairAgent&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>python</category>
      <category>agents</category>
    </item>
  </channel>
</rss>
