<?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: TaskPool AI</title>
    <description>The latest articles on DEV Community by TaskPool AI (@taskpool_ai_461d0838c8f56).</description>
    <link>https://dev.to/taskpool_ai_461d0838c8f56</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%2F4135463%2Fd4328cdb-bcf4-40bc-9ac8-d86fb05533b5.png</url>
      <title>DEV Community: TaskPool AI</title>
      <link>https://dev.to/taskpool_ai_461d0838c8f56</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/taskpool_ai_461d0838c8f56"/>
    <language>en</language>
    <item>
      <title>Escalate on a budget, not an error: when an AI agent should hand off to a human</title>
      <dc:creator>TaskPool AI</dc:creator>
      <pubDate>Wed, 14 Oct 2026 14:38:42 +0000</pubDate>
      <link>https://dev.to/taskpool_ai_461d0838c8f56/escalate-on-a-budget-not-an-error-when-an-ai-agent-should-hand-off-to-a-human-2810</link>
      <guid>https://dev.to/taskpool_ai_461d0838c8f56/escalate-on-a-budget-not-an-error-when-an-ai-agent-should-hand-off-to-a-human-2810</guid>
      <description>&lt;p&gt;&lt;em&gt;A retry is not a fresh draw, and waiting for an exception means waiting past the point where anything was recoverable. Four counters you can read before the failure, a handoff packet, and Python that runs.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Mercor's best agent on APEX-Agents passes &lt;a href="https://arxiv.org/abs/2601.14242" rel="noopener noreferrer"&gt;24.0% of its tasks on one attempt&lt;/a&gt;. Give it eight attempts and &lt;a href="https://www.mercor.com/blog/introducing-apex-agents/" rel="noopener noreferrer"&gt;it reaches about 40%&lt;/a&gt;. A flat 24% chance per attempt would put eight tries near 89%, so most of those failures belong to the task and not to the dice. That inference is mine. Nothing I read in Mercor's write-up says it, and you are free to argue with it.&lt;/p&gt;

&lt;p&gt;The usual answer to a failed run is another run, often resumed from the same conversation. &lt;a href="https://arxiv.org/abs/2509.09677" rel="noopener noreferrer"&gt;Sinha and colleagues&lt;/a&gt; found that models make more mistakes when their own earlier mistakes sit in the context, and that scaling the model does not remove the effect, although thinking models mitigate it. Their tasks were synthetic, with the plan handed to the model, so carrying the result over to a production retry is my extension and not theirs.&lt;/p&gt;

&lt;p&gt;That leaves a move most harnesses never budget for: stopping on purpose and giving the work to someone else. The rest of this post covers the trigger logic and the packet, and then what it costs. If you came for code, skip to the section called "Counters that fire before the exception".&lt;/p&gt;

&lt;h2&gt;
  
  
  Read the stop reason first
&lt;/h2&gt;

&lt;p&gt;I'm putting the objection ahead of the fix. A lot of what looks like an agent giving up is a harness limit doing exactly what it was configured to do, and handing that to a person means paying a stranger to find your bug. I wrote up &lt;a href="https://taskpool.ai/blog/agent-stops-mid-task" rel="noopener noreferrer"&gt;six causes of agents stopping mid-task&lt;/a&gt; on the Taskpool blog, and five of them live in the harness. Run that triage before you wire up anything below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Counters that fire before the exception
&lt;/h2&gt;

&lt;p&gt;The class below watches four things per tool call: an identical call repeating inside a short window, steps since the last verified checkpoint, total steps, and wall-clock time. It returns a reason string the moment one trips and &lt;code&gt;None&lt;/code&gt; otherwise.&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;from&lt;/span&gt; &lt;span class="n"&gt;__future__&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;annotations&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Callable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Budget&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;max_steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;           &lt;span class="c1"&gt;# set from your own successful runs, not a default
&lt;/span&gt;    &lt;span class="n"&gt;max_seconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
    &lt;span class="n"&gt;max_repeats&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;     &lt;span class="c1"&gt;# identical tool call (name + args) this many times...
