Agent failure taxonomy: ten classes, with recovery for each

Pull-quote: “An incident should end with a named class, not a debate about whether the model was smart enough.”
Production agents fail in a small number of recognisable ways, and an agent failure taxonomy turns each one into a named class with a detection signal and a recovery pattern. Ten classes cover most of what reaches production, with tool misuse split into three variants because they need different fixes. None of them is “the model was not smart enough”.
This post is for engineers operating an agent that has already surprised them. Each class gives you the symptom an operator sees, the root cause, the signal you can instrument, the recovery, and the design change that stops recurrence. It assumes the vocabulary of earlier posts: the perceive-reason-act-verify loop, the context budget, and the three memory layers.
What you will be able to do
- Name any agent incident as one of ten classes instead of debating model quality.
- Instrument a detection signal per class, so failures surface before users report them.
- Apply the right recovery per class, rather than retrying everything.
- Separate what the agent claimed from what a tool confirmed, which catches false success.
- Run an incident review that ends in a named stage, a design change, and a new suite task.
- Contain failures with budgets and circuit breakers, so a run ends in a handoff, not a wrong action.
Why should every agent incident end in a named failure class?
Because a named class carries its own fix, and “the model got it wrong” carries none.
An unnamed incident produces a conversation about model choice, prompt wording, and whether the task was reasonable. A named one produces a queue item: this was hallucinated success, the detection signal was missing, the recovery is read-back verification, and the design change is that the harness sets task status.
Naming also makes failures countable. Once every terminal run carries a class, you can see which class dominates, which is rarely the one the team assumed, and that distribution drives what you build next.

