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
TypeScript
/**
* 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;