Chapter 25 — Debugging Agent Loops
Jordan gets duplicate WhatsApp messages on O-1001. Ops restarts the worker. Duplicates stop briefly, then return on the next retry.
trace by case_id → waterfall → disease heuristics → diff last good run → reproduce on fixture → fix + regression
Restart clears a stuck lease; it does not name the broken invariant — missing idempotency key, policy bypass, non-terminating tool loop.
This chapter adds an operational debug sequence: pull the trace, classify the loop disease, diff against a known-good run, seal the fix in a harness fixture before touching prod restart policy.
First principles
Common loop failures:
| Disease | Symptoms | First checks |
|---|---|---|
| Non-termination | max steps, repeated tool | duplicate fingerprints, stop conditions |
| Hallucinated success | answer without evidence | fabricate guards, required_evidence |
| Policy bypass | tool span without policy span | executor mounting, dry-run flags |
| Token burn | huge model spans | context assembler, retry storms |
| Duplicate side effects | N sends | idempotency keys, at-least-once |
Use this operational sequence:
- Pull trace by
case_id/run_id - Waterfall — where is time and where are denies?
- Disease heuristics (
debug_cli inspect) - Diff against last good run (Ch 24)
- Reproduce on harness fixture
- Fix in code/policy; add regression fixture
- Only then touch prod restart policies
Restart for a stuck lease, not a logic bug. Two Outreach send spans without the same durable idempotency key → channel-executor defect. Fix: pass and persist the key; verify repeated delivery produces one customer message.
Concrete example
Duplicate WhatsApp sends:
- Two
tool:send_whatsappspans - Both missing
idempotency_key classify_loop_disease→duplicate_send_risk: missing idempotency_key- Root cause: gateway client didn’t pass the key from the executor
- Fix + fixture: second send with same key returns
{duplicate: true}and customer sees one message
Diagram
flowchart TD
Start[Incident] --> Trace[Load trace]
Trace --> WF[Waterfall]
WF --> Dis[Disease heuristics]
Dis --> Diff[Run-diff vs last good]
Diff --> Repro[Harness repro]
Repro --> Fix[Fix + fixture]
Fix --> Done[Deploy]
Start -.->|avoid| R[Restart and pray]
Implementation
Reference: shopops/debug_cli.py.
# from manuscript/code/shopops with PYTHONPATH=.
python -m shopops.debug_cli inspect /tmp/o1001.jsonl
python -m shopops.debug_cli diff /tmp/run-good.json /tmp/run-bad.json
Programmatic:
from shopops.debug_cli import classify_loop_disease
spans = [
{"kind": "tool", "name": "send_whatsapp", "attrs": {"tool": "send_whatsapp"}},
{"kind": "tool", "name": "send_whatsapp", "attrs": {"tool": "send_whatsapp"}},
]
diseases = classify_loop_disease(spans)
assert any("idempotency" in d for d in diseases)
Sealed failing fixture (exercise): ship a JSONL with a get_order loop; students run inspect and name the disease before reading the answer key.
Failure modes
- Restart as fix — clears symptoms, not causes.
- Debugging in prod with live customers — no harness repro.
- Only reading model text — ignoring policy/tool spans.
- Heuristics as certainty — they suggest, they do not prove.
- No regression fixture — bug returns next week.
Production considerations
- On-call runbook = this chapter’s tree + link to trace UI.
- Auto-attach
inspectoutput to incident tickets. - SLOs on duplicate send rate with paging.
- Pair with ledger CONFLICT dashboards when multi-agent.
- Culture: blame the missing invariant, not “the agent.” Production debugging culture notes vary by org — write yours down. [VERIFY SOURCE if citing a specific text.]
Playbook card (keep near the pager)
1. case_id / run_id → fetch JSONL
2. shopops-debug inspect <file>
3. If duplicate_send_* → check gateway idempotency + executor keys
4. If non_termination_suspect → check max_steps + duplicate tool guard
5. If policy_bypass → freeze deploys; audit tool mounts / dry-run flags
6. Diff vs last green run_id (Ch 24)
7. Reproduce with harness scenario; merge fixture with fix
8. Restart workers only for lease/poison issues — after the fix ships
The WhatsApp duplicate case closes Part VII: observability without idempotency is a camera pointed at a fire. You need both the picture and the extinguisher (Ch 6).
Sealed fixture workflow
- Export a failing prod trace (redacted) into
evals/scenarios/sealed/ shopops-debug inspect→ disease list- Student/engineer writes hypothesized root cause
- Patch + add harness scenario that fails before patch and passes after
- Only then clear the incident
Skipping (4) is how the WhatsApp duplicate returns after the restart folklore fades.
Chapter summary
- Playbooks beat restarts.
- Diseases: loops, fiction, bypass, burn, duplicates.
- Trace → waterfall → heuristics → diff → repro → fixture.
- Idempotency keys are send-span mandatory.
- Heuristics are leads, not verdicts.
- Every fix earns a harness case.
- Policy bypass means executor/authz bugs.
- Part VII closes: you can now see and compare behaviour.
Exercises
- Debug a sealed hostile-loop JSONL with
inspect; paste the disease list. - Extend
classify_loop_diseaseto flag model→tool without intervening policy span for write tools. - Create run-good/run-bad artifacts for the WhatsApp duplicate;
diffthem. - Write the one-page on-call runbook for ShopOps sends (link metrics, traces, harness).
References
- Ch 23–24 — traces and run-diff.
- Ch 6 — idempotency and retries.
- Ch 12 — duplicate tool detection.
- Production debugging culture notes. [VERIFY SOURCE]