<?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: AgentBasics</title>
    <description>The latest articles on DEV Community by AgentBasics (@agentbasics).</description>
    <link>https://dev.to/agentbasics</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%2F4233157%2Fd6096b9d-efc5-45b3-8ba2-5ee276011865.png</url>
      <title>DEV Community: AgentBasics</title>
      <link>https://dev.to/agentbasics</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/agentbasics"/>
    <language>en</language>
    <item>
      <title>LLM JSON output truncated? A deterministic repair ladder</title>
      <dc:creator>AgentBasics</dc:creator>
      <pubDate>Thu, 12 Nov 2026 16:09:36 +0000</pubDate>
      <link>https://dev.to/agentbasics/llm-json-output-truncated-a-deterministic-repair-ladder-772</link>
      <guid>https://dev.to/agentbasics/llm-json-output-truncated-a-deterministic-repair-ladder-772</guid>
      <description>&lt;p&gt;You prompted for JSON. The model returned something that is 95% JSON. &lt;code&gt;json.loads&lt;/code&gt; throws. Now what?&lt;/p&gt;

&lt;p&gt;The two common reactions are both wrong. Reacting with "the API errored" wastes a perfectly good response. Reacting with a pile of regex hacks silently corrupts data — a greedy &lt;code&gt;re.sub&lt;/code&gt; that strips a comma inside a string, a naive brace-matcher that trips over a quote in a value. The fix is a &lt;strong&gt;repair ladder&lt;/strong&gt;: a deterministic sequence of progressively more aggressive fixes, stopping at the first that parses, and — this is the part people skip — &lt;em&gt;reporting which rung saved you&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually breaks LLM JSON
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Markdown fences: triple-backtick json blocks wrapped around valid JSON. Harmless, constant.&lt;/li&gt;
&lt;li&gt;Prose around the object: "Sure! Here's the JSON:" then the object, then "Let me know!" Your parser needs the first balanced {...}, string-aware.&lt;/li&gt;
&lt;li&gt;Trailing commas: {"a": 1,} — JSON forbids them, JavaScript habits produce them.&lt;/li&gt;
&lt;li&gt;Python literals: True/None instead of true/null, single-quoted keys.&lt;/li&gt;
&lt;li&gt;Truncation mid-string: max_tokens fires in the middle of a value. The output is not recoverable in full — and pretending otherwise is how agents invent the missing half.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The ladder
&lt;/h2&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;repair_json&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="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;rungs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"clean"&lt;/span&gt;&lt;span class="p"&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="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"fence"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="n"&gt;strip_code_fences&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;# strip markdown fences&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"extract"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="n"&gt;first_balanced_span&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;# string-aware brace matcher&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"trailing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="n"&gt;remove_trailing_commas&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# ,} -&amp;gt; }&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"pyliterals"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;normalize_true_none&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;# True -&amp;gt; true, None -&amp;gt; null&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"quotes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="n"&gt;single_to_double_quotes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"truncation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;close_open_strings_and_brackets&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fix&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rungs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&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;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;fix&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="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;RepairReport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rung&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&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="nc"&gt;JSONDecodeError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;NoBalancedSpan&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;RepairReport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rung&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"hopeless"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two design details matter:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The brace matcher must be string-aware.&lt;/strong&gt; Counting &lt;code&gt;{&lt;/code&gt; and &lt;code&gt;}&lt;/code&gt; naively breaks the moment a value contains a brace. Track quote state and escapes while scanning — ten lines, no dependencies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The truncation rung is a last resort, and it must announce itself.&lt;/strong&gt; Closing open strings and brackets turns &lt;code&gt;{"items": [1, 2, 3&lt;/code&gt; into something that parses — but you recovered 3 of maybe 60 items. That's why the function returns the report: &lt;code&gt;rung="truncation"&lt;/code&gt; is your signal to log it, retry with a higher token cap, or tell the user the result is partial. A repair that silently pretends nothing happened is just hallucination with extra steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this looks like in practice
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;report&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;repair_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_llm_output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# -&amp;gt; obj = {"items": [1, 2, 3]}, report.rung = "truncation"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a real run from our test suite: fence stripped, trailing comma inside the array dropped, the truncated string closed — and the report says the answer came from the desperate rung, so the caller decides whether to trust it. A clean parse reports &lt;code&gt;rung="clean"&lt;/code&gt;; over a week of production runs the rung distribution tells you whether your prompts or your token caps are the real problem.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This is the json_repair module in AgentOps Autopilot (MIT, stdlib only). Pair it with the retry pattern from the previous post: a truncated response should count as a retryable failure, not a success.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Next in the series: &lt;a href="https://dev.to/blog/ai-agent-timeout-handling-python/"&gt;timeout handling so 40-step runs stop dying at step 41&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>debugging</category>
      <category>llm</category>
      <category>python</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Python Retry with Exponential Backoff and Jitter (stdlib only)</title>
      <dc:creator>AgentBasics</dc:creator>
      <pubDate>Thu, 12 Nov 2026 16:08:37 +0000</pubDate>
      <link>https://dev.to/agentbasics/python-retry-with-exponential-backoff-and-jitter-stdlib-only-o6g</link>
      <guid>https://dev.to/agentbasics/python-retry-with-exponential-backoff-and-jitter-stdlib-only-o6g</guid>
      <description>&lt;p&gt;Most retry code I see in agent projects looks 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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;call_api&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="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="s2"&gt;"gave up"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It survives a demo. In production it fails in four different ways, and each one is its own failure mode. Here's what's wrong and the stdlib-only pattern we ship instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 1: it retries permanent errors
