UNPKG

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
/** * 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; }