Certified Agent Harness Engineer
Course sections

M8.5

Debugging from logs, not by re-prompting

788 words · about 5 min

Concept

The default move for a misbehaving agent is to change the prompt and run it again. It is fast, it occasionally works, and it is not debugging — it is search. Each iteration costs a real model call, produces one noisy sample from a stochastic system, and leaves you knowing nothing you can write down.

Methodical debugging replaces "what if I change this?" with "what actually happened?" The session event log makes that answerable, because the loop's entire history is durable and inspectable without re-running anything.

Mechanism: what the log must guarantee

Mechanism

The invariant Module 4 established is what makes log-based debugging possible: model-visible ⟺ logged. If something reached the model, it is in the log; if it is in the log's surface, the model saw it. Any leak in either direction and the log stops being evidence — you are then debugging a story about the run rather than the run.

The projection layer follows from the same requirement. packages/session/session-projection/ folds committed events into whole current values: pure synchronous apply, plain-JSON state, and "a unit uninterested in an event MUST return the same state reference." The debugging value is that the log is the truth and every derived view is reproducible from it, so if a projection and the log disagree, the projection is wrong. There is no third possibility to investigate.

Worked example: the methodical procedure

Worked example

An agent was asked to fix a test and instead deleted it. The disciplined sequence:

  1. Reproduce from the log, not the prompt. Pull the session and read what happened. Zero model calls.
  2. Find the divergence point. Walk the turns until the first one where behavior stopped matching intent. Usually earlier than the visible symptom.
  3. Read the model's actual input at that turn. Not your template — the assembled request. This is where most bugs are found, and the discovery is nearly always that the model saw something different from what you believed you sent.
  4. Classify the failure. Harness bug (wrong request assembled), tool bug (tool returned something wrong), or model behavior (correct input, bad decision). These have completely different fixes, and only the third is a prompting problem.
  5. Only now form a hypothesis — and encode it as a test, not as a re-prompt.

Step 4 saves days. Postmortem 0001 is the canonical example of what happens when it is skipped: the investigation opened with an elegant theory about Cordis traceable/shadow contexts, pursued it, and found the real cause only after instrumenting the actual fiber walk — a stray export default apply. The postmortem's own lesson:

Trust the trace, not the theory. The elegant shadow explanation was real but was the second bug; the first was a one-line export mistake that a fiber-walk console.error found in minutes after hours of plausible-but-wrong reasoning.

The elegant theory was not even wrong. It was a genuine mechanism that caused a genuine second bug. It was just not the one being debugged. Plausibility is not evidence.

Tradeoffs — the honest bill

Tradeoffs

Log-based debugging requires the log to be complete, which costs storage and imposes the "model-visible ⟺ logged" discipline on every code path that touches a request. Teams that log "the important parts" discover during an incident that the important part was one they hadn't classified as important.

Some failures genuinely are model behavior, and for those the log tells you the input was correct and the output was bad — still valuable, because it is a definitive answer rather than a hypothesis, and it redirects you from harness debugging to prompt or model work with evidence in hand.

How dsh does it

Ledger telemetry mirrors the log, so a production session a developer cannot access locally is still reconstructible from exported records deduped on (session.id, event.seq). Snapshot fixtures come from real recorded sessions, so a production failure that gets recorded becomes a permanent replay test — the loop from incident to regression test is short by design.

InvariantError closes the loop from the other direction. When a runtime invariant fires it raises with code: 'INVARIANT', an owning packageName, and a message prefixed invariant violated by "<package>": …. The failure arrives pre-attributed, which converts "something is wrong somewhere" into "this package's contract broke" before anyone opens a file.

How others differ

Harnesses that treat conversation history as an in-memory array and print to stdout can debug only what was on screen — and the model-visible surface diverges from the printed one the moment you add compaction or tool-result truncation. τ-bench makes the same argument from the eval side: because it grades on final database state, a failed task hands you a state diff rather than a transcript to squint at.


Check yourself

2 questions on M8.5

Start drill