&lt;/h2&gt;

&lt;p&gt;A 401 (bad key) and a 429 (rate-limited) are not the same failure. Retrying a 401 three times burns 6 seconds and a support ticket. Classify first:&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="no"&gt;RETRYABLE_EXCEPTIONS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;TimeoutError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ConnectionError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;OSError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="no"&gt;RETRYABLE_STATUS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;408&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;409&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;425&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;502&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;504&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anything outside those lists should fail on the first attempt. In an agent loop, a permanent error you keep retrying becomes the retry spiral: the same failure five times in a row until the runtime hard-stops the turn.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 2: fixed sleep creates a thundering herd
&lt;/h2&gt;

&lt;p&gt;When an API wobbles, every client that went down at the same moment comes back at the same moment — fixed 2-second sleeps synchronize the retry wave and knock the service down again. The fix is exponential backoff plus jitter:&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;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# 1.5, 3, 6, 12... capped&lt;/span&gt;
&lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;*=&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.75&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.25&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="c1"&gt;# jitter desynchronizes clients&lt;/span&gt;
&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cap matters too. Uncapped exponential backoff means your fifth retry sleeps four minutes — through your agent's entire turn budget.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 3: no total budget
&lt;/h2&gt;

&lt;p&gt;This is the one that kills AI agents specifically. If each attempt gets its own timeout and each retry its own sleep, total worst-case time is &lt;code&gt;timeout × (retries+1) + sleeps&lt;/code&gt;. A 45-second timeout with 3 retries can eat 4+ minutes — and agent platforms kill turns at 5. The retry loop needs a &lt;em&gt;shared deadline&lt;/em&gt; across all attempts, so the last attempt gets whatever time is left, not a fresh allowance:&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;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;deadline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;budget_s&lt;/span&gt;          &lt;span class="c1"&gt;# ONE budget for the whole call&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retries&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;deadline&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;                        &lt;span class="c1"&gt;# budget exhausted, fail honestly&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;run_with_timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;remaining&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="no"&gt;RETRYABLE_EXCEPTIONS&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;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;deadline&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;backoff_base&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jittered_delay&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# only sleep if budget survives it&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Problem 4: "success" that isn't
&lt;/h2&gt;

&lt;p&gt;The API returned 200 with 10 of 60 results. The call "succeeded" and your agent built on garbage. A retry helper for agents should take a semantic validator, and a rejected result should count as a retryable failure:&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;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;resilient_call&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="nf"&gt;my_llm&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="p"&gt;,&lt;/span&gt;
                     &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"llm"&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;45&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retries&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="n"&gt;validate&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;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"results"&lt;/span&gt;&lt;span class="p"&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;expected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The whole thing, assembled
&lt;/h2&gt;

