PromptABCD
FeaturesLearnHow it worksUse casesFAQGuideBlogContext Blocks
Sign inGet started free
Sign inSign up
PromptABCD

A calm home for your best AI prompts. Save them once, find them in seconds, reuse them forever.

Product

  • Features
  • Chrome Extension
  • Free Courses
  • How it works
  • Use cases
  • Blog
  • Context Blocks
  • Export Anywhere
  • FAQ

Resources

  • User guide
  • Learn prompting
  • Sign in
  • Get started free

© 2026 PromptABCD. All rights reserved.

Privacy PolicyTerms and Conditions
Home/Blog/Agent Loop Engineering/Recovering the Loop After a Bad Tool Result
Agent Loop Engineering

Recovering the Loop After a Bad Tool Result

Roughly a third of agent failures trace to one bad tool result the loop couldn't recover from. Agent loop recovery is the difference between a hiccup and a crashed run. Here's the teardown.

August 25, 2026·9 min read
ShareShare
⚡Featured Prompt— copy and use right now
def agent_loop(state, max_steps=12):
    for _ in range(max_steps):
        action = model_decide(state, tools)
        if action.name == "finish":
            return action.args["answer"]
        result = run_tool(action)          # what if this throws or returns junk?
        state = update_state(state, action, result)
    return force_answer(state)

About a third of the agent failures I've traced in production came down to a single bad tool result that the loop had no idea how to handle. Not a model failure, not a hard task — just one API returning a 500, or malformed JSON, or an empty payload where data was expected, and the whole run falling apart around it. Agent loop recovery is the discipline of surviving that moment, and most agents are shockingly bad at it because nobody designs the error path until it burns them.

This is a teardown of a fragile recovery flow, why it collapses on the first bad result, and the rewrite that turns a broken tool call into a recoverable bump instead of a dead run.

Before: The Weak Prompt

Here's a common loop with the error handling most agents ship with. The tool-calling code looks reasonable until a tool actually misbehaves.

hljs python
[object Object], ,[object Object],(,[object Object],):
    ,[object Object], _ ,[object Object], ,[object Object],(max_steps):
        action = model_decide(state, tools)
        ,[object Object], action.name == ,[object Object],:
            ,[object Object], action.args[,[object Object],]
        result = run_tool(action)          ,[object Object],
        state = update_state(state, action, result)
    ,[object Object], force_answer(state)

What this does: It runs the standard decide-act-update cycle, but it assumes every tool call returns clean, usable data — there's no path for a tool that errors, times out, or hands back something the model can't use, so any of those crashes the run or silently poisons the state.

Why It Fails

The loop fails because a bad tool result breaks it in one of two ways, and neither is handled. In the first,

run_tool
raises an exception — a network error, a 500, a timeout — and the whole loop crashes, ending the run with a stack trace instead of an answer. In the second, and this one's nastier, the tool doesn't raise but returns garbage: an empty list, a malformed object, an error message as a string. That garbage flows into
update_state
and the model now reasons over corrupted context, usually producing a confident wrong answer or spinning in confusion.

The deeper problem is that the loop treats tool results as trustworthy by default. In production, tools fail constantly — rate limits, transient outages, edge-case inputs — and an agent that assumes success is an agent that works in the demo and breaks in the wild. Good agent loop recovery starts from the opposite assumption: any tool call might fail, and the loop's job is to notice, interpret, and route around it.

⚡ Pro tip: Separate "the tool failed to run" from "the tool ran and returned nothing." These need completely different recovery. A failure to run means retry or try another tool; a successful empty result means the data genuinely isn't there and the agent should adjust its approach, not retry. Conflating them causes the exact infinite loops that plague fragile agents.

There's a third failure mode that's easy to miss: partial success. A tool returns some data but with a warning, or returns data that's stale, or succeeds but flags degraded results. A loop with no recovery logic treats partial success as full success and builds on a shaky foundation. Recovery isn't only about total failures; it's about the whole spectrum between clean success and hard error.

