Recorded chat
Record a chat turn-by-turn so any reply can be explained, counterfactually re-run, and forked — without re-writing the glue every chat host gets subtly wrong.
Beta
recordedChat composes the beta context-bisect tools (localizeContextBug, rerunWithoutSources). It works and ships with a tested example, but the API may still change before GA.
You've built the Influence Map loop — localize a reply, ignore a source, re-run. Now put it inside a conversation.
recordedChatrecords a chat turn-by-turn so any reply can be explained, counterfactually re-run, and forked — and it owns the correctness-critical glue every chat host otherwise re-writes and gets subtly wrong.
The three traps it owns
Multi-turn chat on agentfootprint is a host convention: AgentInput has no history field, so you thread the transcript into the message string yourself. That convention hides three correctness traps:
getLastSnapshot()is last-run-only. A secondrun()clobbers it, so reasoning about turn K after turn K+1 ran silently attributes the wrong run.send()captures the snapshot, events and last LLM call immediately and freezes them on the turn — the stale-snapshot bug becomes structurally impossible.- History is a convention, not an API. The transcript preamble must be byte-identical between the recorded turn and its counterfactual re-run, or the re-run ablates a subtly different scenario without failing anything.
recordedChatcomposes the message once at record time and stores the exact string on the turn; every re-run replays those bytes verbatim. - The
AblationRunnerduplicates turn construction. The re-run is only valid if the rebuilt turn matches the recorded one.rerunTurn(k)derives the runner from the samemakeAgentthat ran the turn, merging the session's persistent removals with the probe's specs.
Quick start
You supply one agent factory. It applies the ablation specs at agent construction (the documented seam — see Localize a context bug) and returns a fresh agent. Live turns, re-run probes, baseline probes and fork turns all go through it.
// The ONE factory — specs applied at CONSTRUCTION, fresh provider per call:
const : = ({ }) => {
const { } = ([...], { : });
let = .({ : (), : 'mock-1' }).('You are an advisor.');
for (const of ) = .();
return .();
};
const = (());
const = ({ , : { : 'Advisor' } });
// 1. Record turns — each send() freezes that turn's evidence.
await .('How is the position looking?');
const = await .('Should we BUY or HOLD this position?'); // → BUY
// 2. reason(k) — what drove reply K? (memoized localizeContextBug)
const = await .(1, { });
(); // the ignore toggles — social-sentiment among them
// 3. rerunTurn(k) — that exact turn, byte for byte, minus a source.
const = await .(1, {
: ['social-sentiment'],
,
: ,
: true, // unlock the causal-tier verdict
});
.; // → HOLD
.?.; // → 'confirmed'
// 4. fork(k) — continue the conversation from the what-if, as a NEW session.
const = .(1, { : });
await .('How much should we allocate?'); // reads "Advisor: HOLD" → KEEPrerunTurn returns agentfootprint's RerunWithoutSourcesResult unmodified — the honesty tiers are not hidden. reason returns the ContextBugReport unmodified, so removableSources(report), formatContextBugReport and your Influence Map joins compose exactly as they do on the flat loop.
Forking — branch, never rewrite
fork(k, { fromRerun }) returns a new RecordedChat whose seed is the conversation through turn K with the reply swapped for the re-run's counterfactual answer. The original session is never touched — its transcript, turns and reports stay whole.
- Provenance is enforced.
fromRerunmust be a result this session'srerunTurnproduced for that turn — a fabricated fork would be a lie. A mismatched result throws. OmitfromRerunto branch from the original reply (no ablation). - The what-if world stays the what-if world. The fork carries the re-run's
removedsources forward: every later turn (and every probe) in the fork also runs without them. - Fork turns are first-class. They run through the same
send(), so a fork's replies get their ownreason/rerunTurn/fork. - Rehydration. Persist
chat.seed-shaped transcript +chat.removed, and reopen withrecordedChat({ makeAgent, seed, removed }). Authenticity of a hand-supplied seed is then yours.
What stays yours
recordedChat records, reasons, re-runs and forks. Everything product-shaped stays host-side by design:
- Session registries & wire ids —
Map<sessionId, RecordedChat>, labels, over-the-wirererunIdmaps. In-process,fork({ fromRerun })identity-checks the result object; over HTTP you keep your ownrerunId → resultregistry. - UI joins — the Influence Map (
removableSources× suspects), strategy pickers, label lookups — all compose from the unmodifiedContextBugReport. - Comparators & embedders — your domain
answerChanged, your shared embedding cache, your real-embedder choice. - Agent construction — provider, model, system prompt, facts/tools/memory,
applyAblationsinsidemakeAgent. The library never builds agents; it only demands specs be applied at construction. - Cost policy —
samples, live-mode gating, cost banners. - Persistence format — how you serialize and rehydrate a session.
Honesty notes
- A verdict only with
checkBaseline. Without it,rerunTurnreports what was observed (whatChanged.answerFlipped) and carries noverdict. Scores suggest; re-runs convict — the same discipline asrerunWithoutSources. - Byte-exact means the MESSAGE. An agent with cross-run persistent memory (
.memory()stores written by later turns) can still recall different entries on a re-run — the baseline probe surfaces that asbaseline-unstable, which is exactly the honest signal. - Pauses are unsupported inside a recorded turn. A run that pauses (
askHuman/checkIn) throws and records nothing — drive pausing agents withagent.run()directly. rerunTurnresolvesignoreagainst the memoized report — a source outside the report'smaxSuspectscap isn't removable until you re-reasonwith a bigger cap.
The full runnable, tested example is 19-recorded-chat.ts — a three-turn chat desk whose turn-2 BUY flips to HOLD and whose fork's turn-3 diverges ADD → KEEP.
Next steps
- Re-run without sources — the flat counterfactual
recordedChatderives per turn - Localize a context bug — where the report and the removable specs come from
Variable recall
Ask about a VARIABLE, not a step — its whole recorded life, in agent vocabulary. And where the dataflow is exact, the backward walk stops guessing.
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.
