Debug

Replay a saved run

Save a run as three things — snapshot, events, structure — then show it in Why Lens or Flow Lens. Nothing re-runs, no model is called.

You have a run. You want to look at it later — in a bug report, in a review, on another machine. Two steps: record it, then render it.

A recording is exactly three things. Miss one and one surface goes dark, so save all three together.

what it iswhat it buys
eventsthe typed agentfootprint stream, in orderthe story, the commentary, the summary
snapshotthe footprintjs run snapshotstate, the commit axis, provenance, every attached recorder's data
structurethe agent's build-time chartthe chart — and only the chart. Nothing else can draw it

Step 1 — Record

In the app that runs the agent. recordRun wires everything and hands back the three fields:

import { recordRun } from 'agentfootprint/observe';
import { narrative, metrics } from 'footprintjs/recorders';

// Optional, and each lights one panel:
agent.attach(narrative());   // the Story panel  (without it: "X executed. Wrote: y")
agent.attach(metrics());     // real durations   (without it: the Gantt shows ORDER, and says so)

const recorder = recordRun(agent);          // BEFORE the run
const reply = await agent.run({ message });

fs.writeFileSync('run.json', JSON.stringify(recorder.toRecording()));
recorder.stop();

toRecording() returns { snapshot, events, structure } — the exact shape the viewers consume. There is no assembly step and no per-viewer adapter.

Record time is the only time

A recording is collected as the run happens, and none of the three can be reconstructed afterwards: a finished run has no chart to give you, the event stream is dropped rather than queued when nothing is listening, and the commit log records what each stage wrote — never when a boundary was crossed. Call recordRun() before run(), or you are recording the next one.

What a recording keeps of a vector

A retrieval turn drags every retrieved entry's embedding through the memory-read subflow's boundary output — measured at 2.76 MB for one recorded turn, ~1.1 MB of it floats nothing renders. Since 8.20.0 a recording keeps a vector's shape, not its bytes: every embedding / embeddings field is replaced with an EmbeddingSummary{ dims, norm } — by recordRun and by boundaryRecorder at capture time. The retrieval evidence a debugger reads (scores, passages, documents, rejected candidates) carries no vectors and is untouched. Pass recordEmbeddings: true to either to keep the raw floats, and use the exported summarizeEmbeddings (whole values, copy-on-write) or summarizeVector (one vector) from agentfootprint/observe when a recording post-processor wants to apply or recognise the same projection.

If you are wiring it by hand

You rarely should, but the requirement is the one recordRun satisfies for you: the boundary recorder — the thing that puts stops on the step strip — needs three connections at record time, and each missing one fails differently.

import { boundaryRecorder } from 'agentfootprint/observe';

const boundary = boundaryRecorder({
  getCommitCount: () => agent.getCommitCount(),   // 3. where each boundary sits
});
agent.attach(boundary);        // 1. the boundaries themselves
boundary.subscribe(agent);     // 2. what happened inside them
missingwhat you get
attachno boundaries at all — loud, the strip is empty
subscribeboundaries with nothing in them — no LLM or tool detail behind any stop
getCommitCountevery boundary stamped commitIdxBefore: 0silent. The events look complete and the strip cannot be rebuilt

enable.flowchart() and enable.localObservability() wire all three for you (since 7.8 — before that they missed the third, and every recording made through them carried a flat axis).

A plain footprintjs pipeline

The same fields, one layer down:

const recording = {
  events:    [],                          // typed events are an agentfootprint thing
  snapshot:  executor.getSnapshot(),
  structure: chart.buildTimeStructure,    // on the BUILT chart — no snapshot carries it
};

Step 2a — Render it in Why Lens (the agent view)

import { observeRecording, Lens } from 'agentfootprint-lens';

const { recorder, runner, boundaryRanges } = observeRecording(
  JSON.parse(await fs.readFile('run.json', 'utf8')),
);

