agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
166 lines • 8.29 kB
JavaScript
/**
* 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.
*/
// FOLD · the one owner of "already hashed this run": buildReceipt reads it, and
// nothing else re-derives a receipt digest across calls
// detached: no — it holds the message objects of the run as WeakMap keys (never
// keeps them alive) and the preimage strings of the last two calls.
/** Start an empty memo. See {@link ReceiptDigests}. */
export function createReceiptDigests() {
let current;
return {
forRun(runId) {
if (current === undefined || current.runId !== runId)
current = runDigests(runId);
else
current.nextCall();
return current;
},
};
}
function runDigests(runId) {
const byPreimage = twoCalls();
const links = twoCalls();
const byObject = new WeakMap();
const message = (value) => {
try {
const keys = Object.keys(value);
const held = byObject.get(value);
if (held !== undefined && sameFields(held, value, keys))
return held.digests;
const entry = {
keys,
values: keys.map((key) => value[key]),
digests: {},
};
byObject.set(value, entry);
return entry.digests;
}
catch {
return {}; // unreadable: derive everything afresh, keep nothing (see TOTAL)
}
};
return {
runId,
hash: (preimage, compute) => byPreimage.get(preimage, compute),
message,
measureOf: (value) => message(value).measure,
keep: (value, measure) => {
message(value).measure = measure;
},
link: (previous, digest, compute) => links.get(`${previous}\u001F${digest}`, compute),
retained: () => byPreimage.size() + links.size(),
nextCall: () => {
byPreimage.rotate();
links.rotate();
},
};
}
/**
* A string memo that keeps the current call's entries and the previous call's,
* nothing older — see LIFETIME. A hit in the previous call's entries is carried
* into the current one.
*/
function twoCalls() {
let current = new Map();
let previous = new Map();
return {
get(key, compute) {
let value = current.get(key);
if (value === undefined) {
value = previous.get(key) ?? compute();
current.set(key, value);
}
return value;
},
rotate() {
previous = current;
current = new Map();
},
size: () => current.size + previous.size,
};
}
/** Are `message`'s own fields still exactly the values `held` saw? Identity per
* field — a field that is a new object is a different field, even when equal. */
function sameFields(held, message, keys) {
if (keys.length !== held.keys.length)
return false;
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (key !== held.keys[i])
return false;
if (message[key] !== held.values[i])
return false;
}
return true;
}
//# sourceMappingURL=receiptDigests.js.map