UNPKG

agentfootprint

Version:

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

147 lines • 7.33 kB
"use strict"; /** * compactionMeter — the instrument a window strategy is allowed to read. * * Pattern: CombinedRecorder (Convention 1 — single purpose: measurement), * with a handle the window stage reads MID-run. That mid-run read is * deliberate and has precedent: the causal-evidence recorder is * attached inline for the same reason (the memory write stage * consumes its accumulators before the run ends). * Role: recorders/core layer. Attached ONLY when a window strategy is * configured (`.window()` / `.compaction()`), and always INLINE — a * window decision cannot ride one beat behind the loop it is * deciding for. * Emits: nothing. It only measures. * * It answers two questions a stage function cannot answer for itself: * * 1. "How big was the last window, as the PROVIDER counted it?" — from the * `stream.llm_end` usage the adapter reported. Counted, not guessed. Fed * by the agent's REAL-value event path (`noteLlmEnd`), never by a served * event: the window decides on this number, so a redaction policy can * never take it away (`src/redaction/runRedaction.ts`). * 2. "Which stage put each message in the window?" — from the * `runtimeStageId` on every write to `history`. A stage function has no * access to its own runtimeStageId (footprintjs exposes no such scope * method), so the recorder channel is the ONLY honest source for the * `removedStageIds` a strategy has to name. That channel is a RECORD: * under a redaction policy that selects `history` whole, footprintjs * serves the write masked, the meter cannot see the window's length, and * an eviction's provenance reads as unknown (`removedStageIds` names none, * `survivalMs` is 0 — the documented "birth unknown"). The record of a * masked key loses its shape; the window's decision does not change. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.compactionMeter = void 0; /** * Build the meter. * * Origin tracking assumes the ONE shape the agent's window actually has: * writes either replace the window wholesale (seed, and resume seeding) or * append to it (tool-calls). Growth fills the new tail with the writing * stage's id; a wholesale replacement re-stamps everything with the writer * that produced it — which is exactly true, since that writer is where those * messages entered THIS run. Shrink only ever comes from a window strategy, * and the stage announces it through `rebaseForWindowChange`. */ function compactionMeter(options = {}) { const id = options.id ?? 'compaction-meter'; const key = options.key ?? 'history'; const now = options.now ?? (() => Date.now()); let origins = []; let last; let unmetered = 0; /** Set by `rebaseForWindowChange`; consumed by the very next write to `key`. */ let pendingRebase; return { id, // A reading survives exactly one iteration boundary — the one it was taken // for. Anything older means the newest call did not report, and a number // that is no longer being refreshed is not a measurement any more. lastCall: (currentIteration) => last !== undefined && last.meteredAtIteration >= currentIteration - 1 ? last : undefined, unmeteredSinceLastGood: () => unmetered, origins: () => origins, rebaseForWindowChange(headCount, keptTailCount, insertedAtMs) { pendingRebase = { head: headCount, tail: keptTailCount, ...(insertedAtMs !== undefined && { bornAtMs: insertedAtMs }), }; }, clear() { origins = []; last = undefined; unmetered = 0; pendingRebase = undefined; }, onWrite(event) { if (event.key !== key || !Array.isArray(event.value)) return; const length = event.value.length; const stageId = event.runtimeStageId; const atMs = now(); if (pendingRebase !== undefined) { // The window stage's own write. Anything it INSERTED is born here; // the head and the kept tail carry their original provenance forward, // which is the point — a message the strategy did NOT write must // never end up looking like the strategy wrote it. const head = origins.slice(0, pendingRebase.head); const tail = pendingRebase.tail > 0 ? origins.slice(origins.length - pendingRebase.tail) : []; const inserted = pendingRebase.bornAtMs === undefined ? [] : [{ stageId, bornAtMs: pendingRebase.bornAtMs }]; origins = [...head, ...inserted, ...tail]; pendingRebase = undefined; // Trust the write's own length over our bookkeeping if they disagree. if (origins.length !== length) origins = normalize(origins, length, stageId, atMs); return; } if (length >= origins.length) { for (let i = origins.length; i < length; i++) origins.push({ stageId, bornAtMs: atMs }); return; } // An unannounced shrink (a consumer stage rewriting history). Nothing // is inferred: re-stamp to the writer that actually produced this array. origins = normalize(origins, length, stageId, atMs); }, noteLlmEnd(raw) { const payload = raw !== null && typeof raw === 'object' ? raw : undefined; const usage = payload?.usage; if (typeof usage?.input !== 'number' || typeof usage?.output !== 'number') { // A call happened and reported nothing usable. The PREVIOUS reading is // deliberately left in place rather than deleted — `lastCall` decides // whether it is still current, and it will not be for long. What we do // record is that the provider answered without counting, so the stage // can say so once instead of the run going quiet. unmetered += 1; return; } // No iteration on the payload is a shape this build does not produce // (`LLMEndPayload.iteration` is required), but a reading that cannot say // WHEN it was taken can never be checked for staleness — so it is not // taken. Counting it as unmetered is the honest answer. if (typeof payload?.iteration !== 'number') { unmetered += 1; return; } unmetered = 0; last = { input: usage.input, output: usage.output, meteredAtIteration: payload.iteration, }; }, }; } exports.compactionMeter = compactionMeter; function normalize(current, length, stageId, atMs) { const next = []; for (let i = 0; i < length; i++) next.push(current[i] ?? { stageId, bornAtMs: atMs }); return next; } //# sourceMappingURL=CompactionMeter.js.map