The Recovery Path for an Agent Failure Is Decided by Where It Began

When an agent fails, the reflex is to ask what to do about it. Retry, fall back, escalate, alert someone. That is the wrong first question. The right one is where the failure came from, because the answer determines which of those responses has any chance of working and which of them will only waste time and money on the way to the same result. A failure is not a single kind of event that admits a single kind of fix. It is a symptom, and the same symptom can point back to three very different causes. Treat them as interchangeable and you will spend your reliability budget retrying things that were never going to succeed and escalating things a short wait would have cleared.

The taxonomy that matters divides failures by origin into three categories: failures that came from an external tool or service, failures that came from the model’s own reasoning, and failures that came from the infrastructure underneath both. These are not academic distinctions. The right response to each is not the right response to the others, and they do not overlap. Retrying is right for one and actively harmful for another. Rewording the request fixes one and does nothing for the others. Waiting resolves one and merely delays the surfacing of a problem in the rest. The entire discipline of recovering from agent failure rests on placing each failure into the correct bucket before deciding what to do, because the bucket is what selects the response.

The origin is the classifier

The most useful thing about this taxonomy is that it is defined by a single, answerable question rather than by a catalog of symptoms. Symptoms are unreliable classifiers. The same visible failure, a step that returned nothing usable, can be produced by a service rejecting a malformed request, by the model choosing the wrong service to call, or by the network dropping the connection before the request arrived. If you sort failures by what they look like, you will keep misfiling them. Sort them by where they originated and the ambiguity mostly dissolves.

The question is: at what layer did the failure actually happen? If an external tool or API ran and reported a problem, the failure is external to the model and lives in the tool layer. If the tool ran correctly and the infrastructure was available, but the model formed a bad plan, picked the wrong tool, or misread a result it had every means to read correctly, the failure is internal to the model. If the tool’s own logic was sound and the model’s plan was sound, but the tool could not reach something it depended on, the failure is in the environment beneath the tool. Origin first, response second. That ordering is the whole method, and everything below is an elaboration of what each origin implies.

When the failure comes from a tool

A tool failure is one the external world hands back to you. The service the agent called was reached and did something, and what it did was refuse the request, hang until the clock ran out, or return a payload that does not match what the caller was promised. The model’s plan was fine and the infrastructure was reachable; the tool itself is where the trouble surfaced. This is the most common category and also the one with the most structure, because tool failures split cleanly into two subtypes whose recoveries are opposites.

Some tool failures are transient. The far side was saturated for a moment, or throttling the caller, or mid-restart. What unites these is that the blocking condition is a passing state of the world rather than a property of the request itself, so letting it pass and sending the identical message again has a real chance of going through. The service was momentarily overloaded, not permanently broken. For these, retrying is exactly right, ideally with a backoff that gives the far side room to recover before the next attempt lands.

Other tool failures are permanent. The rejection is a verdict on the request as sent: the arguments were malformed, the caller lacks the authorization it is asking to use, the target it names was never there. No amount of waiting changes any of that, because time is not the variable. Waiting does not make a rejected argument well-formed, conjure a resource that never existed, or grant an entitlement the caller does not hold, so the identical call keeps drawing the identical verdict. Retrying a permanent failure is not a harmless precaution. It burns tokens and latency while delaying the recovery that could actually help, which is a different action entirely: a corrected request, a fallback to another tool, or an escalation to someone who can resolve what the automation cannot.

So the whole tool category reduces to one test: given identical inputs, does a repeat of this exact call have any path to success? If it does, the failure is transient and a retry policy fits. If it cannot, retrying is pure waste and the recovery has to be something else. Status codes are a useful signal for this but not a verdict. A failure raised by the server tends to be the transient kind and a failure raised on the client’s behalf tends to be the permanent kind, but the exceptions are the ones that trip people up. Being throttled and running out of time both come back framed as client-side rejections, yet both describe a passing condition rather than a bad request, so both are worth retrying. Read the code as evidence about which subtype you are in, then confirm it against what the failure actually means rather than trusting the numeric band blindly. And record the outcome of every attempt, because the signal you are watching for is a call that used to recover on a second try and has stopped recovering at all, which is how a transient problem announces that it has hardened into a permanent one.

When the failure comes from the model

The second category is the one that does the most damage, because it hides behind a recovery that feels responsible and is worthless. A reasoning failure is the one case where everything outside the model behaved and the model still came up short. Whatever it called executed, the layers beneath held, and the fault lives entirely in the judgment applied to it. Maybe the plan it committed to was the wrong plan. Maybe it reached for a capability that was never going to answer the question. Maybe the result came back correct and it drew the wrong conclusion from it. In each case the model had what it needed and misused it. Nothing external broke. The model simply got it wrong.

What makes this category dangerous is that its correct recovery is the opposite of the intuitive one. When something fails, retrying is the natural first move, and for tool failures it is often right. For reasoning failures it is guaranteed to fail, and it fails silently, which is worse than failing loudly. Send the model the same prompt with the same context and it will produce the same wrong answer, because nothing that caused the mistake has changed. A model handed the same tokens in the same order has nothing new to reason from, so it arrives at the same place it did the first time. A retry here does not recover anything. It launders the same defect as a fresh attempt and consumes budget doing it.