&lt;p&gt;Put the four pieces together — classification, jittered capped backoff, shared budget, semantic validation — and you get a wrapper that returns a structured result instead of either a value or an exception:&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;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;resilient_call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"llm"&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;45&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retries&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="n"&gt;budget_s&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;validate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;shape_check&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;res&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;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# res.failure = {kind, name, attempts, last_error, elapsed_s}&lt;/span&gt;
    &lt;span class="n"&gt;journal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;failure&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# "same failure 5x today" becomes a query&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The structured failure record is the quiet win: your agent can &lt;em&gt;reason&lt;/em&gt; about why a call died ("429 three times, budget exhausted") instead of hallucinating a plausible answer over stack-trace soup.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This is exactly the resilient_call module in AgentOps Autopilot (MIT, stdlib only, 24/24 tests + a chaos gauntlet that injects each failure kind). If you'd rather paste it than rebuild it, the repo is free; the paid tiers add the templates and the runbook.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Next in the series: &lt;a href="https://dev.to/blog/llm-json-output-truncated-fix/"&gt;what to do when the LLM's JSON is truncated mid-string&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>backend</category>
      <category>python</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>We asked the internet what agent failure mode we were missing. It shipped 24 hours later.</title>
      <dc:creator>AgentBasics</dc:creator>
      <pubDate>Wed, 11 Nov 2026 22:30:16 +0000</pubDate>
      <link>https://dev.to/agentbasics/we-asked-the-internet-what-agent-failure-mode-we-were-missing-it-shipped-24-hours-later-36mf</link>
      <guid>https://dev.to/agentbasics/we-asked-the-internet-what-agent-failure-mode-we-were-missing-it-shipped-24-hours-later-36mf</guid>
      <description>&lt;p&gt;Yesterday our launch thread ended with a genuine question: &lt;em&gt;what's the sixth killer we're missing?&lt;/em&gt; We'd been writing about the five we keep hitting in production agents — wall-clock death at step 41, silent API truncation ("success" with 10 of 60 results), JSON cut mid-string by max_tokens, the double-charge after a timeout, and context death by a thousand tokens.&lt;/p&gt;

&lt;p&gt;One reply stopped us cold. It came from someone who clearly runs agents against real work, and it was one line:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"the failure mode is partial success with a confident summary"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Read that again, because it's nastier than the other five. Every failure we'd built defenses against &lt;em&gt;errors somehow&lt;/em&gt; — an exception, a timeout, a bad shape. This one doesn't. The call returned. The run completed. Eleven steps were planned, four left receipts, and then the model wrote a fluent, confident wrap-up of everything it "found." Your retry logic never fires because nothing failed. Your monitoring stays green. The user gets a beautifully written report of work that didn't happen.&lt;/p&gt;

&lt;p&gt;We call it completion ≠ correctness. And the honest answer to "do our tools catch it?" was: no. Not one of them.&lt;/p&gt;

&lt;p&gt;So today it shipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  The defense: receipts, not vibes
&lt;/h2&gt;

&lt;p&gt;The new module is &lt;code&gt;receipt_audit.py&lt;/code&gt; (MIT, stdlib-only, ~120 lines). The idea is boring and that's why it works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Plan.&lt;/strong&gt; Before the run, declare the steps you intend to take.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Receipts.&lt;/strong&gt; Every step that actually executes leaves a receipt — including partial ones (&lt;code&gt;detail="4/11 rows"&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit.&lt;/strong&gt; Before the final summary is allowed to reach the user, diff planned steps against receipts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enforce.&lt;/strong&gt; If the run is partial but the summary &lt;em&gt;sounds&lt;/em&gt; complete, the summary gets rewritten out loud:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PARTIAL (4/11 receipts; not done: post_report, verify_upload).
Found the issues! Everything is in place.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tone detector is deliberately crude — it flags summaries that say "all/every/done/successfully" when the receipt diff says otherwise, and it stays quiet when the summary already admitted partiality. The point isn't to catch every lie; it's to make pretending-competent structurally expensive.&lt;/p&gt;

&lt;p&gt;The verdict also feeds straight into the failure journal as a &lt;code&gt;partial_success&lt;/code&gt; record, so "same run went half-done 5 times this week" becomes a query instead of a vibe.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this felt like as a builder
&lt;/h2&gt;

&lt;p&gt;The loop — publish the failure taxonomy, get named a hole, ship the module, credit the person who named it by name in the README and changelog — took about 24 hours end to end. Two things surprised us:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The community audits better than we do.&lt;/strong&gt; We shipped five modules around failures we'd personally bled on. A stranger named the sixth in one sentence because they'd bled on a different one. Your failure taxonomy is whatever your scars are; other people's scars are the roadmap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Replies shape the release" is only marketing if you don't ship.&lt;/strong&gt; Say it once, ship once, in public, with credit — and it stops being a slogan and starts being a contract.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your agent's final answer is a paragraph, that paragraph is currently unaudited. Diff it against its receipts.&lt;/p&gt;