&lt;/span&gt;    &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;          &lt;span class="c1"&gt;# ...inside the last N calls
&lt;/span&gt;    &lt;span class="n"&gt;stall_steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;    &lt;span class="c1"&gt;# steps allowed without a verified checkpoint
&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Tripwire&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;budget&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Budget&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;clock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Callable&lt;/span&gt;&lt;span class="p"&gt;[[],&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;]&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="n"&gt;monotonic&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;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;budget&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;clock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;clock&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;t0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;clock&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&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&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;since_checkpoint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&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;recent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;deque&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;=&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="n"&gt;budget&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;observe&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&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;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;checkpoint_advanced&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&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="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Optional&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="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Call once per tool call. Returns a reason string when it is time to hand off.&lt;/span&gt;&lt;span class="sh"&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&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&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;since_checkpoint&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;if&lt;/span&gt; &lt;span class="n"&gt;checkpoint_advanced&lt;/span&gt; &lt;span class="k"&gt;else&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;since_checkpoint&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

        &lt;span class="n"&gt;blob&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;tool&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="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="n"&gt;fp&lt;/span&gt; &lt;span class="o"&gt;=&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;blob&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="mi"&gt;12&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;recent&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;fp&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;recent&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;fp&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_repeats&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;repeated_action&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;since_checkpoint&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;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stall_steps&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;checkpoint_stall&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;steps&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;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_steps&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;step_budget&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clock&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;t0&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;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_seconds&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;time_budget&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="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Handoff&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;goal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;                &lt;span class="c1"&gt;# one sentence, readable by someone with no context
&lt;/span&gt;    &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;              &lt;span class="c1"&gt;# which tripwire fired
&lt;/span&gt;    &lt;span class="n"&gt;last_verified&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;       &lt;span class="c1"&gt;# last checkpoint that passed its check
&lt;/span&gt;    &lt;span class="n"&gt;tried&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&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="c1"&gt;# short descriptions, not the transcript
&lt;/span&gt;    &lt;span class="n"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;                 &lt;span class="c1"&gt;# the single thing the human has to do
&lt;/span&gt;    &lt;span class="n"&gt;accept_if&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;           &lt;span class="c1"&gt;# how you will judge what comes back
&lt;/span&gt;    &lt;span class="n"&gt;deadline_hours&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;spend_cap_pence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;     &lt;span class="c1"&gt;# integer minor units, never floats
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The thresholds are defaults I picked so the tests had something to assert. They are not measured. &lt;code&gt;max_steps&lt;/code&gt; is the one that matters, and I'd start from the longest successful run you have on file, add slack, and then look at which counter actually fires.&lt;/p&gt;

&lt;p&gt;The checkpoint counter is the best signal and the most expensive to earn. A checkpoint here means a state something outside the model confirmed, such as a passing test or a row that exists in the database. If you cannot say what a checkpoint is for your task, stall detection is not available to you yet, and defining it will take longer than writing the class.&lt;/p&gt;

&lt;p&gt;Arguments are serialised with sorted keys before hashing, so &lt;code&gt;{"a": 1, "b": 2}&lt;/code&gt; and &lt;code&gt;{"b": 2, "a": 1}&lt;/code&gt; count as the same call. Without that, an agent can loop forever while shuffling its arguments.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the human is handed
&lt;/h2&gt;

&lt;p&gt;The transcript stays out. It carries the model's own errors, which is what the Sinha result says to keep away from the next attempt, and it costs a person real time to read. The &lt;code&gt;Handoff&lt;/code&gt; dataclass above is the replacement: a goal a stranger can follow, which counter fired, the last checkpoint that passed, short descriptions of what was tried, one ask, and a deadline.&lt;/p&gt;

&lt;p&gt;Two fields do more than they look like. &lt;code&gt;accept_if&lt;/code&gt; forces you to write the check before the work leaves, which is the point where it is cheapest to be strict. &lt;code&gt;spend_cap_pence&lt;/code&gt; is an integer because money held as a float eventually produces a bug report.&lt;/p&gt;

&lt;p&gt;Wiring it to LangGraph takes one node. Its &lt;a href="https://docs.langchain.com/oss/python/langgraph/interrupts" rel="noopener noreferrer"&gt;interrupt docs&lt;/a&gt; require a checkpointer and a &lt;code&gt;thread_id&lt;/code&gt;, and resuming goes through &lt;code&gt;Command(resume=...)&lt;/code&gt;.&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;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asdict&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langgraph.types&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;interrupt&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;escalate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;packet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Handoff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;goal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Confirm the shop sign matches the listing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;last_verified&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;listing page parsed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;tried&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;fetched listing twice&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;street view unavailable&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;ask&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Photograph the shopfront and sign&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;accept_if&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Sign text legible in at least one photo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;deadline_hours&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;spend_cap_pence&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Nothing with side effects above this line.
&lt;/span&gt;    &lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;interrupt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;asdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;packet&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;human_result&lt;/span&gt;&lt;span class="sh"&gt;"&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 comment is not decoration. The docs say the node restarts from its first line when you resume, so anything above &lt;code&gt;interrupt()&lt;/code&gt; runs twice, and they warn against wrapping the call in a bare &lt;code&gt;try/except&lt;/code&gt; because the pause is implemented as an exception. I ran this round trip against &lt;code&gt;langgraph&lt;/code&gt; 1.2.15 with the in-memory checkpointer and it passes, along with five tests for the counters. In production the checkpointer has to outlive your process, because a person who answers in two days will not be answering a process that is still running.&lt;/p&gt;

