A Tool’s Error Response Is an Interface the Model Acts On

When a tool succeeds, its return value is data. When a tool fails, its return value is an instruction. An autonomous system reads the failure and immediately decides what to do about it: try the same call again, change the arguments and retry, hand the problem back to a person, or abandon the path and take another. That decision is made by the model, in the moment, on the basis of whatever the tool sent back. So the shape of a tool’s error is not a diagnostic afterthought written for whoever reads the logs later. It is an interface, and the behavior it produces is the recovery logic of the whole system. Design it as loosely as you would a log line and you have left the most consequential branch in your agent’s control flow to be inferred from prose.

This is the reframe that reorganizes everything else. In a conventional program, an error propagates upward until it reaches a handler you wrote, and that handler decides deterministically what happens next. The error object can be as terse or as sprawling as you like, because the code consuming it already knows what each case means; the meaning lives in the handler, not in the error. An agent inverts that arrangement. There is no handler downstream that you authored to interpret this specific failure. The consumer is a model reading under uncertainty: it takes in the error text, forms a judgment about what went wrong, and acts on that judgment. Whatever the error leaves implicit, the model supplies by guessing, and it acts on the guess with the same confidence it would bring to a fact. The meaning has to live in the error itself, because there is nothing else that carries it.

The error’s job is to make one decision obvious

Everything a well-designed tool error does serves a single purpose: to make the model’s next move the obvious one rather than an inference it has to reach for. Reaching for an inference is where the cost lives. A model that cannot tell from the error whether a failure is worth retrying will sometimes retry a call that can never succeed, burning turns and filling its own context with repeated failures, and will sometimes surrender on a call that a single retry would have cleared. Both are failures of the error to communicate, not failures of the model to reason.

Four elements, in combination, take out the guesswork. The first is a stable code, a short machine-readable token that says which category of failure this is without anyone having to parse a sentence. The second is a human-readable message, plain enough that it can surface directly to a person if the model decides to escalate, without exposing internals or requiring a translation step. The third is the context: the specific values that identify what failed, so the model knows which part of its request was the problem rather than only that something was wrong. The fourth is an explicit statement of the recommended next action, so the model does not have to derive the recovery path from the code alone.

What makes these four worth separating is that each one answers to a different reader. The code is consumed by logic, whether that is routing logic you write or the pattern-matching the model does, so it needs to stay stable and mean exactly one thing. A person is the audience for the message, on the occasions a failure reaches one, which is why it has to be clean and self-contained. The context serves two readers at once, the engineer at debugging time and the model when it decides what to adjust, so it has to name the offending value precisely. The recommended action feeds the model’s decision process, and it needs to point at a path rather than describe a symptom. An error that collapses these into a single blob of text technically contains the information and still fails, because the model has to disentangle four audiences from one string every time, and it will not always do it correctly.

The one distinction that governs recovery is whether retrying can help

Of all the things an error can say, the most valuable is whether a second attempt could land differently from the first. That is the axis the model’s opening decision turns on, and it sorts failures cleanly into two kinds.

One kind is caused by the caller. The arguments were malformed, a required value was missing, a field was out of range, an identifier pointed at nothing. Repeating the call unchanged will fail again in exactly the same way, because nothing about the world has changed and the input was the problem. The correct response is to change something, and often the thing that has to change is a value the model cannot invent on its own, which means surfacing the problem so a person can correct it. The other kind originates in the system or something downstream of it. A request ran past its deadline, a dependency dropped out for a moment, an upstream service handed back something the tool did not expect. The caller did nothing wrong, and the same call a moment later may well succeed. Here retrying, waiting, or falling back to an alternative is exactly right, and involving a person would be a waste of their attention.

The return has to carry that split as a fact the model can read, because the model cannot reliably recover it from the message text. The cleanest way to carry it is to mark retryability directly, as a property of the error itself, so the recovery path never rides on an inference. The alternative is to let the model deduce it from a status class, leaning on the familiar rule of thumb that server-side failures skew transient and caller-side failures skew permanent. That rule holds often enough to be tempting, but it misfires on the cases that matter most: throttling and an expired deadline both arrive stamped as caller-side rejections, and both clear on a second attempt after a short wait. An error scheme that makes the model reason from the status class alone will handle the ordinary failures and stumble on exactly the ones dressed as the wrong category. State retryability as its own signal and the question never reaches the model as a puzzle.

The recommended action is where you hand the model the recovery path

Naming the failure tells the model what happened. Naming the recommended action tells it what to do, and those are not the same thing even when the second seems to follow from the first. For common, well-understood failures the model can usually derive the right response from the category alone. For anything domain-specific, the derivation is a guess, and the value of stating the action outright is that it converts a guess into an instruction.

