UNPKG

agentfootprint

Version:

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

124 lines (123 loc) • 7.57 kB
/** * receiptDigests — what a run's receipts have already hashed, so each piece of * a request is hashed ONCE per run instead of once per call. * * Role: Memo. A plain data structure the receipt mint (`receipt.ts` · * `buildReceipt`) reads and fills; it hashes nothing itself, reads no * scope and emits nothing. * Reads: what the mint hands it. * Emits: N/A. * * ── WHY IT EXISTS ─────────────────────────────────────────────────────────── * An agent's request at call k is the request at call k−1 plus what the loop * appended — and the mint used to hash every message of it again, on every * call. Measured on an agent run with 1,000-row tool results: SHA-256 input * grew with the square of the iteration count, and the mint was the largest * single cost of the run (63% of its CPU at 40 iterations, under a caching * provider). The digests themselves never needed to change: a receipt's hashes * are functions of the bytes, so a message hashed at call 3 has the same hash * at call 30. This keeps them. * * ── THE TWO LOOKUPS ───────────────────────────────────────────────────────── * BY PREIMAGE `hash(preimage)` — the salted hashes of the per-call pieces * that are not messages (system text, system pieces, tool * schemas), keyed by the exact string that was hashed. Exact by * construction: a hash is a function of its preimage, so a hit * returns what recomputing would return. * BY OBJECT `message(m)` — what was derived from one message OBJECT: its * entry hash, its transform-chain digest, whether it serializes, * and its request measurement (`requestMeasurement.ts`), so a * message the run has already seen costs no string building at * all. It keeps DIGESTS and counts, never a copy of the message's * text — a 1,000-row tool result is not held twice. Valid only while * the object's own fields are the values they were: every field * is compared by identity on each hit, and a mismatch derives * again. Committed state is never edited in place (footprintjs's * immutable-after-swap law), so a hit is the common case — the * agent's history hands the mint the same message objects call * after call. * * ── WHAT IT DOES NOT CHANGE ───────────────────────────────────────────────── * Every hash a receipt carries is the hash it carried before this memo existed: * same preimage, same salt, same digest. A receipt minted with the memo and one * minted without it are byte-identical, and so is the request measurement — * pinned by test/core/agent/receipt/receipt-incremental.test.ts. * * The one assumption a hit makes: a message's NESTED values (`toolCalls`, * `args`, `thinkingBlocks`) are not edited in place while the message object * is reused. The agent's history is committed state, which is never edited in * place; a caller that hands the mint its own objects and then mutates one * deep inside must hand a new object instead. * * ── LIFETIME ──────────────────────────────────────────────────────────────── * One memo per run, bound to the run id (the salt). `forRun` hands back the * current run's memo, and a different run id starts a fresh one — so a reused * agent never carries one run's strings into the next. * * Within a run, the two string memos (BY PREIMAGE, and the transform chain's * links) keep TWO CALLS' worth — the call being minted and the one before it. * Each `forRun` is one mint, and it rotates them. A piece that repeats from one * call to the next (a system prompt, a tool schema, the chain's prefix) is * found; a piece that changes every call (a system prompt with the time in it) * is not kept past the next call. What the memo holds is bounded by two * requests' worth — their non-message pieces and one chain link per message — * never by how many calls the run made. The BY OBJECT memo is weak — it lives * as long as the message objects do. * * ── TOTAL ─────────────────────────────────────────────────────────────────── * A receipt is bookkeeping and must never stop a run. A message whose fields * cannot be read (a throwing getter on an object a caller handed in) is not * memoized: `message()` hands back a fresh, unkept record and the mint derives * everything from the message as it would with no memo at all. */ import type { MessageMeasure, MessageMeasureCache } from './requestMeasurement.js'; /** What one message object has already been turned into. */ export interface MessageDigests { /** `messageDigestInput(m)` hashed — the receipt's `messages.entries[].hash`. */ entryHash?: string; /** * Whether `stableJson([m])` — the message as one array element — produced a * value. Read by the cache comparison, which is `'unknown'` when a message * cannot be serialized. */ serializable?: boolean; /** `H(stableJson([m]) ?? UNSERIALIZABLE)` — the transform chain's digest of * this message (`receipt.ts` · `transformHashOf`). Kept as the digest, never * as the JSON, so the memo holds no copy of a message's text. */ elementDigest?: string; /** The message as `measureRequest` measured it at a request's * `messages[i]` (`requestMeasurement.ts` · `MessageMeasure`). */ measure?: MessageMeasure; } /** One run's memo. */ export interface RunDigests extends MessageMeasureCache { /** The run id every hash in this memo is salted with. */ readonly runId: string; /** The salted hash of `preimage`, computed by `compute` the first time only. */ hash(preimage: string, compute: () => string): string; /** What has been derived from `message` while its fields are unchanged. */ message(message: object): MessageDigests; /** The chain link after `previous` and `digest` — see `receipt.ts` · * `transformHashOf`. Computed by `compute` the first time only. */ link(previous: string, digest: string, compute: () => string): string; /** @internal Strings the two string memos hold right now — pinned by * test/core/agent/receipt/receipt-incremental.test.ts (bounded by two calls). */ retained(): number; } /** * A memo the receipt mint keeps across the calls of a run. Create one per * agent; it rebinds itself when the run id changes. * * @example * ```ts * const digests = createReceiptDigests(); * buildReceipt(callOne, digests); // hashes every piece * buildReceipt(callTwo, digests); // hashes only what call two added * ``` */ export interface ReceiptDigests { /** The memo for `runId` — the current one, or a fresh one for a new run. */ forRun(runId: string): RunDigests; } /** Start an empty memo. See {@link ReceiptDigests}. */ export declare function createReceiptDigests(): ReceiptDigests;