Session 9. Trace and evaluate an agent — Thu 24 Sep

A trace, and what an event has to carry

"It seemed to work" is a report about one run

It is also a report by a motivated observer, on a run they chose to remember. Nothing in it survives a model upgrade, a prompt edit, or a colleague asking "are you sure".

uv run bootcamp-agent --eval

That is a report about the system: fixed cases, a defined pass condition, rerunnable by anyone. The difference between the two sentences is the whole of this session. Everything below is either the measurement or the evidence you read when the measurement goes red.

The event

An agent that returns only an answer cannot be debugged. answer_question returns what it did as well:

@dataclass(frozen=True)
class TraceEvent:
    kind: str  # "retrieve" | "tool_call" | "llm_call" | "decision"
    detail: str

Two fields, and they are enough to bucket a failure:

[retrieve ] top_k=3 -> [('rag-basics', 0), ('rag-basics', 1), ('rag-basics', 2)]
[llm_call ] attempt 1: 121 chars
[decision ] answered with citations []

Retrieval returned rag-basics, three chunks of it. The model was called once. The answer cited nothing. Three lines, and the failure already has a name.

What an event has to carry to be worth keeping

A trace is read by someone who was not there, later, in a hurry. Four questions get asked every time, and an event that cannot answer them is decoration.

Question The field What goes wrong without it
What kind of step was this? kind you cannot filter, count, or group anything
What happened in it? detail you know a step occurred and nothing else
Which run was this? a run id two runs interleave in one file and read as one
When, and for how long? a timestamp, a duration you cannot tell slow from stuck

kind carries most of the weight, and it is worth being strict about the values. Four names you chose beat free text: retrieve, tool_call, llm_call, decision can be counted. "called the model (retry)" cannot.

Provider-neutral, on purpose

Name the step you took, never the vendor you took it with.

TraceEvent("llm_call", "attempt 1: 121 chars")          # yours
TraceEvent("anthropic.messages.create", "200 in 840ms")  # theirs

The second line ties your log to one provider's API shape. Swap the provider in session 2's adapter and every dashboard, filter and comparison built on that log breaks — and worse, last month's runs stop being comparable with this month's. The provider belongs inside detail as a value. It does not belong in the name of the step.

The same reasoning already shaped LLMClient: one seam, many providers behind it. A trace is that seam pointed at the past.

The trace is the input to everything else

Built on the trace Session
the budget you can prove was respected 5
the bucket a failure lands in 9, this one
the eval report's "why" column 9
the incident you reconstruct after the fact 14

The next page spends that trace: it turns a red eval into a named bucket. The one after it asks the other question — what a trace must not carry, and what happens when part of it is missing. A trace is durable and it is trusted. Those are its two best properties, and its two worst failure modes.