/** * Trace — a UI-free, JSON-lossless snapshot of a run for OFFLINE REPLAY. * * `localObservability()` (Tier-3 / Debug) retains a live model during a run. * `serializeTrace()` freezes that model into a `Trace` — plain JSON you can * persist (file, Redis, a bug report) and later rehydrate WITHOUT re-running * the agent. `agentfootprint-lens`'s `` consumes it and * draws the flowchart via the existing translators. * * A `Trace` stores the domain-event log (the single source of truth the Lens * already reads) plus, since 7.8, the run's footprintjs snapshot. The step * graph is ALWAYS a derived projection of those events (footprint.js's "graph * is derived, never post-processed" principle) — it is rebuilt at render time, * never stored. Storing a derived graph would be redundant AND a redaction * hazard: a second content surface a per-event `redact` could never reach. * * WHAT A TRACE CAN AND CANNOT DRIVE * ───────────────────────────────── * `` draws the chart from `structure`, and that is all it * draws: a static shape, not a lit-up path. It does not read `trace.events` — * time-travel over them is a planned refinement, and until it lands an offline * `` is a strictly smaller view than the live ``, not a match * for it. * * For the full viewers — ``, ExplainableShell, WhereFrom, the commit * axis — what you want is a RECORDING: `{ snapshot, events, structure }`, the * shape `recordRun()` produces and lens's `observeRecording()` consumes. Note * `events` there means the TYPED agentfootprint stream (`runner.on('*')`), * which is not the domain-event log a Trace carries: the two are different * projections of the same run, and a Trace holds only the second. So a Trace * with a `snapshot` can drive the snapshot-fed surfaces, but it is not itself * a recording — reach for `recordRun()` when the destination is a viewer. * * PII / trust boundary: the event log carries real content — `llm.end.content`, * `tool.start.args`, `tool.end.result`, `context.injected.contentSummary`, * `run`/`subflow` `payload`, `decision.branch.rationale`. A live, in-process * model is fine, but **serializing is a trust-boundary crossing** (the trace * can travel). So redaction is applied HERE, at serialize time, via a * consumer `redact` function — PII never enters the `Trace`. `redactContent` * is a ready-made redactor covering every content field. The result is * self-describing: `trace.redaction`. See * `docs/design/local-observability-and-pii.md`. * * Because `getEvents()` is FLAT (parent + every subflow), one `redact` pass * covers the whole tree — no per-subflow inheritance needed here. (The engine's * `RedactionPolicy` separately propagates to subflows for the OBSERVER mirror.) */ import type { DomainEvent } from './BoundaryRecorder.js'; import { type StepGraph } from './FlowchartRecorder.js'; /** * How a `Trace` was redacted before serialization. * - `'none'` — raw content (no `redact`). A `` UI may warn. * - `'pii'` — a consumer `redact` ran (the default label when one is given). * - `'policy'` — produced from a declarative `RedactionPolicy` (future). */ export type TraceRedaction = 'none' | 'pii' | 'policy'; /** Cheap headline rollup, so a consumer can show totals without folding `events`. */ export interface TraceSummary { readonly tokens: { readonly input: number; readonly output: number; }; readonly llmCalls: number; readonly toolCalls: number; readonly durationMs?: number; } /** * A JSON-lossless, UI-free snapshot of one run. Persist it, ship it, replay it. * `events` ARE the timeline (the graph is a derived projection, rebuilt at * render); `structure` is the chart; `snapshot` is the state the run left. */ export interface Trace { /** Schema version. Bump on a breaking shape change. */ readonly version: 1; /** The domain-event log — the whole timeline. Already redacted if `redact` ran. */ readonly events: readonly DomainEvent[]; /** * The serialized STATIC chart structure (footprint.js `buildTimeStructure`). * Design-time data (stage ids/names/types/edges) — UI-free. `` draws * the flowchart from this. NOT runtime-redacted (it carries no user data; the * `redact` function targets runtime events). * * Absent, `` has nothing to draw and says so. */ readonly structure?: unknown; /** * The run's footprintjs snapshot (`runner.getLastSnapshot()`) — shared * state, the commit log, and every attached recorder's data. * * This is the piece the snapshot-fed surfaces need and the event log cannot * supply: ExplainableShell's memory and provenance panels, WhereFrom, and * the commit axis all read the commit log, which is a footprintjs artifact * and appears nowhere in `events`. Without it a Trace draws a chart and a * timeline and nothing else — which is what every Trace captured before 7.8 * is, and why they replay as a static picture. * * `unknown` on purpose: this is footprintjs's shape, not agentfootprint's, * and a Trace is a transport — it should not pin a peer's type into its own * schema. Cast at the consumer. * * NOT redacted by `redact`, which runs per domain event and cannot reach * inside a snapshot. This field carries whatever the run's own footprintjs * redaction policy (`setRedactionPolicy()`) let through, so a run whose * state holds secrets wants that policy set at run time — which is why * `enable.localObservability({ includeSnapshot: true })` is opt-in and a * redacted Trace does not quietly grow a second content surface. */ readonly snapshot?: unknown; /** Optional headline totals. */ readonly summary?: TraceSummary; /** Self-describing redaction state — travels with the trace. */ readonly redaction: TraceRedaction; /** Wall-clock capture time, stamped by the caller (the engine has no clock here). */ readonly capturedAtMs?: number; } export interface SerializeTraceOptions { /** * Consumer redaction — runs once per domain event at the serialize boundary, * so PII never enters the `Trace`. Return a scrubbed COPY (do not mutate the * input — the live model still references it). Use `redactContent` for a * ready-made redactor. When omitted, content is raw. */ readonly redact?: (event: DomainEvent) => DomainEvent; /** Override the `redaction` label. Defaults to `'pii'` when `redact` is given, else `'none'`. */ readonly redactionLabel?: TraceRedaction; /** The serialized static chart (`getSpec().buildTimeStructure`) — what `` draws. */ readonly structure?: unknown; /** * The run's footprintjs snapshot (`runner.getLastSnapshot()`) — the commit * log and recorder data the snapshot-fed panels read. `enable.localObservability()` * fills this for you; pass it explicitly when calling `serializeTrace` by hand. */ readonly snapshot?: unknown; /** Optional precomputed headline rollup. */ readonly summary?: TraceSummary; /** Wall-clock capture time. Pass `Date.now()` from the call site. */ readonly capturedAtMs?: number; } /** * Ready-made redactor: replaces every content-bearing field with a marker, * keeping structure/counts for a useful replay. Covers ALL `DomainEvent` * content surfaces — pass it to `getTrace({ redact: redactContent })`. * * Returns a copy only when it changes something, so unaffected events stay * referentially identical (cheap) and the caller's live model is never mutated. */ export declare function redactContent(event: DomainEvent): DomainEvent; /** * Freeze a live run model into a `Trace`. Pure: pass the `BoundaryRecorder`'s * `getEvents()` output. * * const trace = serializeTrace(handle.boundary.getEvents(), { * redact: redactContent, // PII stripped before it enters the trace * structure: agent.getSpec().buildTimeStructure, // the chart * snapshot: agent.getLastSnapshot(), // state + commit log + recorders * capturedAtMs: Date.now(), * }); * fs.writeFileSync('run.trace.json', JSON.stringify(trace)); * * `enable.localObservability().getTrace()` passes all three for you. */ export declare function serializeTrace(events: readonly DomainEvent[], options?: SerializeTraceOptions): Trace; /** * Rebuild the step graph from a `Trace` — the offline half of replay. The graph * is ALWAYS a derived projection of `trace.events`; because those events were * already redacted at serialize time, the rebuilt graph is clean too (no extra * redaction needed — that's exactly why the graph is never stored). UI-free: * `agentfootprint-lens`'s `` translates this `StepGraph` into its * xyflow render model. */ export declare function traceToStepGraph(trace: Trace): StepGraph;