agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
134 lines (133 loc) • 7.05 kB
TypeScript
/**
* context-ledger/types.ts — the bookkeeper's vocabulary.
*
* WHY THIS LIBRARY EXISTS: context engineering fails in one predictable
* direction — "include everything to be safe". Every injection, skill and
* tool schema costs tokens EVERY turn, whether or not it ever mattered. The
* ledger keeps score across runs: which pieces EARNED their tokens (were
* called, activated, or sat on the answer's dependency slice) and which
* never did. Its rows feed the L2 gates (gatedTools predicate, skill-graph
* EntryScorer, injection demotion) so future turns include less — the
* mechanism that makes lesser models viable.
*
* HONESTY (the discipline everything here inherits):
* - Every counter is a STRUCTURAL fact from the run's own commit log —
* offers are recorded commits, uses are recorded calls/activations/slice
* membership. Nothing is inferred from model internals. Under an agent's
* `redact`, a final value the log keeps out is read from the run's live
* end state, and a kind nothing can answer for is left UNMETERED for the
* run ({@link RecordedRun.unmetered}) — a placeholder never counts as
* "not used".
* - Each kind's `used` definition is explicit ({@link UsedSignal}) and rides
* every count — a consumer can always see WHY a piece counted as used.
* - Slice membership is slot-granular (all injections sharing a slot share
* its write) — the signal name says so: `'answer-slice(slot)'`.
* - `approxTokens*` is a serialized-length estimate (JSON chars ÷ 4), named
* so nobody mistakes it for a tokenizer count.
* - The ledger never claims causation. `earnRate` is bookkeeping;
* ablation (context-bisect) can upgrade individual claims when you pay
* for it.
*/
import type { ValueBasis } from 'foottrace';
/** What kind of context piece a row tracks. Skills are injections with
* flavor 'skill' — split out because their `used` signal differs. */
export type PieceKind = 'injection' | 'skill' | 'tool';
/**
* The per-kind structural definition of "used" — explicit, never blended:
* - `'tool-called'` — an assistant message actually called the tool.
* - `'skill-activated'` — the run activated the skill
* (`activatedInjectionIds`).
* - `'answer-slice(slot)'` — the injection's SLOT write sits on the final
* answer's backward dependency slice
* (slot-granular by construction).
*/
export type UsedSignal = 'tool-called' | 'skill-activated' | 'answer-slice(slot)';
/** One context piece's accumulated bookkeeping across recorded runs. */
export interface LedgerRow {
/** Injection/skill id, or tool name. Unique within its kind. */
readonly id: string;
readonly kind: PieceKind;
/** Offer events — once per iteration the piece was in context, across runs. */
readonly offered: number;
/** Serialized-length token ESTIMATE spent on those offers (chars ÷ 4). */
readonly approxTokensSpent: number;
/** Structural uses (per-kind definition — see breakdown in usedVia). */
readonly used: number;
/** Which signal(s) produced the `used` count, and how many each. */
readonly usedVia: Readonly<Partial<Record<UsedSignal, number>>>;
/** Distinct recorded runs in which the piece was offered at least once. */
readonly runsSeen: number;
/**
* Outcome labels of runs where the piece was OFFERED (label → run count).
* Labels are consumer-supplied via {@link ContextLedger.recordOutcome}
* (thumbs, eval scores bucketed by the consumer, triage verdicts).
* Presence-correlation bookkeeping — NOT a causal claim.
*/
readonly outcomes: Readonly<Record<string, number>>;
/** used ÷ offered (0 when never offered). THE headline number. */
readonly earnRate: number;
}
/** What `recordRun` extracted from one run — returned for inspection/tests. */
export interface RecordedRun {
/** Handle for {@link ContextLedger.recordOutcome}. */
readonly runRef: string;
/** Piece ids offered in this run (kind-qualified internal keys). */
readonly offeredPieces: readonly string[];
/** True when the answer slice could be computed (finalContent + reads). */
readonly sliceAvailable: boolean;
/**
* The reason codes behind any NON-exact answer the ledger counted from, per
* state key — the union of footprintjs's `commitValueAtWithBasis` /
* `findLastWriterWithBasis` `basis` over every read of that key in this run
* (`'nested-rows'`, `'from-initial-state'`, `'redacted'`, `'deleted'`,
* `'never-written'`). Each code's sentence is `HONESTY_CODES[code]` on
* `foottrace`. ABSENT when every answer was exact.
*/
readonly basis?: Readonly<Record<string, readonly ValueBasis[]>>;
/**
* The kinds this run was NOT metered for: a read their offers or uses come
* from rests on a redaction (`basis` names the key as `'redacted'`) that no
* live state answered — an agent's `redact` kept it out of the record and
* the source was a snapshot, or it is a value per call (what each call was
* offered lives only in the log). Neither the kind's offers nor its uses are
* counted for this run, so a gate never demotes a piece on uses the record
* could not show. ABSENT when every kind was metered (always, without a policy).
*/
readonly unmetered?: readonly PieceKind[];
}
/** JSON-safe persisted shape (consumer owns storage). */
export interface LedgerJSON {
readonly version: 1;
readonly rows: readonly LedgerRow[];
readonly runsRecorded: number;
}
/** Minimal runner surface the ledger reads (any agentfootprint Runner). */
export interface RunnerLike {
getLastSnapshot(): unknown;
}
export interface ContextLedger {
/**
* Ingest one FINISHED run: walk its commit log for offers (activeInjections
* / dynamicToolSchemas per iteration), uses (assistant tool calls,
* activatedInjectionIds, the final answer's dependency slice), and
* accumulate. Accepts a runner or a snapshot. A runner of this library is
* read LIVE (its live snapshot and end state — never its served record);
* any other runner through `getLastSnapshot()`.
*/
recordRun(source: RunnerLike | unknown): RecordedRun | undefined;
/**
* Label a recorded run's outcome ('good'/'bad'/any consumer label —
* thumbs, bucketed eval score, triage verdict). Defaults to the most
* recently recorded run. Increments the label for every piece OFFERED in
* that run.
*/
recordOutcome(label: string, runRef?: string): boolean;
/** All rows, worst earnRate first (the review order that matters). */
rows(): readonly LedgerRow[];
/** One row (by kind + id). */
row(kind: PieceKind, id: string): LedgerRow | undefined;
/** JSON-safe export (consumer persists). */
exportJSON(): LedgerJSON;
/** Merge a previously exported ledger in (counts add). */
importJSON(json: LedgerJSON): void;
}