---
title: "Agent errors"
canonical: https://guavaindex.com/specs/agent-errors
modified: 2026-11-10T08:36:22Z
lang: en
---

# Agent errors

Status: draft 0.1. Covers friction #26 (errors come back as red text on a page). Reference implementation: `agent_index/errors.py`, used by the site middleware, the agent client and the fetcher.

Every failure an agent can hit comes back as JSON with a stable code, the field at fault when there is one, and what to do next:

```json
{"error": {"code": "out_of_stock", "message": "Sold out", "field": "product_id", "retry_after": null}}
```

| Code | Agent should |
|---|---|
| `invalid_input` | Fix the named field and retry. |
| `missing_field` | Supply the named field and retry. |
| `not_found` | Stop; the thing doesn't exist. |
| `not_allowed` | Stop; the site's [agent policy](/specs/agent-policy) doesn't allow this use here. |
| `conflict` | Re-read the current state, then retry. |
| `out_of_stock` | Stop or choose another item. |
| `auth_required` | Get a grant for this site. |
| `scope_missing` | Ask the person for a grant with the named scope. |
| `payment_required` | Pay the advertised price (see [payments](/specs/agent-payments)). |
| `payment_declined` | Ask the person. |
| `rate_limited` | Wait `retry_after` seconds. |
| `temporarily_unavailable` | Wait and retry. |
| `needs_human` | Hand the step to the person. Used for anything only a human can do, so agents stop retrying it. |
| `internal` | Retry once. |

Sites may add their own codes; agents treat unknown codes as `internal`. Identity and payment checks add their own codes (see those specs), with the same shape.

Use the HTTP status that matches (400, 401, 402, 403, 404, 409, 429, 503) so tools that only read the status still behave sensibly.

## The `retry` field

Every error carries `retry`, one word an agent can switch on: `fix_input`, `no`, `refetch`, `get_grant`, `pay`, `ask_human`, `after_delay` or `once`. Codes from the identity, payment and negotiation specs have the same shape and their own `retry` values (`errors.CODES` lists them all). A site's own codes are passed through unchanged and advised as `once`.

`rate_limited` and `temporarily_unavailable` responses also send a `Retry-After` header. The agent client waits it out once when it is 30 seconds or less, and otherwise hands the error back with `retry_after` so the agent can do something else meanwhile.

## Sites that don't speak this yet

`errors.from_response(status, headers, body)` turns any response into this shape. Structured JSON is read as is. Otherwise the code comes from the status (404 `not_found`, 429 `rate_limited`, 503 `temporarily_unavailable`...), refined by the page text where that is safe: a CAPTCHA page becomes `needs_human`, "sold out" becomes `out_of_stock`. A server failure stays `internal` whatever its page says. The message is the page title, never a stack trace.

`AgentFriendly` applies the same conversion to the wrapped app's own error pages whenever the caller is an agent (signed, or asking for JSON or Markdown), so a site gets structured errors without touching its error handling. `fetch` returns `code`, `retry` and `advice` with every failure, and when it serves an older copy because the site failed, `stale_because` says why.
