Module 2 · Tools, Knowledge, Memory & Research Design · scripted

Errors That Teach

30 minoutcomes: tool-design, agent-architectures

Who were error messages written for?

Every tool call can fail. And computing has fifty years of tradition about what to say when it does:

The classic error, and its intended reader

ERROR 422: INVALID_PARAMETER E_PRECONDITION_FAILED status=403 message="Forbidden"

Written for a human — reading a log file, hours or days later, reconstructing what went wrong. A code to look up. A string to search for. The receiver of the error at the moment it happened — the program — had no intelligence. It could not read, could not adapt, could not recover. So no one wrote errors for it.

The classic error is a coroner's report.

That design was correct for its world. The program that hit the error could only crash or retry blindly; the only reader who could act on an explanation arrived after the fact. Terse codes for the machine, searchable strings for the post-mortem human — that is what errors were for.

The receiver changed

An error returning into an agent's conversation lands somewhere no error has landed before: in front of an intelligent reader, mid-task, holding the full context of what it was trying to do — and occupying the user seat, where words steer. This reader can recover. But only if the error tells it how. Watch the same failure, written for each reader:

Example 1 · The coroner's report, delivered to the patient
user

You have the following tools available for use:


  1. book_room( room, date, start, end )

Task: Book the department seminar room for Thursday 14:00–16:00.

assistant
⟨tool_use⟩
book_room(room = "seminar", date = "2026-09-17", start = "14:00",
          end = "16:00")
user
⟨tool_result⟩
ERROR 409: CONFLICT
assistant
⟨tool_use⟩
book_room(room = "seminar", date = "2026-09-17", start = "14:00",
          end = "16:00")
user
⟨tool_result⟩
ERROR 409: CONFLICT
assistant

The booking system is returning an error I can't interpret. The room may be taken, or the request may be malformed — I can't tell which. I'll report back that the booking failed.

NoteThe agent has intelligence to spare and nothing to apply it to. A code with no WHAT, no WHY, no next move — so it guessed, retried the identical call, and gave up. The failure isn't the agent's. The error was addressed to someone else.
Example 2 · The same failure, written for the reader it has
assistant
⟨tool_use⟩
book_room(room = "seminar", date = "2026-09-17", start = "14:00",
          end = "16:00")
user
⟨tool_result⟩
CONFLICT: seminar room is taken 13:00–15:00 on 2026-09-17
(research group meeting). Free 15:00 onward. Room "aud-2" (cap. 30)
is free 14:00–16:00. To book it:
book_room(room="aud-2", date="2026-09-17", start="14:00",
end="16:00").
assistant
⟨tool_use⟩
book_room(room = "aud-2", date = "2026-09-17", start = "14:00",
          end = "16:00")
user
⟨tool_result⟩
Booked: aud-2, 2026-09-17 14:00–16:00.
assistant

The seminar room was taken until 15:00, so I've booked auditorium 2 — same time, seats 30. If you'd rather keep the seminar room, it's free from 15:00.

NoteIdentical failure, identical agent. The only change is what came back — and the trajectory turned from two blind retries and a shrug into a recovery and an informed alternative offered to the user. The error did the teaching; the agent did the reasoning.

The recovery contract

What did the second error contain that the first didn't? Three things — and they are the whole design:

The recovery contract

Every error is a turn in the conversation. Make it say:

WHAT   happened — precisely, in the terms the agent is working in
WHY    — the rule or state of the world that caused it
NEXT   — the best move from here, as concretely as code can make it

An error with WHAT alone is a report.
With WHY, it's an explanation.
With NEXT, it's a recovery.

Notice this is our closure principle, extended to the path we forgot: every successful result should hand the agent its next move — and every failure should too. A tool set with closure only on the happy path is a language where any stumble is a dead end.

Precompute the recovery

Here is the part almost everyone underuses. At the moment of failure, your code knows things — the valid values, the free time slots, the current state — with a certainty the model can never match. Don't describe the recovery. Compute it.

Example 3 · The error that writes the correct call
user

You have the following tools available for use:


  1. log_expense( amount, currency, date, category )

Task: Log this expense: conference dinner, 940 kr, yesterday.

assistant
⟨tool_use⟩
log_expense(amount = 940, currency = "kr", date = "yesterday",
            category = "food")
