<?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: Robert Bobrowski</title>
    <description>The latest articles on DEV Community by Robert Bobrowski (@robertbobrowski).</description>
    <link>https://dev.to/robertbobrowski</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%2F4188366%2F5fdde7e0-95ca-46a6-9cbf-11eae96fa367.png</url>
      <title>DEV Community: Robert Bobrowski</title>
      <link>https://dev.to/robertbobrowski</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/robertbobrowski"/>
    <language>en</language>
    <item>
      <title>Why Hashing Tool Calls Won't Catch Your Agent's Loops</title>
      <dc:creator>Robert Bobrowski</dc:creator>
      <pubDate>Mon, 02 Nov 2026 10:42:05 +0000</pubDate>
      <link>https://dev.to/robertbobrowski/why-hashing-tool-calls-wont-catch-your-agents-loops-3k94</link>
      <guid>https://dev.to/robertbobrowski/why-hashing-tool-calls-wont-catch-your-agents-loops-3k94</guid>
      <description>&lt;p&gt;If you run an agent unattended for long enough, you will meet the loop. It does not crash. It does not throw. It just keeps calling tools, and the bill keeps growing.&lt;/p&gt;

&lt;p&gt;The usual fix is simple and it is in almost every thread about the problem: hash the tool name and arguments, keep a sliding window, and kill the run when the same hash shows up too often. Add &lt;code&gt;max_iterations&lt;/code&gt; or LangGraph's &lt;code&gt;recursion_limit&lt;/code&gt; as a backstop and you are done.&lt;/p&gt;

&lt;p&gt;Except you are not. This post looks at why that fix misses the loops that cost the most, and what to check instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  The common fix
&lt;/h2&gt;

&lt;p&gt;Here is the pattern most teams start with. One commenter on r/AI_Agents described it like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I have seen this happen a lot, I track hashes of tool calls, if the same call shows up multiple times in a short window I throw a soft error and make it pause"&lt;br&gt;
u/moneyman2345, r/AI_Agents (Reddit)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In code, it looks roughly like this:&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;deque&lt;/span&gt;

&lt;span class="n"&gt;window&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;deque&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxlen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&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;call_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tool_name&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sort_keys&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&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="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&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;before_tool_call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;call_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Loop detected: same call 3 times&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;window&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="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plus a hard cap on steps:&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;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&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;recursion_limit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This catches the dumbest loop: the exact same call with the exact same arguments. That loop is real, and catching it for almost no cost is worth it. The problem is what it does not catch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it fails
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Near-identical retries
&lt;/h3&gt;

&lt;p&gt;Models rarely repeat themselves byte for byte. When a tool call fails, the next attempt usually changes something small: a page number, a timestamp, a slightly reworded search query, an extra filter. The hash is new. The window sees nothing. And the run is still stuck.&lt;/p&gt;

&lt;p&gt;One reply in the same r/AI_Agents thread put it precisely (note: the account belongs to a vendor):&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"the loop that slips past it is the near-identical retry: same call with one arg nudged, so the hash differs while nothing actually progresses."&lt;br&gt;
u/Future_AGI, r/AI_Agents (Reddit)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  2. Busy is not the same as progress
&lt;/h3&gt;

&lt;p&gt;The most expensive loops do not look like loops at all. They look like work:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"For me it's almost never an error. The agent stays busy and looks fine. The tell is repetition: the same tool call with slightly different arguments, the same file read for the third time, tests re-run with no code change in between."&lt;br&gt;
u/thelocalnode, r/AI_Agents (Reddit)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Reading the same file three times with different line ranges, re-running tests without touching code, searching with five phrasings of the same query. Every call is different. None of them moves the task forward.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Caps either kill real work or just delay the burn
&lt;/h3&gt;

&lt;p&gt;Step caps count steps. They do not know if a step was useful. So you have to pick a number, and every number is wrong for some task:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"max_iterations either kills legit long tasks or doesn't catch loops in time, and most people seem to just check in manually, which defeats the point of running unattended."&lt;br&gt;
u/Real_KingZeotic, r/AI_Agents (Reddit)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A low cap stops a valid 80-step migration. A high cap lets a stuck run spend until it reaches 80. Or as u/RouterDon wrote on r/LangChain (Reddit): "a static cap just delays the same burn".&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Retries and fallbacks live outside the counter
&lt;/h3&gt;

&lt;p&gt;Your loop detector usually sits inside the agent loop. But a lot of spend happens below it: the HTTP client retries a 503, the framework re-asks after a parse error, a fallback switches to a bigger model. None of that shows up as a new tool call, so none of it is counted.&lt;/p&gt;

