agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
126 lines (125 loc) • 6.99 kB
TypeScript
/**
* 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.
*/
import type { Recording } from '../../recorders/observability/recordRun.js';
import type { ToolExecutionContext } from '../../tools/tools.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.
*/
export declare const DEFAULT_RECORDING_MAX_BYTES = 5000000;
/** The resolved recording policy — what `walk: { recording }` normalizes to. */
export interface ResolvedRecordingPolicy {
readonly maxBytes: number;
readonly label?: string;
}
/** What the recording mint needs from the call. */
export interface ChartRecordingMintFacts {
/** The runbook tool's name — names the default label. */
readonly toolName: string;
/** The OUTER tool call — `origin.toolCallId`, the join a consumer uses to
* pair this recording with the tool call that produced it (and with the
* walk minted beside it, which carries the same key). */
readonly toolCallId: string;
/** The outer run, when there is one — `origin.runId`. */
readonly runId?: string;
/** The resolved policy from `walk: { recording }`. */
readonly policy: ResolvedRecordingPolicy;
}
/**
* The recording fields spread onto the `WalkDescriptor`. `recording_note` is
* always present — this type exists to make "the absence is stated" a thing
* the compiler enforces rather than a thing a reviewer has to notice.
*/
export interface RecordingDescriptorFields {
readonly recording_ref?: string;
readonly recording_kind?: string;
readonly recording_bytes?: number;
readonly recording_note: string;
}
/**
* 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.
*/
export declare function resolveRecordingPolicy(recording: boolean | {
readonly label?: string;
readonly maxBytes?: number;
} | undefined): ResolvedRecordingPolicy | undefined;
/**
* 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.
*/
export declare function chartRecordingOf(redactedSnapshot: unknown, structure: unknown): Recording;
/**
* 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.
*/
export declare function mintChartRecording(ctx: ToolExecutionContext, recording: Recording, facts: ChartRecordingMintFacts): Promise<RecordingDescriptorFields>;