Getting out of a reasoning failure means altering what the model sees, not resending it. The instruction may be ambiguous and need sharpening. The context may have left out something true about the world as it stands right now, and the model closed that gap on its own with an invented detail that read as reasonable. The framing may have aimed the model at the wrong subgoal. Whatever the specific cause, the fix is to alter something the model sees before it reasons again, or, when the model cannot be steered into a correct result, to escalate to a person who can. The tell for this category is precisely that retrying without modification would be pointless. If sending the exact same thing again could not possibly help, you are looking at a reasoning failure, and you should be reaching for a change to the input rather than a repeat of it.

When the failure comes from the environment

The third category sits below both the model and its tools. In an environment failure the plan was sound and the tool’s own logic was sound, but the substrate the tool runs on top of stopped answering. The disk it writes to comes back mounted read-only, the datastore behind it refuses connections, a network segment between two services simply drops. Nothing about the agent’s decision-making or the tool’s implementation was at fault. The ground they were standing on gave way.

Environment failures are frequently transient in the same way many tool failures are, and for the same reason: severed connectivity gets restored, an overloaded datastore catches up, a vanished mount is brought back. The correct recovery usually resembles the recovery for a transient tool failure, which is to wait, retry after a delay, and monitor for the condition to clear. What distinguishes the category is not the recovery, which overlaps with the transient-tool case, but the origin, and the origin matters because it changes how much confidence you should have that waiting will help. A dependency outage is often a self-resolving condition. Treating it as an escalation-worthy emergency the instant it appears generates noise and pulls humans into something that would have fixed itself before they finished reading the alert.

The line between a tool failure and an environment failure

The two categories that blur into each other are tool and environment failures, because both surface through the same tool call and can look identical from the outside. The clean separator is again the origin question, made more precise: was the error the tool’s own considered response, or did the tool never make it far enough to have one? A service that runs and returns an error is a tool failure, because the tool executed and had an opinion about the result. A service that cannot reach its own database, or a call that never completes because the network dropped, is an environment failure, because the tool never got the chance to run its logic at all. One is the tool speaking. The other is the tool’s inability to speak.

The reason this line is worth drawing carefully is that it changes your prior about self-resolution. An environment failure carries a reasonable expectation that the underlying condition is temporary and will clear on its own, which justifies patience. A tool failure carries no such expectation by default; it might be transient, but it might just as easily be permanent, and you have to run the retryability test to know. Collapsing the two loses that signal. If you file every unreachable-dependency failure as a generic tool error, you forfeit the built-in bias toward waiting that the environment category gives you for free, and you will either retry too aggressively or escalate too soon.

Classification is a routing decision, and getting it wrong cuts both ways

It is tempting to treat this taxonomy as vocabulary, a set of names to attach to failures after the fact. It is not that. It is a routing table, and the classification is the routing decision that sends a failure toward the one recovery that fits it. A tool failure enters the retryability test and leaves as either a backed-off retry or a switch to another path. A reasoning failure bypasses retry entirely and demands that something in the input change first. An environment failure is the one that most rewards patience: hold, retry on a delay, and keep watching for the dependency to come back. The name earns its keep only because it selects which of these the failure goes to.

Which is why misclassification is the real failure mode, and it is costly in both directions. Classify a permanent failure as transient and you retry something that cannot succeed, spending tokens and latency while delaying the fallback or escalation that might have worked. Classify a reasoning failure as a tool failure and you retry the model into the same wrong answer a second time, now with the audit trail of a diligent recovery attached to it. Run the errors in the other direction and the costs invert. Treat a transient environment blip as a hard failure and you escalate a condition that a short wait would have cleared, training reviewers to expect noise and eroding the value of the escalation channel for the cases that genuinely need it. Over-retrying wastes machine resources. Over-escalating wastes human ones. Both are symptoms of the same underlying mistake, which is jumping to a response before establishing an origin.

The one honest complication is that origin is not always immediately clear. A server-side error could be a transient overload or a persistent bug on the far side, and from your vantage point the two look the same at the moment they arrive. The disciplined answer is not to guess and commit, but to allow a single conservative retry with a delay before treating an ambiguous failure as permanent. One retry is cheap and catches the transient case; unbounded retries are how a genuinely permanent failure turns into a spiral. The bounded probe respects the ambiguity without surrendering to it, and it keeps a single unclear failure from becoming an expensive one.

Ask where before you ask what next

The instinct to fix a failure immediately is the thing to resist, because acting before classifying is how good recovery machinery gets pointed at the wrong problem. A retry loop is only as good as the judgment that decided a retry was the right response, and that judgment comes entirely from knowing where the failure originated. An external service reporting an error, the model reasoning its way to a wrong answer, and the infrastructure beneath them failing to hold are three different events that happen to produce similar-looking symptoms, and the only reliable way to tell them apart is to trace each one back to the layer it came from.

Build the discipline in this order and the rest follows almost mechanically. Locate the origin: tool, model, or environment. Let the origin select the recovery: retryability check and retry-or-fallback for tools, input modification for reasoning, patient retry with monitoring for the environment. Bound the ambiguous cases with a single conservative probe rather than a guess. A system built this way spends its retries where retries can work, its input changes where the model actually erred, and its escalations on the failures that genuinely exceed what automation can settle. A system that skips the classification step and reaches straight for a response will keep doing the wrong thing energetically, and energetic wrong recovery is more expensive than the failure it was meant to fix.