
One crash report, triaged by an agent
The crash-triage agent’s first run on a real Go web service, replayed offline. Four crash reports, two already filed, one bug that lives away from the line that crashed; fourteen tools, a final answer in a fixed shape, and a grade from the crash catalog.
This page works on all four stages of the loop
- 1Build the request (this page)
- 2Call the model (this page)
- 3Run the tools (this page)
- 4Decide (this page)
From chapter 7 on, the companion code’s agent has a real job: triaging crash reports from remind, a small web service for reminders (twenty packages, roughly 4,800 lines of Go), with bugs seeded in it on purpose.
This program runs one task from the crash catalog, replayed from a run recorded on anthropic/claude-haiku-5.5 in October 2026, with every tool working for real against a fresh copy of the service.
What it shows
- An agent with fourteen tools grouped by prefix:
crash_*to read reports,code_*to read and patch code,issue_*for the tracker,run_tests, and a final tool,submit_diagnosis. Designing tools a model uses well explains the choices. - A final answer as typed data, one
Diagnosisper crash group, checked before it is accepted: structured output. - A reproducer test that must fail on the original code and pass on the fix.
- A grade from the crash catalog’s grader, check by check.
Run it
From the code directory of a clone of go-agents-book-code, with no OPENROUTER_API_KEY set:
$ go run ./ch07
The task is m1-done-after-delete, which hands the agent four crash reports.
The tracker already has issues for two of them.
The other two crashed in one place because of a mistake made somewhere else.
The run starts like this:
task: m1-done-after-delete, 4 crash reports, 2 issues already filed
turn 1
model asks: crash_list {}
tool result: 4 reports: (5 lines)
turn 2
model asks: crash_read {"id": "cr_fd4982"}
model asks: crash_read {"id": "cr_1e2ea0"}
model asks: crash_read {"id": "cr_f9ee9e"}
model asks: crash_read {"id": "cr_ee551c"}
First the model gets the list of reports and opens all four in a single turn; then it goes to the code that each stack points to. Over 17 turns it searches the tracker, writes a reproducer test for each of the three causes, runs them to see them fail, fixes all three, runs the whole suite, comments on the two filed issues, files one new issue, and submits.
What to look for
The root cause it found. The service panicked in api.owned, dereferencing a nil reminder.
The real fault is upstream, in store.Get: asked for a deleted reminder, it returns a nil reminder together with a nil error.
Patching api.owned with a nil check would have ended this particular panic, but every other function that calls Get would still be open to the same mistake.
The agent’s evidence, in its final answer, cites the stack, the code, and the log line showing the reminder was deleted.
The grade:
stopped: the model answered
305001 tokens in, 7558 out, $0.0343
diagnosis:
fp_6b14b3 cr_f9ee9e: fixed, root cause auth/auth.go parseBearer, issue #101
fp_4828a6 cr_1e2ea0,cr_ee551c: fixed, root cause store/store.go (*Store).Get, issue #103
fp_2bcd17 cr_fd4982: fixed, root cause notify/notify.go (*Dispatcher).Stop, issue #102
filed #103: store.Get returns nil with no error for a deleted reminder, so api.owned panics
graded against the catalog: 12 of 16 checks passed
failed: status N1A: answered "fixed", want "duplicate"
failed: duplicate of N1A: answered issue 0, want 101
failed: status N2A: answered "fixed", want "duplicate"
failed: duplicate of N2A: answered issue 0, want 102
The run passed 12 of 16 checks.
The four failures say the agent marked both already-filed crashes as fixed, treating them as its own to fix, instead of as duplicates of issues #101 and #102.
The first system prompt never tells the model how to handle a report the tracker already knows about, and chapter 8’s prompt work starts from exactly this failure.
A typed answer is what made the mistake a failed check instead of a paragraph to read.
The cost. 17 turns, $0.034, and every request is bigger than the one before, growing from roughly 3,800 tokens to roughly 29,600. Context management is what brings that down.
Things to try
Check whether the model picks the right tool. A cheap smoke test, replayed offline:
$ go run ./ch07/evals/toolselect
$ go run ./ch07/evals/toolselect -distractors
The first replays fifteen short requests, three times each on four models, and prints each answer that scored below full marks; the second adds thirty plausible extra tools. Several “misses” are the system prompt winning: asked to file an issue, models search the tracker first, as the prompt tells them to.
Read a tool’s output the way the model does. The recorded tool outputs under ch07/testdata/out/tools show what concise results look like: crash_read shows the service’s own stack frames, one line each, and crash_goroutines condenses an 85 KB goroutine dump, 46 goroutines, into 8 groups that take 7 KB.
Run it live, with a key and OPENROUTER_MODEL set to another tool-capable model, and compare its grade with the recorded one.
This makes real model calls; the recorded run cost $0.034.
Expect a different path, and possibly a different grade; one run says little, which is what evals are for.
Key files
| File | What it holds |
|---|---|
ch07/triage/tools.go |
The fourteen tools and their safety flags |
ch07/triage/diagnosis.go |
The Diagnosis type, submit_diagnosis, and the reproducer check |
ch07/triage/crashtools.go |
Reading, searching, and summarizing crash reports |
ch07/triage/redact.go |
Removing credentials from tool output before the model sees it |
ch07/agent/registry.go, schema.go |
Tools from typed Go functions, and schemas from structs |
remind |
The practice service, with its seeded bugs |
catalog |
The crash catalog, its tasks, and the grader |