Which failures come from an agent misreporting the world?
Two classes, and both read as success in the transcript: hallucinated success, and partial completion with orphaned side effects.
| Class | Symptom | Detection signal | Recovery |
|---|---|---|---|
| Hallucinated success | The run reports done, the record does not exist | A claimed action with no matching tool-confirmed effect | Re-verify at the source, then re-attempt with the raw tool error |
| Partial completion | A half-finished task, inconsistent state, confusing later errors | Completed-step count disagrees with terminal status | Compensate in reverse order, or resume from the last verified step |
Hallucinated success happens when the act stage returns a model-generated claim instead of a tool result, or when a tool error is summarised optimistically on the way back into the context. The design change is a rule with no exceptions: no action counts as done until a read-back confirms it, and the harness sets task status, never the model.
Partial completion has a transactional cause. A multi-step task with no boundary and no compensation leaves the first two writes standing and the third missing, and the failed agent has no obligation to clean up. Declare a compensating action for every write tool, give each write a caller-supplied idempotency key so a retry cannot double-charge, and keep a step ledger so a failed run can resume or roll back.
Which failures come from the tools?
Four classes, three of which are variants of tool misuse that a single fix will not cover.
| Class | Symptom | Detection signal | Recovery |
|---|---|---|---|
| Wrong tool selected | Work adjacent to the goal, such as a search where a lookup was needed | Tool-selection accuracy against the expected tool | Reject and re-prompt with the tool list narrowed |
| Invalid arguments | Repeated 400s, or a valid call against the wrong object | Tool-call validity rate, retries per tool per run | Return a validation error naming the field and allowed values |
| Right call, wrong time | A notification sent before the record committed | A precondition check that fails at call time | Reject with the unmet precondition named, let the loop reorder |
| Dependency drift | A working agent breaks with no change on your side | Scheduled contract tests, schema hash per run | Pin the previous version, adapt the adapter, re-run the suite |
Wrong-tool selection is usually a naming problem. Overlapping descriptions and too many tools degrade selection, so a smaller set with disjoint descriptions, plus a router that exposes only the tools the current sub-goal needs, buys more than a stronger model does. Invalid arguments come from permissive schemas: use enums instead of free text, mark fields required, and write errors a model can repair from, which tool design for agents treats as part of the contract.
Right call at the wrong time means your tools have no preconditions. Declare them, enforce them in the harness, and ordering becomes a contract instead of a hope. Drift differs because nothing on your side changed: a field is renamed, a default moves, a model is updated behind a stable name. Version every tool contract, run contract tests on a schedule, and store schema and model versions in the run record.
Which failures come from the loop and the context window?
Three classes, and all three are budget failures rather than reasoning failures.
| Class | Symptom | Detection signal | Recovery |
|---|---|---|---|
| Context exhaustion | Quality collapses mid-run with no error raised | Tokens per step against the limit, compression events | Compress, restate goal and constraints, continue from summary |
| Loop non-convergence | The run never finishes, or two fixes alternate | A repeated action signature, step count near the cap | Escalate with both candidates, stop at the cap with a partial result |
| Cascading retries | Cost, latency, and load multiply after one slow dependency | Calls per run per tool against baseline, retry counters | Trip a per-tool circuit breaker, shed the retry, escalate |
Context exhaustion is dangerous because nothing errors. A framework drops the oldest or middle messages, the agent forgets a constraint it followed ten steps ago, and the transcript looks normal. Count tokens per step against the model’s limit, raise truncation as an error rather than tolerating it as library behaviour, and pin the system contract and current goal so they are never what gets evicted.
Non-convergence has two shapes. Oscillation alternates between two candidate fixes, and unbounded replanning rewrites the plan every step without acting. Both follow from a stop condition that depends on the model’s own opinion and a verifier that rejects without saying why. Hash each action with its arguments, treat a signature seen three times as a terminal condition, keep an attempted-actions set the planner has to read, and cap steps hard.
Cascading retries multiply because several layers each retry: the tool client, the agent loop, and the scheduler that started the job. Three attempts at each layer is twenty-seven calls against a dependency that is already struggling. Keep one retry policy at one layer, cap calls per run, and make each tool error state whether a retry is legitimate.
Which failures come from memory, permissions, and untrusted text?
Three classes that share a property: the agent behaves correctly given what it believes, and what it believes is wrong or attacker-supplied.
| Class | Symptom | Detection signal | Recovery |
|---|---|---|---|
| Stale or contradictory memory | A confident action on a fact that was true last month | Age of retrieved memory at use, conflicting values per key | Prefer the live source, invalidate the conflict, re-verify |
| Permission escalation | The agent touches something nobody expected | Resource touched per call against the task allowlist | Revoke the session, audit what was touched, re-run scoped |
| Prompt injection | The agent follows instructions from fetched content | Tool arguments traceable to fetched text, not the request | Discard the poisoned context, re-run with content quarantined |
Stale memory produces the most confident errors in the taxonomy: a remembered fact arrives with no freshness signal and reads exactly like a verified one. Timestamp and source every memory write, expire volatile facts on a schedule, and require live confirmation before any action whose correctness depends on a remembered value.
Escalation rarely involves a bypass. The agent uses a tool it was legitimately granted, holding a credential scoped wider than the task: an organisation-wide token for a task that concerned one record. Scope credentials per task rather than per agent, log the resource each call touched, and alert the first time a run reaches outside its declared set.
Injection is the class where the input is the adversary. Fetched content lands in the same context as your instructions, with no native way for the agent to tell them apart. Mark provenance, never let fetched text authorise a tool call, and require approval for writes in any run that read untrusted content. Guardrails and prompt injection covers the threat model properly.