It helps to name the taxonomy explicitly, because each kind demands a different response and blurring them is what produces flaky agents. Transient failures — timeouts, rate limits, 500s — are worth retrying, because the same call may well succeed a moment later. Deterministic failures — bad input, missing permissions, not-found — will fail identically on retry, so the loop should route around them immediately rather than waste steps. Empty successes mean the data isn't there, so the agent should change tactics, not repeat the call. And degraded successes mean the data is usable but flawed, so the agent should proceed while flagging the caveat. Four categories, four responses; a loop that collapses them into one "something went wrong" bucket can't recover intelligently from any of them.

⚡ Pro tip: When you design a tool, design its failure contract at the same time. Decide up front what it returns for each failure category and make those returns machine-distinguishable, not just prose. A tool that signals

{"status": "rate_limited"}
versus
{"status": "not_found"}
lets the loop recover correctly; a tool that returns the string "error" for everything forces the model to guess, and it will guess wrong.

After: The Improved Prompt

The rewrite classifies every tool result before the model sees it, and gives the loop explicit recovery paths for each failure kind. The model is told, in the system prompt, how to respond to each.

hljs python
[object Object], ,[object Object],(,[object Object],):
    ,[object Object],:
        result = run_tool(action)
    ,[object Object], TimeoutError:
        ,[object Object], {,[object Object],: ,[object Object],, ,[object Object],: ,[object Object],}
    ,[object Object], ToolError ,[object Object], e:
        ,[object Object], {,[object Object],: ,[object Object],, ,[object Object],: ,[object Object],(e)}
    ,[object Object], result ,[object Object], ,[object Object], ,[object Object], result == []:
        ,[object Object], {,[object Object],: ,[object Object],, ,[object Object],: ,[object Object],}
    ,[object Object], {,[object Object],: ,[object Object],, ,[object Object],: result}

,[object Object], ,[object Object],(,[object Object],):
    retries = {}
    ,[object Object], _ ,[object Object], ,[object Object],(max_steps):
        action = model_decide(state, tools)
        ,[object Object], action.name == ,[object Object],:
            ,[object Object], action.args[,[object Object],]
        outcome = run_tool_safe(action)
        ,[object Object], outcome[,[object Object],] == ,[object Object],:
            key = action_fingerprint(action)
            retries[key] = retries.get(key, ,[object Object],) + ,[object Object],
            ,[object Object], retries[key] <= ,[object Object],:
                ,[object Object],                    ,[object Object],
            outcome[,[object Object],] = ,[object Object],    ,[object Object],
        state = update_state(state, action, outcome)
    ,[object Object], force_answer(state)

What this does: It wraps every tool call in a classifier that tags the result as ok, empty, retryable, or failed, auto-retries transient failures a bounded number of times, and passes a clear status into the state so the model always knows whether it's reasoning over real data or an error — the core of agent loop recovery.

The system prompt then tells the model how to react to each status.

hljs text
Each tool result has a status:
- ok: use the data normally.
- empty: the tool worked but found nothing. Do NOT retry the same
  call. Try a different approach or report the gap.
- failed: the tool could not run. Try an alternative tool or path;
  if none exists, tell the user what you could not do and why.
Never present failed or empty results as if they were successful data.

What this does: It gives the model a clear, per-status playbook so recovery is a reasoned choice rather than an accident, and explicitly forbids the most damaging behavior — dressing up a failure as a real answer.

Breaking Down Each Element

The result classifier is the foundation. By tagging outcomes before the model reasons over them, you convert silent corruption into an explicit signal the model can act on. This single layer prevents the "garbage in context" failure that produces so many confident wrong answers.

The bounded retry handles the most common real failure — transient errors — without inviting infinite loops. Two retries catches the vast majority of flaky-network cases; beyond that, something is genuinely broken and retrying just wastes steps. The per-action retry counter is what keeps a retryable failure from becoming an endless one.

The status-aware prompt closes the loop by turning the classification into behavior. A classifier that tags results is useless if the model ignores the tags; the prompt makes the tags actionable and, critically, bans presenting failures as data.

