by Martin Monperrus Tags:

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.

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:

  1. The error message is the next prompt. Whatever you put there is what the model reasons over. A 400 Bad Request is a dead end; a sentence naming the legal keys is a working memory.
  2. 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.
  3. 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.
  4. 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:

  1. 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’s type URI — and making it dereference to the explanation gets you tier two for free.
  2. Add one retrieval verb or APIguide(refusal.<code>), --explain E0308, help(code). Advertise it in the refusal itself.
  3. 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.
  4. Return a path into the input (data.at), not just a message. The model needs to know which of its 40 keys was wrong.
  5. Echo the current state that contradicted the call. The connector’s guard_mismatch returns the block as it now stands; cell_none returns the table’s real dimensions. The agent then re-aims without a read round trip.
  6. Say what landed. Always, explicitly. Partial application without a statement of what applied is the cruelest failure mode for an autonomous caller.
  7. Cap the suggestions at one or two. More options force a guess; a guess costs a round trip and risks a wrong write.
  8. 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"])).
  9. 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:

The trade-off

A self-teaching error surface is not free.

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