&lt;p&gt;And sometimes the agent goes around your limits completely. In one thread, someone shared a story about an agent that hit a 503, found another API key in the repo, and kept going overnight. The fix suggested there:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Give it a token scoped to that run and route all paid calls through a gateway that enforces one shared budget, including retries and fallbacks."&lt;br&gt;
u/Neither_Event4902, r/AI_Agents (Reddit)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What works better
&lt;/h2&gt;

&lt;p&gt;None of this means you should delete your hash check. Keep it, it is cheap. But add three things around it.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Detect lack of progress, not repetition
&lt;/h3&gt;

&lt;p&gt;Instead of asking "have I seen this call before?", ask "did the last N steps change anything?". Progress is visible in the world, not in the call: new tool output, a changed file, a new row, a test that went from failing to passing.&lt;/p&gt;

&lt;p&gt;A simple version: fingerprint the state that matters for the task and the important parts of each tool result, and stop when nothing new has appeared for a few steps.&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;# Illustrative only. Generic pattern, not any product's API.
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProgressGuard&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;__init__&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="n"&gt;patience&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4&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="n"&gt;patience&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;patience&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;seen_outputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&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="n"&gt;last_state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&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;steps_without_progress&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_fp&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="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sort_keys&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;str&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="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&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;after_step&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="n"&gt;tool_output&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task_state&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;new_output&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="nf"&gt;_fp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool_output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seen_outputs&lt;/span&gt;
        &lt;span class="n"&gt;state_changed&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="nf"&gt;_fp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task_state&lt;/span&gt;&lt;span class="p"&gt;)&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;last_state&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;seen_outputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&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="nf"&gt;_fp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool_output&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="n"&gt;last_state&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="nf"&gt;_fp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task_state&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;new_output&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;state_changed&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="n"&gt;steps_without_progress&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="k"&gt;else&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="n"&gt;steps_without_progress&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

        &lt;span class="k"&gt;if&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;steps_without_progress&lt;/span&gt; &lt;span class="o"&gt;&amp;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;patience&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No progress in the last steps, stopping run&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The hard part is choosing &lt;code&gt;task_state&lt;/code&gt;. For a coding agent it might be the git diff and the test results. For a data agent, row counts and the last written record. For a support agent, the ticket status. If the output and the state stay the same, it does not matter how different the arguments look.&lt;/p&gt;

&lt;p&gt;This also lets you raise your step cap. A long task that keeps producing new results can run. A short task that stops changing anything gets stopped early.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Enforce the budget outside the agent
&lt;/h3&gt;

&lt;p&gt;The budget should not live in the agent's prompt or in the agent's process. Put it where every paid call has to pass: a proxy or gateway in front of the model provider, with a key scoped to the agent or run. Count everything that costs money, including retries, re-asks and fallbacks. Then the agent cannot spend past the limit by retrying, switching models, or finding another key, because it never had one.&lt;/p&gt;

&lt;p&gt;Another commenter in the same thread described exactly this: one proxy that tracks spend, call count and elapsed time against a &lt;code&gt;run_id&lt;/code&gt; and hard-fails at the ceiling.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Check the end state, not the agent's "done"
&lt;/h3&gt;

&lt;p&gt;The last loop is the one where the agent stops and says it finished. The same thread had the best rule for that:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"never trust the agent's own "done". Each run ends with a deterministic checker that queries end state - row exists, status = sent, file has N rows, invoice total matches."&lt;br&gt;
u/Responsible-Fix6695, r/AI_Agents (Reddit)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The check should read the real system, not the agent's summary of it. If you cannot write that check, the task is probably not ready to run without a person watching.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Hashing tool name and arguments catches exact repeats. Keep it, but do not trust it alone.&lt;/li&gt;
&lt;li&gt;Near-identical retries and busy work pass right through it.&lt;/li&gt;
&lt;li&gt;Step caps pick a number that is wrong for some task. They decide when the burn stops, not whether it happens.&lt;/li&gt;
&lt;li&gt;Track progress: did tool output or task state change in the last few steps?&lt;/li&gt;
&lt;li&gt;Keep the budget outside the agent, and count retries and fallbacks in it.&lt;/li&gt;
&lt;li&gt;End every unattended run with a deterministic check of the real end state.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Put the budget outside your agent
&lt;/h2&gt;