return (
  <>
    {boundaryRanges === 0 && <p>This recording has no step boundaries.</p>}
    <Lens recorder={recorder} runner={runner} theme={{ mode: 'light' }} />
  </>
);

Nothing re-runs, no model is called, no network is touched. boundaryRanges === 0 means the run was recorded without a commit-tracking boundary recorder — the step strip stays quiet rather than inventing stops.

Step 2b — Render it in Flow Lens (the pipeline view)

import { ExplainableShell, overlayFromSnapshot } from 'footprint-explainable-ui';
import { structureGraphFromSpec } from 'agentfootprint-lens/core';

const rec = JSON.parse(await fs.readFile('run.json', 'utf8'));

return (
  <ExplainableShell
    runtimeSnapshot={rec.snapshot}
    traceGraph={structureGraphFromSpec(rec.structure)}
    runtimeOverlay={overlayFromSnapshot(rec.snapshot)}
    traceTheme={{ mode: 'light' }}
  />
);

You do not pass narrative entries: the story is read out of the snapshot's own recorder data. traceTheme={{ mode }} re-themes the whole shell — for a light app, that one word is the entire theme wiring.

Pass runtimeOverlay explicitly. Without it the chart renders in its unvisited colours with no warning — the snapshot is right there, but the shell does not reach into it on your behalf.

<Replay trace> — the chart-only subset

localObservability() freezes a different, smaller artifact: a Trace, which is the domain-event log plus the chart. <Replay> renders it.

import { Replay } from 'agentfootprint-lens';

const trace = JSON.parse(fs.readFileSync('run.trace.json', 'utf8'));
return <Replay trace={trace} />;   // draws trace.structure

The flowchart below is that component rendering a Trace captured from a real run (scripts/gen-replay-trace.mjs) — offline, with no runner.

Loading the replay…

Be clear about what this is: <Replay> draws the chart's shape and stops there. It does not read trace.events — time-travel over them is a planned refinement — so an offline <Replay> is a strictly smaller view than the live <Lens>, not a match for it. And a Trace carries no footprintjs snapshot unless you ask for one, so on its own it cannot drive ExplainableShell, WhereFrom, or the commit axis:

const dev = agent.enable.localObservability({ includeSnapshot: true });
const trace = dev.getTrace();   // now carries state + commit log too

Reach for <Replay> when a static picture of the run's shape is what you want. When you want the run, record it and use step 2a.

Redaction travels with the Trace

A live, in-process model is fine to hold raw, but serializing is a trust-boundary crossing — the artifact can travel. So redaction runs at getTrace(), and the result is self-describing: trace.redaction is 'pii' when a redact ran, 'none' when it didn't. When a Trace carries raw content, <Replay> shows a banner so a shared trace is never mistaken for safe.

You want…Call
Best-practice redactiongetTrace({ redact: redactContent })
Custom scrub (write-once fn)getTrace({ redact: (e) => myScrub(e) })
Raw content (trusted, local)getTrace()redaction: 'none' (banner shown)

The graph is always derived from the (already-redacted) events — it is never stored — so redaction reaches the rendered flowchart with no second content surface to leak.

redact runs per domain event, which is exactly why includeSnapshot is opt-in: a snapshot's shared state is the run's raw working memory, and no per-event function can reach inside it. Redact that half at run time with footprintjs's setRedactionPolicy(), where the values are written.

What a recording honestly cannot show

Each viewer states these on screen rather than faking them:

  • Per-stage durations without metrics() — the Gantt shows execution ORDER instead, and says so.
  • The step strip without a commit-tracking boundary recorder — the strip stays quiet.
  • Error messages — the commit log has no error channel. A failing stage's writes land; its message does not.
  • Deep subflow internals — footprintjs deliberately keeps them out of the run-level commit log, so a replay lights the mount stages, not their insides.
  • Detail behind each stop, on a lean recordingrecordRun(agent, { boundaryDetail: 'lean' }) stores boundary structure without captured content. The bundle says so in its own meta.mode, so a viewer can tell you rather than showing empty panels.

Next steps

On this page