&lt;p&gt;The sixth killer: &lt;strong&gt;partial success with a confident summary.&lt;/strong&gt; Named in our comments by @ChanYop. Now a module, credited in the changelog.&lt;/p&gt;

&lt;p&gt;What's the seventh? The repo is free (MIT, zero dependencies):&lt;br&gt;
&lt;a href="https://github.com/AgentBasicsHQ/agentops-autopilot" rel="noopener noreferrer"&gt;https://github.com/AgentBasicsHQ/agentops-autopilot&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>The 5 failure modes killing your AI agent (and the 10-line fix for each)</title>
      <dc:creator>AgentBasics</dc:creator>
      <pubDate>Sun, 08 Nov 2026 19:33:37 +0000</pubDate>
      <link>https://dev.to/agentbasics/the-5-failure-modes-killing-your-ai-agent-and-the-10-line-fix-for-each-1jbb</link>
      <guid>https://dev.to/agentbasics/the-5-failure-modes-killing-your-ai-agent-and-the-10-line-fix-for-each-1jbb</guid>
      <description>&lt;p&gt;Every agent that calls external APIs meets the same five killers. Not model failures — plumbing failures. And they're the same five, in every language, for every framework, because they come from the network and the runtime, not the LLM.I got burned by all five in production (a 300-second runtime timeout that killed a 40-step run at step 41; an API that silently truncated "all 60 results" to 10 with no error; JSON cut mid-string by max_tokens; the same ValueError retried five times until a hard stop). So I extracted the fixes into five zero-dependency Python modules. MIT core: &lt;a href="https://github.com/AgentBasicsHQ/agentops-autopilot##" rel="noopener noreferrer"&gt;https://github.com/AgentBasicsHQ/agentops-autopilot##&lt;/a&gt; 1. The wall-clock killer (dies at step 41)&lt;strong&gt;The story:&lt;/strong&gt; your agent runs fine for 40 steps, then the platform's 300s turn limit fires. Everything is lost. No partial work, no status, nothing.&lt;strong&gt;The fix:&lt;/strong&gt; a turn budget shared across every call, with a reserve, and a ship-partial policy.&lt;br&gt;