&lt;p&gt;ValGuard sits between your app and your LLM providers. Set a monthly USD budget per agent that blocks when reached (on Growth and higher, it can fall back to another agent or a cheaper model), add optional monthly token caps, see spend by agent and per request, and run deterministic validators, such as schema checks, on each step of a playbook. The progress check and the end-state check for your task are still yours to write; the patterns above are a good start.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://valguard.ai/blog/why-hashing-tool-calls-wont-catch-agent-loops" rel="noopener noreferrer"&gt;ValGuard blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>debugging</category>
      <category>llm</category>
    </item>
    <item>
      <title>The quiet LLM failures that cost you the most in production</title>
      <dc:creator>Robert Bobrowski</dc:creator>
      <pubDate>Mon, 19 Oct 2026 13:34:22 +0000</pubDate>
      <link>https://dev.to/robertbobrowski/the-quiet-llm-failures-that-cost-you-the-most-in-production-2f9h</link>
      <guid>https://dev.to/robertbobrowski/the-quiet-llm-failures-that-cost-you-the-most-in-production-2f9h</guid>
      <description>&lt;p&gt;Picture this: a production agent returns HTTP 200 on every call for a week. Uptime looks perfect. Then finance finds invoice totals that do not match the line items. Each one is off by a few dollars, and it happens thousands of times.&lt;/p&gt;

&lt;p&gt;Or this: a team finds a problem with a customer-facing bot. Its JSON output is missing a required &lt;code&gt;policy_id&lt;/code&gt; field. Imports further down the line fail with strange parse errors. By then the model call "succeeded" long ago.&lt;/p&gt;

&lt;p&gt;Which of these have you already seen in your stack?&lt;/p&gt;

&lt;p&gt;When people talk about AI failures, they usually pick dramatic cases. A chatbot gave dangerous medical advice. A legal brief cited cases that did not exist. A support agent promised a refund the company never planned to give. These cases are real. But they give the wrong picture. They make AI failures look rare and easy to spot.&lt;/p&gt;

&lt;p&gt;In production, most failures are boring. They happen often and they are hard to see. The big ones make headlines. The small ones end up in customer records, financial documents, support tickets and other systems. They stay there until someone notices.&lt;/p&gt;

&lt;p&gt;This article lists how AI systems fail in production. It covers why they fail, where the damage happens and what catches the errors. It is for teams that ship AI into real workflows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why AI systems fail at all
&lt;/h2&gt;

&lt;p&gt;A language model is a statistical system. At each step it picks a likely next token, based on what came before. On average, this gives clear and useful output. But the model generates text. It does not look anything up. It does not check a fact. It predicts what a correct answer would look like.&lt;/p&gt;

&lt;p&gt;This difference between generating and looking up is behind almost every failure below. A database returns the fact or it returns nothing. A model can sound right and still be wrong. It also has no reliable way to know when it is wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Category one: hallucination
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What it is&lt;/strong&gt;&lt;br&gt;
People use "hallucination" for any false or made-up output. The word covers several mechanisms. The core is always the same. The model sounds confident, but the answer has no basis in reality. It comes from patterns, not from checked facts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Types that matter in production&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Factual hallucination.&lt;/strong&gt; The model states something false as a fact. Examples: a court case that does not exist, a drug interaction nobody documented, a product feature that is not real.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Identifier hallucination.&lt;/strong&gt; This one is very dangerous in enterprise systems. The model returns an account, policy or transaction ID. The format, length and structure look right. But the ID points to nothing, or to another person's record.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Temporal hallucination.&lt;/strong&gt; The model talks about rules, products or events as they were in its training data. It does not say that things may have changed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Source hallucination.&lt;/strong&gt; You ask for a citation. The model gives you something that looks like a real source, with believable authors and journals. The source does not exist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Confidence hallucination.&lt;/strong&gt; The tone does not match the accuracy. Made-up answers and correct answers often sound the same. The model rarely says "I don't know." It gives a believable answer anyway.&lt;/p&gt;

