Development

Surfacing Tool Errors to an Agent Without Looping Forever

Antonio Evans9 min read

A clean isometric diagram of a loop between two boxes labeled 'Agent' and 'Tool'.
Key points

Key points

  1. 1A tool error is the model's only signal for what happened, so vague text like "Error: failed" or a raw stack trace teaches it nothing and invites an identical retry.
  2. 2Write every returned error as an instruction: what failed, why, which arguments were rejected, and one concrete next action.
  3. 3Hash the tool name plus a canonical form of its arguments, count identical failures, and after two or three return a terminal message that forbids the repeat.
  4. 4Stop conditions belong in the orchestration layer you control, not in the prompt, because the model cannot reliably police its own loop.

Why a model retries, and why your error text is the cause

When a language model calls a tool, the result comes back to it as an observation, alongside the full prior conversation (race and observation loop). The model has no debugger and no logs. The string you return is the entire world. A message of "Error: failed" tells the model that something broke but not what, so its only available move is to try again, hoping the transient condition clears. Raw stack traces are worse: they are long, full of frame names the model cannot act on, and consume tokens on every turn.

This is the same problem technical writers have solved for decades in other directions. Classic error-message guidance says to show the error close to its source and make it constructive, so the reader can act (error-message guidelines). API design guidance is blunt about it: an error exists so that both humans and software can decide what to do next, and it should carry an application-specific code that clients can branch on (errors best practices). If you treat the model as a client rather than a person, those rules transfer almost unchanged.

There is a second, subtler cause. Ambiguous tool feedback creates loops. A tool that returns a partial result, or adds a phrase like "more results may be available", signals to the agent that another call might produce something better, so it calls again with the same arguments. In a published demonstration of this pattern, ambiguous tools produced fourteen calls before the agent gave up, while the same tasks with clear success states stopped in two (reasoning-loop experiment). The extra twelve calls were pure spend. The model was doing what the text implied.

