Σ 0:00▸ 0:00

Module 3 · Agent Tools, Knowledge, Memory & Research Design · scripted

Errors That Teach

30 minoutcomes: tool-design, agent-architectures

Overview

Every tool call can fail — and computing has fifty years of tradition about what to say when it does: terse codes, written for a human reading a log file later. But an error, like every result, is a message into a conversation — and for the first time in computing history, the reader at the moment of failure can actually read. This lesson rewrites errors for that reader: what happened, why, and what to try next — so a failure becomes the most instructive message on the wall, and the conversation gets better after things go wrong.

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

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

Most errors are terse records of a problem in the code. They are not instructive: someone has to look at the error, look at the code, and start figuring it out.

That design was correct for its world: the program that hit the error had no intelligence and could only crash or retry blindly, so the explanation waited for a human. But the receiver of a tool-call error is intelligent — so we can make errors more useful.

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 classic error, delivered to an intelligent reader
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
book_room(room = "seminar", date = "2026-09-17", start = "14:00",
          end = "16:00")
user
ERROR 409: CONFLICT
assistant
book_room(room = "seminar", date = "2026-09-17", start = "14:00",
          end = "16:00")
user
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
book_room(room = "seminar", date = "2026-09-17", start = "14:00",
          end = "16:00")
user
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
book_room(room = "aud-2", date = "2026-09-17", start = "14:00",
          end = "16:00")
user
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
log_expense(amount = 940, currency = "kr", date = "yesterday",
            category = "food")
user
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
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
log_expense(amount = 2340, currency = "NOK", date = "2026-09-15",
            category = "client-meals")
user
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
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")
user
Exception request filed; awaiting dept-head approval.
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?