UNPKG

agentfootprint

Version:

The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.

132 lines • 6.92 kB
"use strict"; /** * 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 `<Replay trace={…} />` 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 * ───────────────────────────────── * `<Replay trace={…} />` 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 * `<Replay>` is a strictly smaller view than the live `<Lens>`, not a match * for it. * * For the full viewers — `<Lens>`, 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.) */ Object.defineProperty(exports, "__esModule", { value: true }); exports.traceToStepGraph = exports.serializeTrace = exports.redactContent = void 0; const FlowchartRecorder_js_1 = require("./FlowchartRecorder.js"); /** * 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. */ function redactContent(event) { switch (event.type) { case 'llm.end': return { ...event, content: `[${event.content.length} chars]` }; case 'tool.start': return event.args !== undefined ? { ...event, args: '[redacted]' } : event; case 'tool.end': return event.result !== undefined ? { ...event, result: '[redacted]' } : event; case 'context.injected': return event.contentSummary !== undefined ? { ...event, contentSummary: '[redacted]' } : event; case 'run.entry': case 'run.exit': return event.payload !== undefined ? { ...event, payload: '[redacted]' } : event; case 'subflow.entry': case 'subflow.exit': return event.payload !== undefined ? { ...event, payload: '[redacted]' } : event; case 'decision.branch': return event.rationale !== undefined ? { ...event, rationale: '[redacted]' } : event; default: return event; } } exports.redactContent = redactContent; /** * 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. */ function serializeTrace(events, options = {}) { const { redact, redactionLabel, structure, snapshot, summary, capturedAtMs } = options; // A fresh array either way, so a held Trace is detached from the live store. const safeEvents = redact ? events.map((e) => redact(e)) : events.slice(); const redaction = redactionLabel ?? (redact ? 'pii' : 'none'); return { version: 1, events: safeEvents, ...(structure !== undefined && { structure }), // Held by reference, not cloned. footprintjs's `sharedState` is a live // view of working memory, so a Trace taken mid-run keeps moving until the // run ends — which is the same caveat `getSnapshot()` carries, and the // reason `onRecorded` fires at the run's exit boundary. Serializing the // Trace (its whole purpose) detaches it. ...(snapshot !== undefined && { snapshot }), ...(summary !== undefined && { summary }), redaction, ...(capturedAtMs !== undefined && { capturedAtMs }), }; } exports.serializeTrace = serializeTrace; /** * 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 `<Replay>` translates this `StepGraph` into its * xyflow render model. */ function traceToStepGraph(trace) { return (0, FlowchartRecorder_js_1.buildStepGraphFromEvents)(trace.events); } exports.traceToStepGraph = traceToStepGraph; //# sourceMappingURL=trace.js.map