agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
180 lines (179 loc) • 9.64 kB
TypeScript
/**
* recordingPack — a recording with every repeated value stored ONCE.
*
* Role: Codec. `packRecording` turns a recording (`recordRun` ·
* `toRecording()`) into plain JSON data where a value the recording
* holds in more than one place is written once and referred to by its
* index everywhere else; `unpackRecording` turns it back. Nothing here
* runs, records or emits.
* Reads: its argument.
* Emits: N/A.
*
* ── WHY ─────────────────────────────────────────────────────────────────────
* An agent's history is IN a recording once per place that saw it: each
* iteration's slot subflows are seeded with the whole conversation, the
* boundary log keeps each subflow's input and output, `iteration_end` carries
* the history, every call re-announces the pieces in its context. So
* iteration k writes the k results before it again, and plain JSON grows
* with K²·R (K iterations, R-row results). Measured on an agent run with
* 1,000-row tool results: 67 MB at 10 iterations, 808 MB at 40, and past
* JSON's own string limit before 80 — the first result appeared 838 times at
* K = 40. Each of those copies is the same value; packed, each is written
* once and the recording grows with K·R.
*
* ── THE FORMAT ──────────────────────────────────────────────────────────────
* { format: 'agentfootprint.recording.packed.v1',
* values: [ <value 0>, <value 1>, … ], each written once
* recording: { snapshot, events, structure } with refs in place }
*
* A REF is the one-key object `{ "$af:ref": n }` — "values[n] goes here". A
* value may hold refs to other values. A value is pooled when it occurs at
* least twice and its JSON is at least {@link MIN_POOLED_CHARS} long; a value
* that occurs once stays where it is, so a run with no repetition packs to
* its plain JSON plus the envelope.
*
* ESCAPING keeps the mapping injective: an object of the recording's own that
* has a key `"$af:ref"` or `"$af:esc"` is written as `{ "$af:esc": <it> }`, and
* the reader takes what is inside literally. No recording can be mistaken for
* a reference — pinned by test/recorders/observability/recordingPack.test.ts.
*
* ── THE LAW ─────────────────────────────────────────────────────────────────
* `JSON.stringify(unpackRecording(JSON.parse(JSON.stringify(packRecording(r)))))`
* is `toWireJson(r)`, byte for byte: packing follows `JSON.stringify` (each
* `toJSON`, dropped `undefined`s and functions, `null` for non-finite numbers,
* a cycle or a `BigInt` throws) and the wire rule for Errors
* (`lib/wireJson.ts`) — the bytes a plain recording is minted as. Values are
* read twice (once to count, once to write), so a getter that answers
* differently on its second read decides what is written; a recording is
* plain data, and reads the same both times.
*
* ── READING ONE ─────────────────────────────────────────────────────────────
* `unpackRecording(value)` takes either shape: a packed recording is expanded,
* anything else is returned as it is — so a reader that calls it reads every
* recording ever minted, plain or packed. A pooled value expands to ONE object
* shared by every place that referred to it (that is what keeps the expanded
* recording linear in memory too): treat a recording as read-only, as every
* viewer does. A `format` from this family that is not v1 is refused by name,
* never half-read.
*/
import type { Recording } from './recordRun.js';
/** The format marker of a packed recording. */
export declare const PACKED_RECORDING_FORMAT = "agentfootprint.recording.packed.v1";
/**
* The smallest JSON length a repeated value must have to be pooled. A
* reference costs about 15 characters, so pooling anything shorter would make
* a recording longer, not shorter.
*/
export declare const MIN_POOLED_CHARS = 64;
/** A recording with every repeated value written once — see the module header. */
export interface PackedRecording {
readonly format: typeof PACKED_RECORDING_FORMAT;
/** Every pooled value, each written once; referred to by its index. */
readonly values: readonly unknown[];
/** The recording, with each pooled value replaced by a reference. */
readonly recording: unknown;
}
/** A packed recording this reader cannot expand — named, never half-read. */
export declare class PackedRecordingError extends Error {
constructor(message: string);
}
/**
* A packed recording whose PLAIN form is larger than the reader agreed to take
* ({@link UnpackRecordingOptions.maxBytes}) — refused before anything is
* expanded. Packed, a recording can stand for far more JSON than it holds (a
* value referred to ten times, by values each referred to ten times, …), so
* its own byte count bounds nothing a tree walk does with it; this does.
*/
export declare class PackedRecordingTooLargeError extends PackedRecordingError {
/** The bound that was exceeded, in UTF-8 bytes of the plain recording's JSON. */
readonly maxBytes: number;
constructor(maxBytes: number);
}
/**
* The default bound on what a packed recording may expand to: 512 MiB of JSON —
* about the largest plain recording a JavaScript string can hold. Reading a
* packed recording through the default is never more work than reading its
* plain twin could have been.
*/
export declare const DEFAULT_UNPACK_MAX_BYTES: number;
/** How much a reader of {@link unpackRecording} agrees to expand. */
export interface UnpackRecordingOptions {
/**
* The largest PLAIN recording this read may stand for, in UTF-8 bytes of its
* JSON — exactly the size the same recording minted plain would have.
* Checked over the PACKED form, in time proportional to the packed size,
* before anything is expanded; a packed recording over it is refused with
* {@link PackedRecordingTooLargeError}. Default
* {@link DEFAULT_UNPACK_MAX_BYTES}. `Infinity` takes anything — only for a
* reader that trusts the recording and never walks it as a tree.
*/
readonly maxBytes?: number;
}
/** Is `value` a packed recording of the format this reader expands? */
export declare function isPackedRecording(value: unknown): value is PackedRecording;
/**
* Pack a recording so every repeated value is stored once.
*
* @throws TypeError where `JSON.stringify` throws — a cycle or a `BigInt`.
*
* @example
* ```ts
* import { packRecording, recordRun } from 'agentfootprint/observe';
*
* const recorder = recordRun(agent);
* await agent.run({ message });
* fs.writeFileSync('run.json', JSON.stringify(packRecording(recorder.toRecording())));
* ```
*/
export declare function packRecording(recording: Recording): PackedRecording;
/**
* @internal {@link packRecording}, counting its work: `work.members` grows by
* one for every member value the packer reads, on both passes — the operation
* count test/recorders/observability/recordingPack.test.ts pins (4× the
* iterations, ~4× the reads). Not on any barrel.
*/
export declare function packCounted(recording: Recording, work: {
members: number;
} | undefined): PackedRecording;
/**
* Read a recording that may be packed: a packed recording is expanded, any
* other value is returned as it is — so one call reads every recording, plain
* or packed. See the module header for what "expanded" shares.
*
* BOUNDED. A packed recording is expanded only when its plain form is at most
* `maxBytes` (default {@link DEFAULT_UNPACK_MAX_BYTES}) — measured over the
* packed form, each pooled value sized once, before anything is built. A plain
* recording is returned as it is: its size is the size of the text it was
* parsed from, which its reader bounds before parsing.
*
* @throws PackedRecordingTooLargeError when the plain form is over `maxBytes`.
* @throws PackedRecordingError for a packed format other than v1, or a packed
* value whose references do not resolve.
*
* @example
* ```ts
* import { unpackRecording } from 'agentfootprint/observe';
* import { observeRecording } from 'agentfootprint-lens';
*
* const recording = unpackRecording(JSON.parse(text)); // plain or packed
* const { recorder, runner } = observeRecording(recording);
*
* // A host that serves recordings to others bounds what one may stand for:
* unpackRecording(JSON.parse(text), { maxBytes: 16 * 1024 * 1024 });
* ```
*/
export declare function unpackRecording(input: unknown, options?: UnpackRecordingOptions): Recording;
/**
* @internal {@link unpackRecording}, counting its work: `work.measured` grows
* by one for every packed node the size check reads, `work.decoded` for every
* node expanded — the operation counts
* test/recorders/observability/recordingPack.test.ts pins (a refused
* amplification reads the packed form once and expands nothing). Not on any
* barrel.
*/
export declare function unpackCounted(input: unknown, options: UnpackRecordingOptions, work: UnpackWork | undefined): Recording;
/** @internal What {@link unpackCounted} counts. */
export interface UnpackWork {
measured: number;
decoded: number;
}