The action a tool recommends resolves toward one of two destinations. Sometimes the model can absorb the failure without leaving its own loop: pause and retry, adjust an argument and call again, reach for a different tool that meets the same need. Sometimes nothing the model does on its own will clear it, because the failure turns on judgment or authority it does not hold. Which destination applies is exactly what the recommendation should name. Throttling points to a pause and a second attempt. Input the model cannot repair on its own points to a correction it has to source from outside its own loop. A hard dependency that has gone dark points to a substitute rather than to another round against a service that will not answer. Supplying this hint is nearly free on the tool side and lifts a real decision off the model, and the payoff is largest exactly where the model would otherwise be least sure, on the failures specific to your domain that no general prior covers.

Name the destination, though, and not the disposition. The line falls where the tool’s knowledge does. A tool knows its own failure and whether anything it can see would clear it, and that is a fact it owns and should state plainly: this is permanent, no argument change will help, nothing local remains to try. It does not know what the caller is trying to accomplish, what the run has already spent, or what stopping would cost, and those are what decide whether a person should be pulled in. A tool that recommends escalation has jumped from the fact it holds to a judgment about work it cannot see, and the recommendation is then only as good as its guess about a context it was never given. Report the finality. Let whoever owns the plan decide what it warrants.

There is a discipline that follows from treating the action as part of the interface. Because the model will build its recovery behavior on the action you recommend, that recommendation has to be trustworthy across every context the tool is called from. An action that tells the model to retry a failure that is in fact permanent teaches it to loop. One that reports a transient blip as final trains it to abandon work that would have resolved untouched. The recommended action is a commitment about what will actually help, and it holds its value only as long as it stays true across every caller.

The most expensive error is the one that reports success

The failure modes in tool error design are not exotic. They are a small set of recurring shortcuts, and the most damaging of them is the one that looks least like a bug: returning a success status with the actual failure tucked somewhere inside the response body. A model reading a success signal has no reason to go spelunking for a buried error, so it proceeds as though the call worked, carrying a failure forward as if it were a result. Everything downstream inherits a corruption that never announced itself. This is worse than any loud failure, because a loud failure at least stops the line. A false success lets the line keep moving with bad cargo, and when the corruption finally surfaces it is nowhere near the tool that let it through. The rule that prevents it is unambiguous: a failure is reported as a failure, in the status, where the model looks first, and never smuggled into a field it may not read.

The other recurring shortcuts are milder but they compound. A code generic enough to fit every failure, one that resolves to nothing more specific than the fact that something broke, pushes the model back into reading the prose to recover the very distinction the code was supposed to carry, which reintroduces the guesswork the structure existed to remove. Omitting the context leaves the model knowing that something in its request was wrong without knowing which part, so it cannot target a fix and has to either retry blindly or escalate a problem it might have corrected itself. Each of these adds friction to recovery, and friction in recovery is measured in extra tool calls, extra latency, and a higher chance that the model’s next move is the wrong one.

Codes are an interface other things depend on, so treat them like one

A stable error code is an API, and the fact that its consumers include a model rather than only your own code does not make it less of one. Once any logic keys off a code, whether that is a routing rule you wrote or a recovery habit the model has formed, changing the code silently breaks that logic in the way any breaking change to an interface does. The message can be rewritten freely between releases, because it is prose meant for a human and nothing branches on its exact wording. The code cannot, because things branch on it. Keeping the two separate is what lets you improve the human-facing explanation without disturbing the machine-facing contract, and conflating them, letting the message be the only signal, means every wording change is a potential regression in behavior you never intended to touch.

This is also why the code has to carry meaning on its own rather than deferring to the message. The point of a stable, categorized code is that a decision can be made from it without reading a sentence. When the code is generic and the real information lives only in the prose, the model is forced to read and interpret natural language on the hot path of every failure, which is both slower and less reliable than switching on a token that means exactly one thing. Put the distinction that matters into the code, and the message becomes what it should be: an explanation for the person who might eventually see it, not a channel the recovery logic has to decode.

An error is a specification for behavior under failure

The habit worth building is to design a tool’s errors with the same seriousness as its success path, because in an agentic system they carry equal weight. The success return determines what the tool contributes when things go right. The error return determines how the whole system behaves when they go wrong, and it carries that weight no matter how rarely it fires. A stable code that says which category of failure this is, a clean message a person can read, the specific context that names what failed, and an explicit recommendation for what to do next: these are not fields on a form to be filled out for completeness. Together they are a specification for how an autonomous system recovers, written in the only place the model will reliably look.

The economy of getting this right is the same economy that governs every part of tool design meant to be read by a model. A precise error prevents a wrong recovery the way a precise schema prevents a malformed call, by removing the ambiguity a model would otherwise close by guessing. The alternative is not that the system stops when it hits a bad error. The alternative is that it keeps going, confidently, in the wrong direction, and you find out later and further away. An error designed as an interface is the difference between a failure that the system absorbs and a failure that it silently propagates, and that difference is decided entirely by what the tool chose to send back.