agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
80 lines • 4.08 kB
JavaScript
/**
* localObservability — Tier-3 (Debug) observability: RETAIN a live run model,
* render it live, and snapshot it for offline replay.
*
* One handle, two outputs:
* - LIVE — `onLive(graph)` fires per event with a fresh `StepGraph`; hand
* it to your own renderer, or read `handle.getSnapshot()` /
* `handle.boundary` whenever you like.
* - OFFLINE— `getTrace()` (any time) and `onRecorded(trace)` (auto, at run
* exit) freeze the model into a JSON-lossless `Trace`.
*
* This handle is NOT a Lens recorder. `<Lens recorder={…} />` wants a
* `LensRecorder` — a different object with a different surface — and passing
* this one gives a component with none of the members it calls. To put a run
* in front of Lens, record it and hand the recording over:
*
* const stop = recordRun(agent); // agentfootprint/observe
* await agent.run({ message });
* const recording = stop.toRecording(); // { snapshot, events, structure }
*
* // …in the viewer:
* const { recorder, runner } = observeRecording(recording); // lens
* <Lens recorder={recorder} runner={runner} />
*
* Contrast with `enable.observability({ strategy })` (Tier-4 / Monitor), which
* ships each event to a vendor and FORGETS. localObservability KEEPS the model
* so you can look at it — locally, with full content. See
* `docs/design/local-observability-and-pii.md`.
*
* It's a thin wrapper over `enable.flowchart` (the existing live StepGraph) +
* `serializeTrace` (the snapshot). UI-free: returns data, never React.
*/
import { attachFlowchart } from './FlowchartRecorder.js';
import { serializeTrace } from './trace.js';
/**
* Attach a local-observability handle. `now` is injectable for tests (the
* library otherwise stamps `Date.now()` at serialize time).
*
* @internal Called from `RunnerBase.enable.localObservability`.
*/
export function attachLocalObservability(runnerAttach, dispatcher, options = {}, now = Date.now, access = {}) {
let completed = false;
// `let` (not const): `handle` is referenced by `buildTrace` and the onUpdate
// closure below, both defined before its assignment. The closures only run
// after `attachFlowchart` returns, so `handle` is always set by call time.
// eslint-disable-next-line prefer-const
let handle;
const buildTrace = (override) => serializeTrace(handle.boundary.getEvents(), {
capturedAtMs: now(),
...(access.getStructure && { structure: access.getStructure() }),
// Opt-in — `redact` is a per-event function and cannot reach inside a
// snapshot, so filling this by default would widen what a redacted
// Trace exports. Read at freeze time, not at attach time: before the
// run there is no snapshot, and during one it grows. `onRecorded`
// fires from the run's own exit boundary, where it is final.
...(options.includeSnapshot && access.getSnapshot && { snapshot: access.getSnapshot() }),
...(options.redact && { redact: options.redact }),
// Self-describing: events served under the run's redaction policy, with
// no per-event `redact` of the caller's over them, say `'policy'`. A
// caller's function still wins the label (`'pii'`) — it ran last.
...(!options.redact &&
override?.redact === undefined &&
access.redactedByPolicy?.() === true && { redactionLabel: 'policy' }),
...override,
});
handle = attachFlowchart(runnerAttach, dispatcher, {
onUpdate: (graph) => {
options.onLive?.(graph);
// Fire onRecorded once, when the root run boundary closes.
if (options.onRecorded &&
!completed &&
handle.boundary.getEvents().some((e) => e.type === 'run.exit')) {
completed = true;
options.onRecorded(buildTrace());
}
},
}, access.getCommitCount);
return { ...handle, getTrace: buildTrace };
}
//# sourceMappingURL=localObservability.js.map