&lt;h2&gt;
  
  
  Not every handoff goes to a stranger
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;The run is stuck because&lt;/th&gt;
&lt;th&gt;Hand it to&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The next step needs a decision only your team has authority to make&lt;/td&gt;
&lt;td&gt;The on-call engineer or the account owner&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The output exists but needs judgement you already employ&lt;/td&gt;
&lt;td&gt;An in-house reviewer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The next step needs a person in a particular place, or an independent check of the agent's own claim&lt;/td&gt;
&lt;td&gt;An outside verified human&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Only the last row is a job for a marketplace. Sending the first two to a stranger gets you an answer from someone with no authority and no context.&lt;/p&gt;

&lt;h2&gt;
  
  
  If the destination is a job board
&lt;/h2&gt;

&lt;p&gt;I co-founded Taskpool International, which runs a marketplace where AI agents post tasks for verified humans, so weight this section accordingly. The Python above is vendor-neutral and I have not wired it to any marketplace in this post. What follows comes from reading &lt;a href="https://taskpool.ai/agent/docs" rel="noopener noreferrer"&gt;Taskpool's agent docs&lt;/a&gt;, not from a run, and each point applies to any board you might use.&lt;/p&gt;

&lt;p&gt;Listings are public. In Taskpool's case the task description appears on the public job board, so the &lt;code&gt;ask&lt;/code&gt; field must never carry credentials or customer data. Plan for a stranger to read it.&lt;/p&gt;

&lt;p&gt;Handoffs have a floor price. The docs set a minimum payout of £5.00 per worker and a 10% platform fee on top, so the cheapest single slot costs £5.50. That number changes which runs are worth escalating at all.&lt;/p&gt;

&lt;p&gt;Silence can pay. The docs say a submission still in review seven days after the worker's deadline is accepted automatically and paid. Run your &lt;code&gt;accept_if&lt;/code&gt; check from a webhook, not from someone remembering.&lt;/p&gt;

&lt;p&gt;A loop that escalates forever is a retry loop that sends you invoices. Cap spend per key as well as per task; Taskpool's docs describe a monthly budget per API key that refuses funding before any money moves.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to change on Monday
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Log the stop reason on every model call before you theorise. It costs you some log volume.&lt;/li&gt;
&lt;li&gt;Pick one verified checkpoint per task type. If the task has none, accept that stall detection is off and say so in the runbook.&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;max_steps&lt;/code&gt; from your own successful runs and log which counter fires. You will pay for handoffs on runs that would have finished, and nobody has published how often.&lt;/li&gt;
&lt;li&gt;Write &lt;code&gt;accept_if&lt;/code&gt; before the work leaves, in a form code can check.&lt;/li&gt;
&lt;li&gt;Strip secrets from the packet and assume a stranger can read it.&lt;/li&gt;
&lt;li&gt;Cap spend per task and per key.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you want the longer argument for why task length rather than difficulty sets the wall, I made it with the benchmark numbers in &lt;a href="https://taskpool.ai/blog/agent-task-duration-failure" rel="noopener noreferrer"&gt;Duration, not difficulty, is what breaks agents&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The number I don't have
&lt;/h2&gt;

&lt;p&gt;I know of no published distribution of why production agent runs get handed off, or of how often a budget like this fires on a run that would have succeeded. That false-positive rate decides whether any of this is worth the money, and I have none. If you have traces, I would rather have your numbers than your agreement.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;When should an AI agent escalate to a human?&lt;/strong&gt; When a counter trips, or when the next step needs a person on site or an independent check that the agent cannot supply. Do it before the failure, while the state is still recoverable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is retrying a failed agent run a good recovery strategy?&lt;/strong&gt; Not by default. Models can condition on their own earlier errors, an effect thinking models mitigate and scale alone does not remove, so a fresh run seeded with a clean summary is the safer retry.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How does LangGraph pause an agent for human input?&lt;/strong&gt; Call &lt;code&gt;interrupt()&lt;/code&gt; inside a node, with a checkpointer and a &lt;code&gt;thread_id&lt;/code&gt; configured. Resume the same thread with &lt;code&gt;Command(resume=...)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I used Claude to draft this post. Every figure links to its source, checked on 14 October 2026, and the Python was run: six tests pass.&lt;/em&gt;&lt;/p&gt;

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