⚠️ Common mistake: Retrying every failure the same way. A malformed-input error will fail identically on retry — the input is still malformed — so retrying it just burns steps before the inevitable failure. Only retry failures that are plausibly transient: timeouts, rate limits, 500s. Deterministic failures (bad input, permissions, not-found) should route to an alternative path immediately, because the second attempt is guaranteed to fail exactly like the first.

⚡ Pro tip: Give the model an explicit "report what failed" exit. Many agents thrash because their only options are "succeed" or "keep trying," so when they can't succeed they try forever. A first-class "I couldn't complete this because X" outcome is both more honest and a hard stop against runaway recovery attempts.

Variations for Different Contexts

For a financial-data agent, recovery distinguishes stale data from missing data — a market feed returning yesterday's price is a partial success that must be flagged, not treated as current, because acting on stale numbers is worse than acting on none.

For a multi-tool research agent, recovery falls back across tools: if the primary search API fails, the loop tries a secondary source rather than giving up, treating the failure as a routing decision. An engineer at a data-vendor company told me this fallback pattern took their agent's completion rate from 88% to 99% with no model change.

For a code-execution agent, a failed run is expected and informative — the recovery path feeds the error back as debugging signal rather than treating it as a dead end, because for that agent, errors are the normal path to a working result.

For a customer-facing agent with external integrations — payment processors, shipping APIs, CRMs — recovery has to account for the fact that some failures leave the outside world in an uncertain state. If a payment call times out, you genuinely don't know whether the charge went through. The recovery path here can't just retry blindly, or you risk double-charging; it has to check the external state first ("did this transaction actually post?") before deciding whether to retry. This idempotency-aware recovery is more work, but for any agent that changes external systems, it's the difference between a resilient agent and one that occasionally causes real damage while trying to recover.

⚡ Pro tip: For agents with side-effecting tools, make recovery idempotent by having write tools accept a client-supplied idempotency key. Then a retry after an ambiguous timeout is safe — the backend recognizes the key and won't duplicate the action. Recovery logic that can't tell "did my last attempt actually happen?" is dangerous precisely at the moment it matters most.

Save and Reuse This

Recovery logic is tedious to rebuild and easy to get subtly wrong, which makes it exactly the kind of thing worth writing once and reusing. The result classifier, the bounded-retry pattern, and the status-aware prompt block are stable across almost any agent you'll build.

A prompt library like PromptABCD is a natural home for these — keep your recovery prompt and status playbook versioned so every new agent inherits resilient error handling from the start, instead of learning it the hard way the first time a tool returns a 500 in production. The teams that treat recovery as a reusable asset rather than a per-project afterthought are the ones whose agents survive contact with real traffic.

error handlingagent loopsrecoveryresiliencetool errors

Continue Reading

Rewriting the Goal Mid-Loop: Self-Reprompting
Agent Loop Engineering

Rewriting the Goal Mid-Loop: Self-Reprompting

An agent chasing a goal it had misread wasted 20 steps before failing. Agent self-reprompting lets a loop rewrite its own objective as it learns. Here's how to build it without letting it drift.

August 25, 2026·9 min read
Building a Loop That Asks for Help When Stuck
Agent Loop Engineering

Building a Loop That Asks for Help When Stuck

Most agent advice pushes full autonomy. That's wrong when the stakes are real. An agent ask for help loop knows when to stop guessing and pull in a human. Here's the teardown.

August 25, 2026·9 min read
State Machines vs Free-Form Agent Loops
Agent Loop Engineering

State Machines vs Free-Form Agent Loops

Wondering whether to let your agent roam free or lock it into defined states? An agent state machine trades flexibility for control. This case study shows when that trade pays off.

August 25, 2026·9 min read

Save the prompts from this post

PromptABCD is a free prompt manager. Paste, organize, and reuse your best AI prompts — no more hunting through chat history.

Start free →
← PreviousHandling Ambiguous Goals in the Agent LoopNext →Loop Timeouts and Wall-Clock Limits
Share this post:
ShareShare