UNPKG

agentfootprint

Version:

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

127 lines (126 loc) • 6.58 kB
/** * 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. */ import type { WriteEvent } from 'footprintjs'; /** What the provider reported for the most recent completed LLM call. */ export interface MeteredCall { readonly input: number; readonly output: number; /** * The ReAct iteration whose call produced this reading, from the * `stream.llm_end` payload. It is what makes the reading EXPIRE (8.14.0): * before it existed, a provider that reported usage once and then stopped * left this number standing forever, and every later window decision was * made on it. Messages were evicted on a count nobody had taken. */ readonly meteredAtIteration: number; } /** Per-message provenance for the CURRENT window. */ export interface MessageOrigin { /** runtimeStageId of the stage whose write first put this message in. */ readonly stageId: string; /** Wall clock when that write happened — the message's birth. */ readonly bornAtMs: number; } export interface CompactionMeterHandle { readonly id: string; /** * The last completed LLM call's reported usage — **only if it is still * current**. `undefined` before call #1, and `undefined` again once the * reading has expired. * * A reading is current for the iteration that follows the call that * produced it, and no longer. The window stage runs at the loop head: at * iteration N the last completed call was made during iteration N−1, so a * reading stamped older than N−1 means the most recent call reported * nothing usable. That is not a smaller number or an older number — it is * an ABSENT number, and this returns `undefined` for it. Every strategy * already treats `undefined` as "do not act", which is the honest response * to a provider that stopped counting. * * @param currentIteration the iteration the caller is deciding for */ lastCall(currentIteration: number): MeteredCall | undefined; /** * How many `stream.llm_end` events carried no usable usage since the last * good reading. Non-zero means the provider is answering without counting; * the window stage turns it into one warning per run. */ unmeteredSinceLastGood(): number; /** Origins aligned index-for-index with the current window. */ origins(): readonly MessageOrigin[]; /** * Tell the meter a window strategy is about to change the window, so it can * re-align origins to the new one — `[...head, (inserted)?, ...tail]`: * `head` is the leading messages the strategy stepped over (turns that * refused to leave), `tail` is the messages kept after the removed span. * * `insertedAtMs` is the birth of the ONE message the strategy put in the * span's place (compaction's summary, a drop's authored notice). Omit it * when the strategy removed messages and inserted nothing. * * Called by the stage that performs the change, BEFORE it writes — the * shape change is the one thing the meter cannot infer, because the * TypedScope set trap JSON-round-trips every write and destroys object * identity. */ rebaseForWindowChange(headCount: number, keptTailCount: number, insertedAtMs?: number): void; clear(): void; /** * Take one `stream.llm_end` reading. Fed from the agent's REAL-value event * path (`EventDispatcher · onRealEvent`), never from a served event: the * count is a measurement the window decides on, and a redaction policy whose * pattern happens to match `input` must not quietly switch compaction off. */ noteLlmEnd(payload: unknown): void; onWrite(event: WriteEvent): void; } export interface CompactionMeterOptions { /** Recorder id (default 'compaction-meter'). */ readonly id?: string; /** Scope key holding the window. Default 'history'. */ readonly key?: string; /** Injectable clock, so tests can pin `survivalMs`. */ readonly now?: () => number; } /** * 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`. */ export declare function compactionMeter(options?: CompactionMeterOptions): CompactionMeterHandle;