&lt;br&gt;
&lt;code&gt;pythonfrom turn_budget import TurnBudgetbudget = TurnBudget(total_s=280, reserve_s=20)for step in plan:    if not budget.can_afford(estimate_s=15):        ship_partial(status="ran out of time", done=completed, remaining=plan[i:])        break    do(step)&lt;/code&gt;&lt;br&gt;
&lt;br&gt;
The rule that matters: &lt;code&gt;reserve_s&lt;/code&gt;. Without a reserve you discover the deadline &lt;em&gt;inside&lt;/em&gt; the last call, which is how you get zero output instead of 80% output.## 2. Silent API truncation ("all results" → 10 results, no error)&lt;strong&gt;The story:&lt;/strong&gt; you ask an API for everything. It returns a page. No error, no "more available" flag you bothered to check. Your agent reports a confident, wrong number.&lt;strong&gt;The fix:&lt;/strong&gt; semantic validation inside the retry wrapper — validate &lt;em&gt;shape and count&lt;/em&gt;, not just "it returned".&lt;br&gt;
&lt;br&gt;
&lt;code&gt;pythonfrom resilient_call import resilient_callr = resilient_call(    lambda: fetch_page(cursor),    name="fetch_all",    validate=lambda x: len(x["items"]) &amp;gt; 0 and "cursor" in x,    retries=3, timeout=30,)&lt;/code&gt;&lt;br&gt;
&lt;br&gt;
A &lt;code&gt;validate=&lt;/code&gt; that encodes your expectation ("non-empty", "has next cursor", "count matches the header") turns a silent wrong-answer into a loud retryable failure. This one check is worth the whole module.## 3. Truncated JSON (max_tokens cuts mid-string)&lt;strong&gt;The story:&lt;/strong&gt; the model returns 95% of a JSON document and stops. &lt;code&gt;json.loads&lt;/code&gt; blows up, your retry loop re-asks the whole thing, you pay double, and sometimes the model truncates at exactly the same spot.&lt;strong&gt;The fix:&lt;/strong&gt; a deterministic repair ladder — fence-strip, trailing-comma removal, Python-literal conversion, then bracket-closure for truncated docs — with an &lt;em&gt;honesty report&lt;/em&gt; telling you which rung fixed it.&lt;br&gt;
&lt;br&gt;
&lt;code&gt;pythonfrom json_repair import repair_jsonobj, report = repair_json(llm_output)if report.rung == "truncation":    # recovered what exists; re-ask ONLY the missing scope    log.warning("truncated at %s; partial recovered", report.detail)&lt;/code&gt;&lt;br&gt;
&lt;br&gt;
&lt;code&gt;strict_json=True&lt;/code&gt; in &lt;code&gt;resilient_call&lt;/code&gt; catches the same failure one level earlier: if your call returns a string that isn't valid JSON, the call &lt;em&gt;fails&lt;/em&gt; instead of returning the broken string three steps downstream.## 4. The retry spiral (same failure, five times, then a hard stop)&lt;strong&gt;The story:&lt;/strong&gt; a call fails. The retry logic retries. It fails identically — it's a ValueError, not a network flake — and burns your whole turn before the platform kills the run.&lt;strong&gt;The fix:&lt;/strong&gt; two parts. (a) Retry an &lt;em&gt;allowlist&lt;/em&gt; of transient classes, never a blanket &lt;code&gt;except&lt;/code&gt;. (b) An anti-spin counter: if this exact (kind, step) failure has happened 3 times today, stop and escalate.&lt;br&gt;
&lt;br&gt;
&lt;code&gt;pythonfrom failure_journal import FailureJournaljournal = FailureJournal("logs/failures.jsonl")# inside the failure branch:if journal.same_failure_count_today(r.failure["kind"], step_name) &amp;gt;= 3:    raise SystemExit(f"anti-spin: {step_name} failed 3x today — stop and report")&lt;/code&gt;&lt;br&gt;
&lt;br&gt;
Bonus: &lt;code&gt;journal.top_failures()&lt;/code&gt; gives you a ranked list of what actually breaks your agent, which is how you prioritize fixes instead of guessing.## 5. Silent context exhaustion (amnesia before the crash)&lt;strong&gt;The story:&lt;/strong&gt; the context window fills gradually. Your agent doesn't crash — it &lt;em&gt;forgets&lt;/em&gt;. Early instructions quietly vanish, and it hallucinates continuity. The crash is loud; the amnesia is silent, and it's worse.&lt;strong&gt;The fix:&lt;/strong&gt; a pressure gauge with graduated actions, checked between steps.&lt;br&gt;
&lt;br&gt;
&lt;code&gt;pythonfrom context_pressure import ContextPressurecp = ContextPressure(model_window=128_000, system_overhead=3_000)cp.add("tool", tool_output)          # every message goes throughmatch cp.action():                   # leading indicator, not post-mortem    case "prune_tool_results":  cp.prune_tool_results(keep_last=5)    case "summarize_oldest_turns": summarize_and_replace(oldest=8)    case _: checkpoint_and_restart()  # hard_stop_ship_partial&lt;/code&gt;&lt;br&gt;
&lt;br&gt;
Pruning tool results (usually 70% of your tokens) buys back most of the window for free. Do it &lt;em&gt;before&lt;/em&gt; the API returns a context error, not after.---## The boring synthesisNone of these are model problems. They're the same failure classes that network programmers solved decades ago — timeouts, budgets, backoff, validation, logging — re-applied to a system where the "network call" is an LLM and the failure modes include &lt;em&gt;silent semantic truncation&lt;/em&gt;.All five modules are stdlib-only, Python 3.10+, 18-check test suite: &lt;strong&gt;&lt;a href="https://github.com/AgentBasicsHQ/agentops-autopilot" rel="noopener noreferrer"&gt;https://github.com/AgentBasicsHQ/agentops-autopilot&lt;/a&gt;&lt;/strong&gt; (MIT core).If you want the production templates (agent loop, cron pipeline, gateway wrapper, eval harness) + the 12-failure-mode runbook, that's the paid tier on the site — but the free core is genuinely useful on its own, and I'd rather you test it first.&lt;strong&gt;What failure mode am I missing?&lt;/strong&gt; Genuinely asking — the runbook is a living doc and the next addition comes from comments.&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
