agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
123 lines • 6.52 kB
JavaScript
/**
* openRecording — a saved run, reopened as evidence a model can navigate.
*
* Pattern: pure adapter. One shape in (`Recording`), one shape out
* (`TraceToolpackArtifacts`). No engine, no agent, no I/O.
* Role: the door between the two halves of this library that already
* knew about each other's data and had no way to say so.
*
* `recordRun(agent)` freezes a run into `{ snapshot, events, structure }` —
* the shape a viewer reads. The trace toolpack reads a different shape.
* Until now, a team with a recording on disk from last Tuesday and a
* question about it had to reassemble the bag by hand, which meant
* reassembling it differently in every integration and losing the
* narrative and the events on the way.
*
* const recording = JSON.parse(fs.readFileSync('run.json', 'utf8'));
* const tools = traceToolpack(openRecording(recording));
* await callTraceTool(tools, 'run_overview');
*
* WHAT SURVIVES THE ROUND TRIP, AND WHAT DOES NOT
* ───────────────────────────────────────────────
* A recording is JSON. Everything the toolpack needs from the snapshot
* survives that (the commit log, the execution tree, shared state, the
* mode discriminants) and so do the events. Two things do not:
*
* - `controlDeps` is a LOOKUP FUNCTION built by a recorder that watched
* the run happen. A function does not serialize, and nothing in a
* finished recording can rebuild one. So slices opened over a
* recording carry the toolpack's existing "⚠ control edges
* unavailable" marker — the honest answer, already written.
* - The narrative survives only if it was ATTACHED. `recordRun`
* deliberately attaches no narrative recorder (see its header), so a
* recording made without `narrative()` has none, and `read_narrative`
* is simply not mounted. This function lifts it from the snapshot's
* recorder rows when it IS there.
*
* `structure` — the build-time chart — has no reader in the toolpack: the
* toolpack navigates what a run DID, and the chart says what it COULD do.
* It is left on the recording for the viewers that draw it.
*/
import { unpackRecording } from '../recorders/observability/recordingPack.js';
/**
* The narrative recorder's row: `data` is an array of `{ text, depth }`.
*
* @internal Exported for the bug-report bundler, which writes the same lines to
* `narrative.txt`. One shape-detector, one answer to "is there a narrative in
* this snapshot?" — two copies would disagree the first time the row changes.
*/
export function narrativeFrom(snapshot) {
const rows = snapshot.recorders;
if (!Array.isArray(rows))
return undefined;
for (const row of rows) {
const data = row?.data;
if (!Array.isArray(data) || data.length === 0)
continue;
// Identify by SHAPE, not by id. A consumer may attach `narrative({ id })`
// under any id they like, and a row whose entries carry `text` + `depth`
// is a narrative whatever it calls itself.
const first = data[0];
if (typeof first?.text !== 'string' || typeof first?.depth !== 'number')
continue;
return data.map((entry) => typeof entry?.text === 'string' ? entry.text : '');
}
return undefined;
}
/**
* Reopen a saved recording as toolpack artifacts.
*
* @param recording the `{ snapshot, events, structure }` bundle from
* `recordRun(...).toRecording()` — live, or parsed back
* from JSON — or the same recording packed
* (`packRecording`), which is expanded first.
*
* A packed recording is expanded only when its plain form is within
* `unpackRecording`'s default bound (`DEFAULT_UNPACK_MAX_BYTES`, 512 MiB): the
* toolpack previews a value by serializing it, so it walks what it reads as a
* tree, and a few KB packed can stand for far more than any plain recording
* could hold. A larger recording you trust is opened by expanding it first,
* with the bound you choose:
* `openRecording(unpackRecording(packed, { maxBytes }))`.
*
* @throws PackedRecordingTooLargeError for a packed recording over that bound.
* @throws when the recording carries no usable snapshot. A toolpack over
* an empty bag would answer every question with "nothing
* happened", which is the one answer a debugging session must
* never be given by mistake.
*/
export function openRecording(input) {
const recording = unpackRecording(input);
const snapshot = recording?.snapshot;
if (snapshot === null || typeof snapshot !== 'object') {
throw new Error('openRecording: this recording has no snapshot. A recording is ' +
'{ snapshot, events, structure } and the snapshot is the part the trace tools read — ' +
'without it there is no commit log, no execution tree, and nothing to explain. ' +
'Produce one with `recordRun(agent)` BEFORE the run and `toRecording()` after it; a ' +
'bundle assembled by hand after the fact cannot contain one.');
}
if (!Array.isArray(snapshot.commitLog) || snapshot.executionTree === undefined) {
const missing = [
Array.isArray(snapshot.commitLog) ? undefined : 'commitLog',
snapshot.executionTree === undefined ? 'executionTree' : undefined,
]
.filter(Boolean)
.join(' and ');
throw new Error(`openRecording: the snapshot in this recording is missing ${missing} — the trace tools ` +
'navigate a run by what each step wrote (commitLog) and what each step was ' +
'(executionTree), so neither is optional. This usually means the object handed in is ' +
'not a footprintjs run snapshot (a partial log, a UI view model, an older format). ' +
'The producer is `recordRun(agent).toRecording()`, whose `snapshot` is ' +
'`agent.getLastSnapshot()` verbatim.');
}
const narrative = narrativeFrom(snapshot);
const events = recording?.events;
return {
snapshot,
// controlDeps is deliberately absent — see the header. The toolpack
// already says "⚠ control edges unavailable" rather than pretending.
...(narrative !== undefined && { narrative }),
...(Array.isArray(events) && { events: events }),
};
}
//# sourceMappingURL=openRecording.js.map