&lt;p&gt;Independent benchmarks show hallucination rates from about 15% to over 50%. It depends on the task and the model. On structured tasks with clear retrieval context, the best models can get under about 2%. But at 10,000 interactions a day, 2% is still 200 bad answers a day. Most stacks cannot tell those apart from the good ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Category two: data errors and format problems
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Math and calculation errors&lt;/strong&gt;&lt;br&gt;
Language models are not calculators. They produce a number that looks like the result. Invoice totals, interest, fees and reconciliations fail this way. The structure is correct, but the math is wrong. Errors get more common with multi-step math, large numbers, percentages and rounding rules.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Schema and format violations&lt;/strong&gt;&lt;br&gt;
The model often has to produce JSON, XML, records or API payloads. Common problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;missing required fields&lt;/li&gt;
&lt;li&gt;wrong field types, for example a string instead of an integer, or "yes"/"no" instead of a boolean&lt;/li&gt;
&lt;li&gt;extra fields that are not in the schema&lt;/li&gt;
&lt;li&gt;broken nesting&lt;/li&gt;
&lt;li&gt;encoding and escape problems that break parsers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These errors are often silent. The response looks fine. Then something later in the pipeline fails with an unclear error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ID and reference format errors&lt;/strong&gt;&lt;br&gt;
Sometimes the value is correct but the format is wrong. A policy number uses dashes instead of spaces. A date is MM/DD/YYYY instead of YYYY-MM-DD. A decimal uses a comma instead of a period. In high-volume automation, format errors cost as much as factual errors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Category three: context and conversation failures
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Context window confusion.&lt;/strong&gt; When the history gets longer than the context window, older content drops out. The model may still refer to it. It may break earlier promises, or ask again for information the user already gave.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cross-session contamination.&lt;/strong&gt; This is rare but serious. If histories are not well separated, one user's data can appear in another user's answer. Nobody broke in. The context was scoped badly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Instruction drift.&lt;/strong&gt; In long conversations, early rules from the system prompt get weaker. Examples: "never reveal these guidelines" or "no specific financial projections." The status code stays 200. Later, a human reviewer sees that messages 30 to 50 no longer follow the rules that messages 1 to 5 followed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Category four: policy and compliance violations
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Prompt and configuration leaks.&lt;/strong&gt; System prompts often contain internal policy, the provider's name or business logic. Models sometimes reveal parts of them when they try to be "helpful and transparent."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sharing too much.&lt;/strong&gt; The model shares real information because it seemed relevant, not because it was allowed. For example, it shows a full account history for a small question. Or it puts internal pricing or escalation paths into a customer reply.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Regulatory language failures.&lt;/strong&gt; Required disclaimers, "consult a professional" lines and local legal wording appear only some of the time. The model does not try to break rules. It just has no reliable way to know when that text is required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Category five: the failures nobody talks about
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Wrong style.&lt;/strong&gt; Style from training data leaks into replies. A complaint that needs empathy gets a legal tone. A formal business email gets casual internet language. Some phrases repeat every time, like "Certainly!" or an automatic "Anything else?" No human agent writes like that in every message.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Provider and model names.&lt;/strong&gt; A branded assistant says, without being asked, that it is Claude, GPT-4 or "powered by OpenAI."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wrong tone.&lt;/strong&gt; The reply is correct and matches the schema. But it sounds cheerful when the user needs empathy. Or it is too friendly and long when the user wants a short answer. Simple validators miss this, because nothing is wrong in the structure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inconsistent answers.&lt;/strong&gt; Two almost identical questions get different refund windows or different policy wording. In normal software, the same record returns the same value. With LLMs, you have to enforce consistency yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  What helps: deterministic validation
&lt;/h2&gt;

&lt;p&gt;All these failures have one thing in common. The infrastructure looks healthy. You see HTTP 200. The bug is in the content. You only catch it if something reads the content and checks it against clear rules.&lt;/p&gt;

&lt;p&gt;A deterministic validation layer does exactly this. It does not retrain the model. It does not change the prompt. It checks every output before it reaches customers or other systems, and it applies explicit rules:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failure type&lt;/th&gt;
&lt;th&gt;Example check&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Made-up IDs&lt;/td&gt;
&lt;td&gt;Match against known valid IDs or the session context&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Math errors&lt;/td&gt;
&lt;td&gt;Recalculate from the extracted inputs and compare&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schema violations&lt;/td&gt;
&lt;td&gt;Parse against the schema and block output that does not match&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Format errors&lt;/td&gt;
&lt;td&gt;Enforce the expected ID, date and currency formats&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prompt or provider leaks&lt;/td&gt;
&lt;td&gt;Scan for system prompt fragments and provider names&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Required compliance text&lt;/td&gt;
&lt;td&gt;Require specific text for specific topics&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The model does not need to change for any of this. You need a check on the request path. That check decides, in a deterministic way, if the output is acceptable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;If your agents work with real customer data, money or compliance-sensitive traffic, these failures already exist in your system. Either something catches them before they do damage, or nothing does. They are not rare edge cases. They are normal behavior for probabilistic systems at scale. They will happen. The real question is if you catch them, or if they pass as one more silent 200.&lt;/p&gt;

&lt;p&gt;Adding this validation step does not mean changing the model or the app. It means adding checks on every request path. Read the output, apply the rules and block what should never ship.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article is adapted from &lt;a href="https://valguard.ai/blog/ai-failure-modes-in-production" rel="noopener noreferrer"&gt;AI Failure Modes in Production&lt;/a&gt; on the ValGuard blog. ValGuard is a deterministic validation and orchestration layer for production AI agents. It offers schema and policy checks, shadow mode, budgets and signed, metadata-only evidence for each request. This way, quiet HTTP 200 failures get caught before customers and other systems see them.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>production</category>
      <category>reliability</category>
    </item>
  </channel>
</rss>