The lesson is uncomfortable but simple: the error message is an API design surface, and it is usually the only error surface the model ever sees. Documentation teams already know that incomplete or misleading descriptions push consumers into support tickets and failed integrations (documentation's effect on adoption). For an agent, "support ticket" means another tool call, billed to you.

The four-part error message and a worked rewrite

Make the error an instruction. Every message returned to a model should carry four parts, in this order:

  1. Code and what failed, in a stable, machine-readable token such as trip_not_possible or invalid_argument.
  2. Why it failed, in the terms the operation uses, not the terms your infrastructure uses.
  3. The rejected arguments, echoed back exactly as the model sent them, so it can see its own mistake.
  4. One next action: change a named parameter, call a named alternative tool, or stop and report.

The first three make the fourth credible. Compare, using a search tool that requires a bounded price range:

Bad:
Error: failed

Bad:
Traceback (most recent call last):
  File "/srv/tools/search.py", line 88, in search_flights
    assert max_price > 0
AssertionError

Good:
search_flights failed (code: invalid_argument).
Reason: max_price must be greater than 0.
Rejected arguments: {"origin": "SFO", "destination": "JFK", "max_price": -1}.
Next action: call search_flights again with max_price as a positive number,
or call search_flights_any_price if no budget constraint applies.

The good version costs perhaps sixty tokens. A single avoided retry on a mid-sized context usually pays for it several times over.

The four parts line up with longer-standing API conventions. A complete error object carries a status category, a short summary, a detailed message, an application-specific code, and a pointer to more information (errors best practices). Follow the same structure on the client side too: validate before the call where you can, and never hand the model a raw server error string without shaping it (API error handling). The failure to avoid, and guidance is explicit about it, is a catch-all that describes the category but not the fix.

Tool definitions deserve equal care, because the model's first mistake usually starts there. The description tells the model not just what a tool does but when to use it, and with several tools available, vague descriptions produce wrong selections before you ever reach the error path (function calling guide). The message-writing pattern below assumes your schema already states required fields, types and allowed values, mirroring the completeness expected of a published API reference (reference documentation).

SituationWhat the model needsNext action to name
Bad argument type or valueThe rejected value and the constraintRe-call with a corrected parameter
Missing required fieldWhich field, and its expected shapeRe-call with the field supplied
Authentication or permissionThat the call failed before any effectStop, report, or use the read-only tool
Resource not foundThat no effect occurredTry the search tool before re-calling
Timeout after dispatchThat the effect may already have happenedVerify with the status tool, never re-send
Rate limitThat waiting is the fixWait, or switch to the fallback tool

That timeout row is where naive retries cause real damage. When a call times out after it was dispatched, the effect may have already landed, and a blind retry duplicates the action. A verification-aware wrapper that checks postcondition state before retrying cuts duplicate actions while keeping task success rates comparable, without changing the model (verified tool calls). Your error text should therefore distinguish three outcomes explicitly: no effect, effect unknown, effect applied.

A hard cap on identical failures in the orchestration layer

Better messages reduce the retry rate. They will not reduce it to zero, and that is why you need a cap that does not depend on the model's cooperation. The pattern is small.

Fingerprint each call as the tuple of tool name and a canonical serialization of its arguments: sorted keys, normalized whitespace, no floats where an integer will do. Count consecutive failures per fingerprint for the turn. On the terminal failure, return a message that states retrying the same call is not allowed, names the alternatives, and tells the model to report the failure if no alternative applies.

import hashlib, json

RETRY_LIMIT = 3  # consecutive identical failures allowed per turn

def fingerprint(name, args):
    canonical = json.dumps(args, sort_keys=True, separators=(",", ":"))
    return hashlib.sha256(f"{name}:{canonical}".encode()).hexdigest()

def wrap(name, fn, counts):
    def call(args):
        fp = fingerprint(name, args)
        counts[fp] = counts.get(fp, 0) + 1
        try:
            result = fn(args)
            counts.pop(fp, None)
            return result
        except ToolError as e:
            if counts[fp] >= RETRY_LIMIT:
                return terminal(name, fp, e)
            return e.rendered_message  # the four-part text
    return call

Two decisions inside that snippet matter more than the code.

What counts as identical. Hash the arguments, not the intent. A model that slightly rewords a string argument has not changed its approach, and an exact-string hash will let it retry indefinitely with cosmetic edits. Normalize aggressively: lowercase where case is not meaningful, strip trailing slashes from paths, round numeric tolerances. When a retry does change the arguments meaningfully, let it through and reset the counter, because that is the behavior you want to reward.

Where the terminal message goes. It must be a tool result the model sees, not an exception your framework swallows. Counting duplicate calls before execution and returning a blocking message to the model is the mechanism; the debounce hook demonstration shows the same idea inside one agent framework, and its author notes the pattern applies to any framework with lifecycle hooks.

The terminal message itself should be short and imperative:

search_flights failed 3 times with identical arguments (code: invalid_argument).
Retrying the same call is not allowed.
You must now either call search_flights with a different max_price,
call search_flights_any_price, or stop and report this failure to the user.

From the model's point of view this is another observation, and observations shape the next action (function calling guide). The difference is that it names the only legal moves.

How many identical failures to allow

Two or three is the practical range, and the choice should follow the failure class rather than a global constant.

Failure classCapWhy
Deterministic (validation, missing field, unlisted enum)1The input cannot succeed on a repeat; fix it or stop
Unknown effect (timeout, disconnect after dispatch)0 blind retriesVerify state first, then retry only if nothing landed
Transient (rate limit, 5xx)2 to 3 with backoffThe same request may succeed after a pause

The distinction comes from treating agent failures in layers: rate limits and provider 5xx errors are transient, while tool failures from bad inputs are not, and a single retry policy for both wastes money or hides real bugs (retries in agents). Retry only what the error text says is retryable, and put that verdict in the message rather than leaving the model to infer it.

If your traces do not distinguish these classes, cap by budget as well. Set a per-turn ceiling on tool calls and a per-task ceiling on spend, and treat a cap hit as a reportable incident rather than a silent stop. Observability at this layer is worth wiring early. Splitting the response channel (what the model sees) from the effect channel (what actually happened) is the root of most duplicate-action bugs, and you cannot fix the gap you cannot see (verified tool calls).

Documenting tool errors for both readers at once

Every tool error has two audiences: the model that reads it now and the engineer who reads your tool catalog later. Your processes should serve both. Industry surveys cited in a documentation guide report that developers overwhelmingly rely on the documentation bundled with APIs and SDKs to learn how to use them, which is why error documentation belongs in the reference rather than a wiki (documentation usage and practices). The UR sequence steps for writing and maintaining API descriptions, applied to agent tools, point the same way: reference, guides, and worked examples (API documentation guide).

Concretely, keep a table with one row per error code: code, which tool raises it, the argument constraints it reflects, whether retrying can help, and the alternative tool to name. Generate the message templates from that table so the text the model sees and the text in your docs cannot drift. This is the docs-as-code idea applied to prompts: documentation lives beside the code it describes, is reviewed with it, and deploys with it (docs as code). When a specification file drives generation, code examples and references stay synchronized as the tool evolves (single source of truth).

Two hygiene rules carry over directly. Do not leak secrets or internal identifiers into error text that gets sent to a model provider; the message should carry only what the problem requires (API error handling). And keep the format uniform across tools, because uniformity is what lets the model generalize what it learned from one failure in part to another tool (errors best practices). That generalization is the difference between an agent that recovers and one that loops.

One practical note on cost: errors that run to hundreds of tokens get appended to context and re-sent on every subsequent turn, so brevity compounds as much as verbosity does. Aim for a message under roughly a hundred tokens unless the alternative genuinely needs explaining, and put the long explanation behind a link referenced in the message.

A rollout checklist and where Quanta fits

Roll the pattern out in stages, and measure at each stage.

StepCheckDone when
1Every error path returns a code, a reason, the rejected arguments and one next actionNo raw exception reaches the model
2Timeouts classified as effect-unknown, never blind-retriedA status-check tool exists for every mutating tool
3Fingerprints normalized and counted per turnCosmetic argument edits do not reset the counter
4Terminal message names alternatives and the stop optionTraces show recovery or clean stop, not repetition
5Caps on tool calls and spend per taskA cap hit raises a report, not a silent truncation
6Error table generated from one source and linked from docsModel text and reference cannot drift

Step 5 is the one teams postpone and regret. The extreme cases are documented, including an agent that ran hundreds of reasoning steps without producing an answer (reasoning-loop experiment). A hard cap is the only defense that survives a model having a bad day.

If you are running these agents across several model providers, the wrapper layer is where the work concentrates, and keeping one client for every model type simplifies the plumbing. Quanta Inference (https://quantaintelligence.ai/inference) routes text, image, video, audio and embedding models through one OpenAI-compatible API with prepaid US-dollar pricing, which keeps the cost side of your caps easy to reason about. If you would rather hand the design to someone who has shipped these loops before, an expert engagement (https://quantaintelligence.ai/skills) puts a specialist on the tool boundary and its failure paths.

Once the caps and messages are in place, the remaining work is evidence: traces that show identical calls blocked, recovery calls succeeding, and cap hits reported. That evidence is what turns an agent that sometimes spins into one you can leave running.

FAQ

FAQ

Why not just tell the model in the system prompt not to repeat calls?

You should, and it helps, but it is not sufficient. Prompts are soft guidance and models under pressure to complete a task will retry despite instructions, so the enforcement has to sit in code you control. The prompt sets intent, the fingerprint counter enforces it, and the terminal message tells the model what to do instead.

Should the error include the full stack trace for debugging?

Not to the model. The trace is useful to your engineers, so log it on your side and give the model a short code plus a reason. A trace is long, full of frames the model cannot act on, and re-sent on every turn, so it raises cost and degrades the signal.

How do I handle a timeout where the action may have already happened?

Treat it as effect-unknown and never blind-retry. Give the model a verification tool it can call to check whether the state changed, and name that tool in the error text as the sole next action. Verification before retry is the pattern that measurably reduces duplicate actions under non-atomic failures (verified tool calls).

Does a better error message replace the retry cap?

No. Better messages reduce the retry rate, and the cap bounds the worst case. One is an instruction to the model, the other is a guarantee from your orchestration layer. You want both, because the cap is what holds when the model ignores the instruction.

What should the model do when the cap is reached and no alternative works?

It should stop and report the failure with the error code and the rejected arguments, and the run should surface that report to a human rather than retrying. A clean stop is a correct outcome. A fourth identical call is not.

Sources

Sources

  1. Function Calling in AI Agents | Prompt Engineering Guide
  2. Error-Message Guidelines
  3. Errors Best Practices in REST API Design - Speakeasy
  4. How to Prevent AI Agent Reasoning Loops from Wasting Tokens
  5. API documentation best practices guide Feb 2026 - Fern: Docs
  6. Best Practices for API Error Handling
  7. API Documentation: How to Write, Examples & Best Practices | Postman
  8. Verified Tool Calls Improve LLM Agent Reliability Under Non-Atomic ...
  9. Building Retries in Agents: How to Build AI Agents That Survive Failures
  10. API Documentation Best Practices Guide - Stoplight.io
  11. Docs as Code - Write the Docs
About the author

About the author

Antonio Evans
Antonio Evans

Antonio Evans is the founder of Quanta Intelligence and a fractional CTO who builds HIPAA-ready AI systems for health and life-science companies. Before that he was technical co-founder of FriendsEAT for twenty years, CEO of Socialdraft, and a data engineer at IBM. Antonio lives with his dog Santino in Lisbon, Portugal.

All articles