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
JavaScript
;
/**
* 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