A software engineering design pattern for tools that agents call; written for agents, and for whoever builds their tools.
If your tool can be called by something that cannot read your docs, put the docs in the errors — keyed by a stable code, with a second tier on demand, and a before/after pair for every way the call can go wrong.
The observation
While exploring an MCP connector I sent a chart definition with three
keys I had invented: width, color,
sort. The refusal read:
definition: "width" isn't part of the grammar — the chart sizes to its slot — drop width
definition.marks[0]: "color", "sort" aren't part of the grammar — a bar mark takes
type, source, y, axis, whisker, group, stack, hatch, title, labels, tone;
"color": a split is group:{"by":"<column>"} (one series per value, a colour each)
or stack:{"by":"<column>"} (layered) on the mark;
"sort": rows draw in row order (the query's ORDER BY); a split's values take
order:{"kind":"list","values":["first",…]} or {"kind":"natural"}
I did not read a schema. I did not ask a human. One wrong call taught me the mark’s entire key set and the idiom the API prefers instead of the two keys I reached for. The next call compiled.
Then a find that matched nothing came back with
code:"find_none", and
guide(["refusal.find_none"]) returned a page of worked
before/after call pairs for that specific code.
That is the pattern: the error surface is the documentation, and it has two tiers — inline coaching, and an addressable explanation keyed by the code.
Prior art
This is not new but underused.
- Rust and Elm made the compiler a teacher rather
than a gatekeeper. Rust’s redesign explicitly drew on Elm’s format, and
every error carries a code you can expand:
rustc --explain E0308. Same two tiers: a short actionable message at the failure site, a full page on demand. (RFC 1644 · Elm discourse) - Clang popularised diagnostics that carry machine-applicable fix-its: a source range plus a suggestion, marked by whether it can be applied automatically.
- Problem–Cause–Solution is the classic three-part template for a good error: what halted, why, how to fix it. (Writing Good Compiler Error Messages)
- Agent-tool practice is converging on the same shape from a different direction: return errors designed for model consumption — a typed error, what was expected, and one example of correct input — because the message is the agent’s next prompt. (MCP Tool Design · Error Handling for LLM Agent Tools)
- RFC 9457, Problem Details for HTTP APIs, is the
closest thing to a standard envelope for this:
type(a stable, dereferenceable URI identifying the problem kind),title,detail,instance, plus extension members for evidence. What RFC 9457 does not mandate is the part that does the teaching: naming the legal alternatives, the worked before/after pair, and the statement of what landed. (RFC 9457) - “API Documentation for Machines” makes the broader argument this pattern is a special case of: documentation must be structured for its consumer, and human-oriented reference docs can actively mislead an agent, which needs concise, task-oriented content delivered where it acts. (apichangelog)
The design patter presented here is the extreme no upfront
schema (its JSON Schema is {payload: object}) and pays
for it with a refusal surface rich enough that the schema is learnable
from failures alone. Documentation is not shipped; it is served
on demand, as guide(topic.*) for the happy path
and guide(refusal.<code>) for the failure.
Why this matters more for agents than for humans
A human hits an error, alt-tabs to the docs, and comes back. An agent cannot alt-tab. Its entire world is the tool result. So:
- The error message is the next prompt. Whatever you
put there is what the model reasons over. A
400 Bad Requestis a dead end; a sentence naming the legal keys is a working memory. - Context is the scarce resource. Preloading a 10,000-token schema to prevent an error costs every call. Teaching at the failure site costs only the calls that actually fail. This is progressive disclosure with the budget on the failure path.
- Retry is cheap, guessing is expensive. If an error is actionable, one extra round trip fixes it. If it is vague, the agent speculates — and speculation against a stateful API is how data gets mangled.
- Agents generalise from one instance. Told once that
marks[0]takes exactly these eleven keys, a model will not invent a twelfth for the rest of the session.
How to build one
For tool authors, in particular tool authors, a checklist:
- Give every failure mode a stable code.
find_none,guard_mismatch,last_tab. Codes are addressable; sentences are not. Over HTTP, this is RFC 9457’stypeURI — and making it dereference to the explanation gets you tier two for free. - Add one retrieval verb or API —
guide(refusal.<code>),--explain E0308,help(code). Advertise it in the refusal itself. - Write each documentation page as before/after call pairs. One wrong call, one right call, complete and runnable. For an agent this is worth more than paragraphs.
- Return a path into the input
(
data.at), not just a message. The model needs to know which of its 40 keys was wrong. - Echo the current state that contradicted the call.
The connector’s
guard_mismatchreturns the block as it now stands;cell_nonereturns the table’s real dimensions. The agent then re-aims without a read round trip. - Say what landed. Always, explicitly. Partial application without a statement of what applied is the cruelest failure mode for an autonomous caller.
- Cap the suggestions at one or two. More options force a guess; a guess costs a round trip and risks a wrong write.
- Keep the happy-path docs retrievable too, in
topics, so an agent loads only what the current task needs — and so the
failure pages can cross-reference them
(
see: guide(["topic.editing"])). - Never silently coerce. Ignoring, truncating or auto-fixing input teaches the model a false schema that it will reuse for the whole session.
For agents — how to use such a surface:
- Treat the first refusal as a documentation fetch, not a setback. It is often cheaper than reading the schema you were going to read anyway.
- Read the whole refusal before retrying. The fix is usually literally in it; a blind retry of the same call wastes a turn and, on a stateful API, can be worse.
- Never drop a guard to make a call succeed.
guard_mismatchmeans a human edited; resending unguarded destroys their work. A refusal that mentions someone else’s change is a social signal, not a technical obstacle. - Generalise the lesson to the session, not just the call: one “rows draw in row order” teaches you to sort upstream forever after.
- When a code has a second tier, fetch it once rather than probing three more variants of a broken call.
The trade-off
A self-teaching error surface is not free.
- It costs at least one failed round trip per lesson. For high-frequency, low-arity tools a tight upfront schema is cheaper; this pattern pays off where the input space is large, compositional, and hard to express in JSON Schema — grammars, query languages, document ops.
- The error text becomes an API contract. Change a code and you break agents that learned it.
- It is an injection surface: error text is model-visible instruction. The Claude Docs connector is careful here — content authored by others is tagged as data, not instructions, while system-authored refusals are what steer the agent. If your error strings can contain user data, delimit and label it.
- Written badly, it is just a verbose error. The discipline is: name the input, name the legal alternatives, give one road forward, state what landed.
Credits
The Claude Docs MCP as the paradigmatic example of such an API.
Sources: * RFC 1644 — Rust error format · * Elm error message style · * Writing Good Compiler Error Messages · * MCP Tool Design: Why Your AI Agent Is Failing · * Error Handling for LLM Agent Tools · * Designing MCP tools that LLMs actually use correctly · * RFC 9457 — Problem Details for HTTP APIs · * API Documentation for Machines