Agents routinely read a non-success response as success.A comprehensive study of bugs in LLM agents finds tool errors, warnings, and non-2xx codes parsed as completion, so the next step runs on a failure (When Agents Fail, arXiv:2601.15232, 2025). A 200 with an error nested inside is the trap your agent walks straight into.

Key facts.
- Agents append the tool observation and keep reasoning, so a non-success status or an error nested in a 200 body is read as ordinary data unless something parses it; PALADIN catalogs API exceptions, malformed outputs, and partial execution among the failure modes (PALADIN, arXiv:2509.25238, 2026).
- A comprehensive study of LLM-agent bugs finds error responses and warnings misclassified as success and propagating silently downstream (When Agents Fail, arXiv:2601.15232, 2025).
- HTTP status classes have defined meanings, a 4xx is a client error to fix and a 5xx a server fault that may be retried, so the response class should decide the next move (RFC 9110, HTTP Semantics), and a non-idempotent call must never be blind-retried.
- With no error taxonomy a 200 soft-decline reads as success, and a stronger model collapses it the same way. (arXiv:2601.15232)
Why status-code handling is where agents fail
The agent treats the whole response as text to reason over, not a typed result with a status to branch on. In a normal loop the tool output gets concatenated into the conversation and the model continues. If nothing parses the status, a 4xx, a 5xx, a warning, or a declined state buried in a 200 body is just more prose, and the model, trained on the happy path, reads it as success. The worst case is the plausible one: a 200 OK wrapping a nested declined or error state. The agent confidently treats it as done and then confirms an order, updates inventory, or emails a customer on a transaction that never cleared.
What an error taxonomy gives you
A defined move for every failure class instead of a blind reaction. A 4xx: the request was wrong, retrying unchanged is pointless, fix the args or stop. A 5xx: usually transient, a bounded retry with backoff is right, but only if the call is idempotent. A soft decline or warning in a success body: don't proceed, escalate. Mapping each class to retry, fix, escalate, or abort turns the response into a decision. It stops the agent from either ignoring failures or hammering a doomed call.
# Status class -> defined action, not a blind retry def handle(resp): if resp.status >= 500: return RETRY_WITH_BACKOFF if idempotent else ESCALATE if resp.status >= 400: return FIX_ARGS_OR_ABORT # retrying unchanged won't help if resp.body.get("state") in ("declined","error"): return ESCALATE # soft fail in a 200 return PROCEED
Building it into the agent
Validate the full response, status code and body, before the result reaches the model. Attach a typed outcome the runtime can branch on. Define the taxonomy once: 4xx fix-or-abort, 5xx bounded idempotent retry, soft-fail escalate, success proceed. Apply it at every tool boundary. Don't auto-retry a mutating call without an idempotency key. Log the full raw response so failures are diagnosable. Fault-inject these conditions in testing so the happy-path bias shows up before production. Then the agent backs off on a 5xx, fixes or stops on a 4xx, and escalates on a soft decline, each on purpose, not by accident.

Status class to action
| Response | What it means | Correct action |
|---|---|---|
| 2xx, clean body | Real success | Proceed |
| 200 with error/declined body | Soft failure | Escalate, do not proceed |
| 4xx | Request was wrong | Fix arguments or abort, do not blind-retry |
| 5xx | Transient server fault | Bounded retry with backoff, if idempotent |
| Warning header / partial | Degraded result | Inspect, validate completeness |
The pattern is that a tool response is a typed outcome, and an agent that reads it as prose either trusts a failure or retries one blindly. Give every status class a defined action, validate the body as well as the code, and gate retries on idempotency, and the agent answers each failure correctly instead of guessing. That disciplined handling of the tool boundary, every outcome mapped to a move, is reliability at the pattern level, which is what VibeModel builds as the Pattern Intelligence Layer.
Frequently asked questions
Why is a 200 with an error body the worst case?
Because the status line says success and the agent believes it, so the failure in the body slips through and the next step runs on a transaction that never completed. Validate the body, not just the code.
when's it safe to auto-retry?
On transient 5xx faults, and only when the call is idempotent so a retry can't duplicate the action. Never auto-retry a 4xx unchanged, and never auto-retry a mutating call without an idempotency key.
Where should the taxonomy live?
At the tool boundary, in the runtime, before the result reaches the model. The model should receive a typed outcome to branch on, not a raw response to interpret.

