agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
181 lines • 9.88 kB
JavaScript
;
/**
* runbook/recording — the inner chart's own recording, filed beside the walk.
*
* Pattern: one pure assembler (`chartRecordingOf`) + one guarded side effect
* (`mintChartRecording`, which files the artifact and NEVER fails the
* answer) — `walk.ts`'s shape, deliberately, because it is the same
* law about a different payload.
* Role: core/runbook. Opt-in via `walk: { recording }`.
* Emits: nothing (the artifact capability emits its own `artifacts.*`).
*
* WHY THIS EXISTS AT ALL
* ──────────────────────
* The walk is a ROW PROJECTION — eight declared columns per execution step,
* values off by construction. It is the right thing to hand a model and the
* wrong thing to hand a renderer: a step graph cannot be inferred from
* sentences about steps, and a consumer that tried would be guessing at edges.
* The one piece that makes a walk drawable is `structure`, the chart's
* build-time graph, which a finished run does not leave behind and no snapshot
* carries. That is the whole gap this closes, and it closes it with the shape
* the viewer side has consumed since 8.x: `{ snapshot, events, structure }`.
*
* THREE LAWS, and each one is a refusal somewhere
* ───────────────────────────────────────────────
* REDACTED AT THE SOURCE. The snapshot comes from `getSnapshot({ redact:
* true })` — the parallel redacted mirror — never the raw working memory.
* Whatever `redact` means for the walk means the same here, because it is
* the same policy read at the same moment; with no policy configured the
* flag is a documented no-op and costs nothing.
* REFUSED, NEVER TRUNCATED. Over the byte ceiling the mint does not happen
* and the descriptor says so with both numbers. A walk can be projected
* because its rows are independently meaningful; a recording is one
* bundle, and half of one draws a picture nobody can check.
* THE ABSENCE IS SPOKEN. No store, an over-size refusal, an unserializable
* snapshot, a store that 500s — every one of them costs the REF and
* returns a `recording_note` naming the reason. A failed mint has never
* been allowed to cost the answer, and this is not the feature that
* changes that.
*
* `events` IS EMPTY, BY CONSTRUCTION AND ON PURPOSE
* ────────────────────────────────────────────────
* `events` is the typed *agentfootprint* stream, and it is fired by an Agent
* turn. What ran here is a footprintjs chart on its own executor: it fires no
* agentfootprint events, so there are none to record and an empty array is the
* honest count rather than a shortfall. The three keys are all present, which
* is what `observeRecording()` reads; the walk's own story rides `snapshot`,
* where the narrative recorder's data already lives. The note says this out
* loud so nobody reads the empty array as a dropped stream.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.mintChartRecording = exports.chartRecordingOf = exports.resolveRecordingPolicy = exports.DEFAULT_RECORDING_MAX_BYTES = void 0;
const recordingArtifact_js_1 = require("../../artifacts/recordingArtifact.js");
const payload_js_1 = require("../../artifacts/payload.js");
/**
* The default size ceiling for a filed chart recording — 5,000,000 bytes.
*
* Chosen against measured sizes rather than taste. A walk of a real triage run
* is tens of kilobytes; this package's own field measurement of a full
* `recordRun` bundle (one retrieval turn, vectors included) was 2.76 MB. Five
* million bytes clears the realistic runbook by more than an order of
* magnitude and still refuses the pathological one — a fleet sweep whose
* commit log carries a row per subject per stage — BEFORE it lands in somebody
* else's store under a retention budget they sized for tickets.
*
* It is a declared number, not a magic one: it is named on the option, printed
* in the refusal, and raised by whoever decides the whole bundle is worth it.
*/
exports.DEFAULT_RECORDING_MAX_BYTES = 5_000_000;
/**
* Normalize `walk: { recording }` into a policy, or `undefined` for OFF.
*
* `undefined` and `false` are both off, and off means off: the caller must not
* take a second snapshot, measure anything, or touch the store.
*/
function resolveRecordingPolicy(recording) {
if (recording === undefined || recording === false)
return undefined;
if (recording === true)
return { maxBytes: exports.DEFAULT_RECORDING_MAX_BYTES };
return {
maxBytes: recording.maxBytes ?? exports.DEFAULT_RECORDING_MAX_BYTES,
...(recording.label !== undefined && { label: recording.label }),
};
}
exports.resolveRecordingPolicy = resolveRecordingPolicy;
/**
* Assemble the `{ snapshot, events, structure }` bundle for one inner chart
* run. Pure — no store, no clock, no events.
*
* @param redactedSnapshot the run snapshot read through `servableSnapshot`
* (the redacted mirror exactly as footprintjs serves it — each subflow's
* state is the subflow's own mirror since footprintjs 9.20.0). Taking the
* raw one here would file whatever the chart wrote under a policy that was
* supposed to scrub it, which is the one mistake this whole module is
* arranged to prevent.
* @param structure the chart's `buildTimeStructure` — the only route to a
* drawable graph, and the reason this bundle beats the row projection.
*/
function chartRecordingOf(redactedSnapshot, structure) {
return { snapshot: redactedSnapshot, events: [], structure };
}
exports.chartRecordingOf = chartRecordingOf;
/** The standing sentence: what a FILED recording contains that the walk's row
* projection does not. This is the sentence that justifies the opt-in. */
const RECORDING_NOTE = "The inner chart's own recording — `{ snapshot, events, structure }`, the shape a viewer " +
'mounts — so this walk can be drawn as the flowchart it actually ran. It carries what the ' +
'row projection cannot: the chart STRUCTURE (the only route to a drawable graph; a finished ' +
"run does not leave it behind), the run's shared state, its whole commit log, and every " +
"attached recorder's data — that is, whatever the chart WROTE, scrubbed by the `redact` " +
'policy of the call this result answers and by nothing else. `events` is empty by ' +
'construction: a chart run is not ' +
'an agent turn and fires no agentfootprint events, so there are none to record.';
/**
* File the inner chart's recording and describe what happened.
*
* Called ONLY when `walk: { recording }` asked for one, and it never throws:
* every failure lands in `recording_note` and the answer travels unchanged.
*/
async function mintChartRecording(ctx, recording, facts) {
if (!ctx.hasArtifacts) {
return {
recording_note: RECORDING_NOTE +
' No artifact store is attached to this run, so the recording was assembled but not ' +
'filed — attach one with `Agent.create({ ..., artifacts })` to get a ref here.',
};
}
// Serialize FIRST, then measure, then put: the mint is where an
// unserializable snapshot must fail (the `recordingPutInput` law), and the
// ceiling is judged on the bytes that would actually be stored rather than
// on a guess about them.
let input;
try {
input = (0, recordingArtifact_js_1.recordingPutInput)(recording, {
toolCallId: facts.toolCallId,
...(facts.runId !== undefined && { runId: facts.runId }),
...(facts.policy.label !== undefined
? { label: facts.policy.label }
: { label: `${facts.toolName} recording` }),
});
}
catch (err) {
return {
recording_note: RECORDING_NOTE +
` This one could not be serialized (${err instanceof Error ? err.message : String(err)}), ` +
'so it was not filed — a recording JSON cannot carry could not cross any wire either. ' +
'The failure cost the ref, never the answer.',
};
}
const bytes = (0, payload_js_1.measureArtifactBytes)(input.data);
if (bytes > facts.policy.maxBytes) {
return {
recording_bytes: bytes,
recording_note: RECORDING_NOTE +
` This one measured ${bytes} bytes, over the declared \`walk.recording.maxBytes\` ` +
`ceiling of ${facts.policy.maxBytes}, so it was NOT filed. It is refused rather than ` +
'truncated on purpose: the walk can be projected because its rows are independently ' +
'meaningful, but a recording is one bundle — half a commit log under a whole chart ' +
'draws a picture nobody can check. Raise `walk.recording.maxBytes` if the whole thing ' +
'is what you need. The walk above is unaffected.',
};
}
try {
const meta = await ctx.artifacts.put(input);
return {
recording_ref: meta.ref,
recording_kind: recordingArtifact_js_1.RECORDING_ARTIFACT_KIND,
recording_bytes: meta.bytes,
recording_note: RECORDING_NOTE,
};
}
catch (err) {
return {
recording_bytes: bytes,
recording_note: RECORDING_NOTE +
` This one could not be filed (${err instanceof Error ? err.message : String(err)}) — ` +
'the mint failure cost the ref, never the answer.',
};
}
}
exports.mintChartRecording = mintChartRecording;
//# sourceMappingURL=recording.js.map