← All packs

Designing tool error messages agents can actually recover from — the runtime half of tool design

A tool can be described perfectly — clear name, precise schema, honest preconditions — and still fail at runtime, because runtime is where reality intrudes: a file doesn't exist, a rate limit trips, an argument that looked valid violates a constraint the schema couldn't express. What happens next depends entirely on what the failure looks like. An agent that gets back Error: 500 or a raw stack trace has to guess what went wrong and what to try instead — and it usually guesses by retrying the identical call, sometimes several times, before giving up or escalating. An agent that gets back what was wrong, what was expected, and what changed since the last known-good state can usually fix its own next call without spending a turn on discovery.

The retry loop is a symptom, not a separate bug

When an agent calls the same failing tool three times in a row with minor variations, the usual diagnosis is "the model isn't reasoning well about the failure." Often the real cause is upstream: the error response didn't contain enough information to reason with. A model faced with an opaque failure has two options — ask a human, or guess and retry — and retrying feels more like progress, so that's the default. The fix is rarely a smarter retry policy; it's giving the first failure enough content that a second attempt isn't a guess.

Error response that causes blind retriesError response that enables self-correction
Error: 400 Bad RequestInvalid "status": "archived" — allowed values are open, closed, pending
raw stack trace or internal exception class namethe user-facing cause, with the stack trace available only in a debug field the agent doesn't need to parse
"File not found" with no path echoed back"File not found: src/util/format.py — did you mean src/utils/format.py?"
silent no-op on a filtered or partial matchan explicit count: "0 of 2 requested items matched; the second id no longer exists"
a generic "rate limited, try again" with no timing"rate limited — earliest retry in 4s" so the agent can wait instead of spinning

State what was expected against what was actually sent

A model correcting a bad call is doing a diff in its head between what it sent and what would have worked — and it can only do that diff accurately if the error response gives it both sides. "Invalid date" makes the agent guess at the format; "Invalid date: got '09/27/2026', expected ISO 8601 (YYYY-MM-DD)" gives it the exact edit to make. This is the same instinct as pushing constraints into the schema rather than the prose (see writing tool descriptions agents actually use correctly), applied one step later — the schema failed to prevent the call, so the error response is the last chance to communicate the constraint before the agent tries again uninformed.

Distinguish what the agent can fix from what only a human can

Not every failure is recoverable by adjusting the call. A malformed argument, a stale id, a wrong enum value — the agent can fix these itself given a clear enough message. An expired credential, a permissions boundary, a service outage, a destructive action that needs explicit confirmation — these need a human, and an error message that reads like the first kind invites the agent to keep trying variations of a call that was never going to succeed. Tag or word the response so the difference is legible: a validation error should look fixable, and a hard-stop error should look like a hard stop, not a puzzle to solve by guessing harder. This is the same judgment call covered in designing when an agent should stop and ask a human, arriving here in the specific form of "does this failure message invite another attempt or ask for a person."

The test to apply to a tool's failure responses: read only the error string, with no other context, and ask whether it names what was wrong, what was expected instead, and whether trying again — with or without changes — is worth doing at all. If a caller with no memory of the original call couldn't answer all three from the message alone, the agent calling it can't either, and will spend retries finding out empirically what the message could have said directly.

A tool's happy-path output has its own correctness problem once it succeeds — see validating and repairing agent structured output for what happens when the call returns but the result itself can't be trusted as-is. A tool can also succeed cleanly and still be unhelpful, by returning far more or far less than the caller can use — see sizing tool output for agents for the case where nothing failed, but nobody decided how much should come back. That problem gets worse when the call keeps succeeding but the meaning of "success" has quietly changed upstream — see tool contract drift for the case where no single call ever produces an error message at all. And when a tool disappears on purpose rather than drifting by accident, the error a stranded caller gets is what determines whether it can recover — see deprecating a tool without breaking sessions already mid-task.