UNPKG

agentfootprint

Version:

The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.

144 lines (143 loc) • 7.45 kB
/** * scripts/rows — the `script` row: built from one call, filed through ONE * writer, read back per turn, and bounded. * * Pattern: Map + one writer (the `findings/ledger.ts` · `recordFindings` * twin for this kind): `scriptRowFor` builds, `recordScriptRow` is * the only thing that writes a `script` row, `scriptRowsOfTurn` is * the one reader the match and the attachment share. * Role: core/ layer leaf. Called by the dispatch loop (`stages/toolCalls.ts`) * at each place a call of a declared code-running tool pauses or * settles; read by the Route decider's judge and the final branch. * Emits: N/A — the row is state; the runner's facts already rode * `agentfootprint.tools.code_run`, which never carries the code. * * ## One unit, and the bounds (design § 4.4) * * UTF-8 payload bytes of the program, measured at record time * (`lib/utf8.ts` · `utf8Length`) — never characters: 32,768 characters of a * 3-byte character are 98,304 bytes. * * - **Per script.** A program over {@link MAX_SCRIPT_BYTES} keeps a row with * `bytes`, `lines` and the outcome, and no `text`: it can be neither matched * nor attached. * - **Per turn.** {@link MAX_SCRIPT_ROWS} calls keep a row; a further call of * the turn files none and is counted in `AgentState.scriptsDropped`. * - **Eligible text.** A row's `text` lives for its own turn: the seed stage's * restore drops it from every earlier turn's row (`withoutEarlierScriptText`), * so the text the ledger holds is at most 16 × 32,768 bytes = 512 KiB before * serialization, whatever the conversation's length. That bounds eligible * text only — prior turns' metadata, history and the answers that carried an * attached script are outside it. * * ## Append only, last wins * * A call PAUSED before it settles (a check-in, a consent, an ask, a tool that * asked a person) files a `submitted` row at the pause, WITHOUT its text: the * settled row filed when the call resumes is the one that carries it, so a * program is never held twice. A reader takes the LAST row per call of the * turn (the ledger's own law). A row that never settles stays `submitted`. */ import type { ToolScript } from '../../../tools/script.js'; import type { FindingsLedger } from '../findings/types.js'; import { type CallSettlement, type RunnerOutcome } from './label.js'; import type { ScriptRow } from './types.js'; /** A program longer than this (UTF-8 bytes) keeps a row with no text. Design § 4.4. */ export declare const MAX_SCRIPT_BYTES = 32768; /** Calls per turn that keep a row; the rest are counted. Design § 4.4. */ export declare const MAX_SCRIPT_ROWS = 16; /** Lines of a program: LF-separated, `0` for an empty one. */ export declare function linesOf(program: string): number; /** One call of a declared code-running tool, as the dispatch loop knows it. */ export interface ScriptCall { readonly script: ToolScript; /** The name the model called. */ readonly tool: string; readonly toolCallId: string; readonly iteration: number; /** `AgentState.turnNumber`. */ readonly turn: number; /** * The declared argument's VALUE as the model proposed it — read once, as * the call arrived, before any before-tool rule ran (`programIn`). A string * is immutable: no code that runs later can change what was captured. */ readonly proposed: unknown; /** * The declared argument's value as handed to `execute` — read once, just * before the tool ran, after every rule. Absent: the call never got there, * and the proposal stands. */ readonly submitted?: unknown; readonly settlement: CallSettlement; /** The runner's facts, when the tool filed them. */ readonly facts?: RunnerOutcome; /** The tool's own ceiling replaced the delivered result. */ readonly withheld?: boolean; /** A pause: the row is filed without its text (the settled row carries it). */ readonly paused?: boolean; } /** * The declared argument's value in `args` — the one read a call's program is * captured with. Read it at the boundary it names (as the call arrives; just * before `execute`) and keep the value: never the arguments object, which app * code may edit after it was handed over. */ export declare function programIn(script: ToolScript, args: Readonly<Record<string, unknown>> | undefined): unknown; /** A row before the writer gives it its dispatch identity. */ export type ScriptRowDraft = Omit<ScriptRow, 'dispatch'>; /** * The row for one call, or `undefined` when the call carried no program — the * declared argument is not a string (an argument check refuses such a call; * there is nothing to record as "the program submitted"). */ export declare function scriptRowFor(call: ScriptCall): ScriptRowDraft | undefined; /** The scope surface the writer needs. Structurally a `TypedScope<AgentState>`. */ export interface ScriptScope { findingsLedger?: FindingsLedger; scriptsDropped?: number; } /** * THE ONE WRITER of a `script` row, and the one place a call gets its * DISPATCH identity: the 1-based order of the actual dispatch within its turn. * A call is a dispatch, never a provider id: a later call that reuses an id is * a new dispatch. `options.dispatch` names a dispatch that already has a row * (a paused call, settling): its row joins that dispatch. * * Appended, unless the turn already holds {@link MAX_SCRIPT_ROWS} other * dispatches — then the call files nothing and `scriptsDropped` counts it * (value-conditional: absent until a call is dropped). A dispatch that already * has a row is never dropped, and a PAUSE is never counted (`paused`): its call * is counted once, when it settles. Returns the dispatch the row was filed * under, `undefined` when it was dropped. Synchronous: one read, one write, no * await between. */ export declare function recordScriptRow(scope: ScriptScope, draft: ScriptRowDraft, options?: { readonly paused?: boolean; readonly dispatch?: number; }): number | undefined; /** This turn's calls — the LAST row of each dispatch, in the order those rows were filed. */ export interface TurnScripts { readonly current: readonly ScriptRow[]; /** Calls of EARLIER turns on the ledger: counted, never compared. */ readonly earlier: number; } /** * The one reader of a turn's script rows: the last row per DISPATCH wins, and * a dispatch sits where its last row was filed — execution order, so a call * that settles after a later one ran is read after it. Earlier turns counted. */ export declare function scriptRowsOfTurn(ledger: readonly unknown[] | undefined, turn: number): TurnScripts; /** * The restore door's half of the bound (seed): every `script` row of a turn * BEFORE `turn` keeps its facts — ids, counts, outcome — and loses its `text`. * The same reference when there is nothing to drop, so a ledger without * script text is restored byte for byte. */ export declare function withoutEarlierScriptText<T>(ledger: readonly T[], turn: number): readonly T[]; /** * Whether a value read from a stored checkpoint is a `script` row this library * could have written — the checkpoint door's arm (`core/runCheckpoint.ts`). * An older runtime refuses a checkpoint carrying one. */ export declare function scriptRowIsWellFormed(r: Readonly<Record<string, unknown>>): boolean;