user
⟨tool_result⟩
2 problems, both fixable:
· currency "kr" — this system uses ISO codes; you mean "NOK".
· date "yesterday" — must be absolute; today is 2026-09-18, so
  yesterday = "2026-09-17".
· category "food" is valid.
The corrected call:
log_expense(amount=940, currency="NOK", date="2026-09-17",
category="food")
assistant
⟨tool_use⟩
log_expense(amount = 940, currency = "NOK", date = "2026-09-17",
            category = "food")
NoteThe error didn't say "invalid currency format" and leave the repair as an exercise. Code knew the ISO table and today's date, so code wrote the corrected call — verbatim, ready to send. Spend code where code is certain; spend the model where judgment is needed. Your error handler has become a prompt writer.

A map from failures to recoveries

Failures are not all one kind, and each kind has a natural NEXT. This table is most of tool-error design:

What failed → what the error should teach

malformed callthe corrected call, precomputed, verbatim
wrong orderthe missing step: "read_file first, then edit"
missing preconditionhow to establish it: "calibrate('S-4') first" — including "read_manual('xrf') before first use"
ambiguous referencethe candidates, with ids: "3 customers named Hansen: #201, #345, #388 — which?"
transient failurethe retry policy: "try again in 60s, max 3×"
not permittedthe escalation path: who can approve, and how to ask them

Two rows deserve a closer look. The precondition row includes a pattern that connects to the library from earlier: an error can require the reading — "first use of this instrument: read its manual, then retry." Self-training on demand, enforced at the gate. And the not permitted row is richer than it looks:

Example 4 · The rule that explains its own exception
user

You have the following tools available for use:


  1. log_expense( … )
  2. request_exception( expense_id, justification, approver )

Task: Log the client dinner from the Oslo trip — 2,340 kr, four people.

assistant
⟨tool_use⟩
log_expense(amount = 2340, currency = "NOK", date = "2026-09-15",
            category = "client-meals")
user
⟨tool_result⟩
DENIED: client-meals limit is 500 kr per person; 4 attendees →
max 2000 kr. This is not final. Exceptions are granted for
client-facing events with a business justification. To request one:
1. the expense is parked as draft-8812;
2. call request_exception(expense_id="draft-8812",
   justification=<why this dinner served a business purpose>,
   approver="dept-head") — approvals usually within 2 days.
assistant
⟨tool_use⟩
request_exception(expense_id = "draft-8812", justification =
"Dinner with 3 representatives of the client following contract
negotiation; continuation of the working session.",
approver = "dept-head")
assistant

The dinner exceeds the per-person meal limit, so I've filed an exception request with the business justification; the department head typically responds within two days. The expense is parked as a draft until then.

NoteA bare DENIED ends the trajectory at a wall. This error taught the rule, taught that the wall has a door, and gave the exact sequence through it — and note the division of labor: code supplied the limit, the draft id, the procedure; the justification required judgment, so that part was left to the model. Each side spent where it is strong.

Fix the instance, or teach the rule?

One economic subtlety. Compare two errors for the same slip:

Two prices for the same lesson

Fixes the instance:"date must be absolute: use 2026-09-17"
Teaches the rule:"dates must be absolute (YYYY-MM-DD) — this applies to all logging tools. Here: 2026-09-17."

The first repairs one call. The second conditions the whole trajectory: the agent won't make the mistake again in THIS conversation — the rule is now on the wall, part of every future turn. Teach the rule when the agent will act again under it; fix the instance when it won't.

Remember the boundary, though: the model's weights are frozen. The rule is learned by the conversation, not by the model — the next conversation starts ignorant again, and its first slip buys the same lesson. That is not a flaw; it is the pricing. The rule costs nothing until a trajectory needs it, once per trajectory that does.

The error audit
  • Find WHAT, WHY, NEXT in Example 4's error. Which sentence is which — and which could code alone have written?
  • In Example 1, whose failure is it — the agent's or the designer's? Defend your answer.
  • What is the most common error your own tools would return — and what does its NEXT look like, precomputed?
Your turn

Take three real error messages from any system you use — an API, a statistics package, a university form. For each, rewrite it as the error an agent should receive: WHAT, WHY, NEXT, with everything code could know precomputed into the text. Then trace one recovery: show the failing call, your error, and the agent's next call. Which of your three rewrites teaches a rule, and which merely fixes an instance?