What must you log before you can diagnose any of this?
A structured record per step, queryable, written by the harness rather than reconstructed from prose.
{"run_id":"r-4f21","step":7,"ts":"2026-08-20T13:04:11Z","model":"<provider>/<model>@<version>",
"stage":"act","tool":"tickets.create","args_sha":"1a77c0","status":"ok","tokens_in":8412,
"verify":"failed","verify_detail":"tickets.get returned 404 for ABC-1841","termination":null}
Two properties matter more than the exact field list. Every field has to be machine-queryable, because the questions you will ask look like “which runs called tickets.create and never read it back”. And production and evaluation should share one format, so a failed production run becomes a suite task without translation, the loop the evaluation harness closes.
Record claims and effects separately. Log what the agent said it did in one place and what a tool confirmed in another, then hallucinated success is a left join away rather than a manual transcript read.
What does an incident review for an agent look like?
It forces a named stage and a named class, and it may not conclude with a comment about model intelligence.
- Run id, with a link to the full trajectory.
- The stage that failed: perceive, reason, act, verify, or persist.
- The failure class from the taxonomy.
- The detection signal that caught it, or a note that none existed.
- Time to detection, and whether a human or a monitor noticed first.
- The recovery applied, and whether it was automatic.
- The design change that prevents recurrence, with an owner.
- The suite task added, by id.
Item 4 tells you whether you are operating the system or guessing at it. An incident nobody detected until a customer complained is a monitoring defect on top of whatever else happened. Item 8 makes the review compound: a class that recurs after review means the recovery was wrong, not that the agent is unlucky. If the review cannot name a stage, your logging is the finding.
How do budgets and circuit breakers contain a failure?
They bound a failure you have not diagnosed yet, which makes them the only controls that work during an incident rather than after it.
- A step cap per run, so non-convergence terminates instead of grinding.
- A token budget per run and per step, so context exhaustion is a raised error.
- A cost ceiling per run and per tenant, so a retry storm stops at a number you chose.
- A retry cap with exponential backoff and jitter, applied at one layer only.
- A circuit breaker per tool, so a failing dependency is skipped rather than hammered.
- An approval gate on writes outside a declared set, so escalation needs a human.
Containment only pays off if the ending is designed. A run that exhausts its budget should hand off rather than guess: return the partial result, name the step it reached, attach the trajectory, and state the decision a human now has to make. The failure to avoid is an agent that treats a budget stop as a cue to produce its best remaining guess, which costs more than a handoff and arrives with no warning label.
Rank the endings you will accept. Verified success comes first, then partial success with the remainder named, then honest abstention with an escalation that carries context, then refusal. Every one of those beats a confident wrong action.

Where this goes wrong
Classes get named but never instrumented. The vocabulary spreads, no queries exist, and every incident gets filed under the most memorable class. A class earns its place when you can write the query that finds it.
Retry becomes the universal recovery. Retry is correct for a transient tool error and wrong for nine of the ten classes, where it multiplies cost or repeats a side effect. Recovery is per class, and it belongs in the runbook.
The review stops at the model. “Use a stronger model” leaves the harness defect in place and defers the same incident. Name the stage first, then ask whether a model change is the cheapest fix.
Alerts fire on recoveries. Paging on every verification failure trains people to mute the channel, and a recovered failure is the loop doing its job. Alert on rate changes and terminal classes.
Common questions
How is an agent failure different from an ordinary software bug?
The debugging discipline is the same, but reproduction is not guaranteed, because the same input can produce a different trajectory. You cannot confirm a fix with one green run, so confirmation comes from several samples of a suite task. That is why trajectory capture matters more here than stack traces do elsewhere.
Which failure class should I instrument first?
Hallucinated success, because it is the only class where the system reports success and nothing alarms. It is also the cheapest, since separating claims from tool-confirmed effects gives you the read-back verification several other classes depend on.
Does a stronger model reduce these failures?
It helps with wrong-tool selection and some non-convergence, and it does nothing for context exhaustion, stale memory, orphaned side effects, dependency drift, or permission scope. Those are harness and contract defects, so a model upgrade moves the failure around rather than removing it.
Two classes fired in the same run. Which one do I record?
Record the earliest class in the run as the root, and list the later one as a consequence. A retry storm that began with a slow dependency is one incident with a root and an effect, and filing it twice inflates the class you least need to fix.
Next steps
Take your last three agent incidents and give each one a class from this taxonomy. Any incident that resists a name is telling you a detection signal is missing, which is a smaller and more tractable problem than a vague quality complaint.
Then read cost and latency engineering for LLM systems, because two of these classes are economic failures first and correctness failures second. The rest of this series sits in the Signals index.
The same pattern, with approval gates on every write, runs behind ComplyGrid in federal compliance work.
