UNPKG

agentfootprint

Version:

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

116 lines (115 loc) • 6.13 kB
/** * eventTail — a bounded, honest copy of an event stream. * * Pattern: ring buffer with an explicit drop counter. * Role: the ONE place the library decides how much of a run's event * stream it keeps in memory, and how it tells a reader that it * kept less than everything. * * Two features hold a tail of the same stream for the same reason — a * recording (`recordRun`) and the self-explaining agent's evidence * (`SelfExplainBinding`) — and both need the same two properties: * * 1. BOUNDED. A long-running server must not grow an array per run * forever. Past the cap the OLDEST events go, because the end of a * turn is the part a reader asks about. * 2. HONEST. A tail that dropped events says how many. Silently * starting a timeline mid-run is the failure this exists to * prevent — a consumer would read the remainder as the whole run. * * Written once here rather than twice, because the drift that matters is * not the ring buffer (easy) — it is the two features disagreeing about * what "dropped" means and one of them forgetting to report it. * * ── A REPEAT HOLDS NO SLOT ────────────────────────────────────────────── * Every model call re-announces what is in its context: each slot's pieces * fire `agentfootprint.context.injected` again on every iteration — by * design, because that is the per-call record of what the model was shown * (`recorders/core/ContextRecorder.ts`, and the answer account reads it as * its witness). Counted like any other event, those repeats grow with the * square of the iteration count and evicted the START of long runs: * measured, a 100-iteration agent run fired ~11,400 events into the default * 10,000-event cap and its recording opened at iteration ~33. * * So the cap counts DISTINCT events. An announcement whose payload is * identical, field for field, to one the tail still holds is a REPEAT: it is * kept — the stream stays whole and in order — but it holds no slot, and it * is stored with the held announcement's payload object (identical content, * one copy). What a repeat costs is its envelope. Identity is decided on the * full payload (`samePayload`), never on the 32-bit `contentHash`, which only * narrows the candidates: two different pieces are never merged. Deciding * reads the payload once, `rawContent` included — the same order of work as * the content hash the slot already computed to announce it. * * Eviction is unchanged in kind: the oldest events go first, repeats with * them, so the retained events are always ONE contiguous suffix of the * stream and `firstRetainedIndex` still says where it starts. * * The tail stays BOUNDED all the same. A repeat holds no slot, but it is still * an envelope in memory (its own type and meta beside the shared payload), so * the whole tail — slots and repeats together — is capped at * {@link REPEAT_ALLOWANCE} times the cap: 100,000 events at the default. A * 100-iteration run (~11,500 events, ~10,000 of them repeats) keeps every one; * a run long enough to cross that loses its oldest events, reported as * dropped, exactly as before this rule. * * Internal: not exported from any barrel. Both consumers wrap it in their * own surface (`RunRecorder.droppedEvents`, the toolpack's ⚠ marker). */ import type { AgentfootprintEvent } from './registry.js'; /** * Default retained-event cap — a few hundred KB for a typical turn, and * far more than any single turn fires. */ export declare const DEFAULT_MAX_EVENTS = 10000; /** A frozen read of the tail: the events kept, and the count of those not. */ export interface EventTailSnapshot { /** A fresh copy — the tail may keep growing behind the caller's back. */ readonly events: readonly AgentfootprintEvent[]; /** How many events were discarded to stay under the cap. `0` normally. */ readonly dropped: number; /** * The ORIGINAL stream position of `events[0]` (9.60.0) — the retained * window is `[firstRetainedIndex, firstRetainedIndex + events.length)` * of the stream as fired, so `events[i]` was stream event * `firstRetainedIndex + i`. * * Under oldest-first eviction from one stream this is always equal to * `dropped` — stated as its own named field because a drop COUNT alone * cannot say WHICH range is gone: a reader aligning this tail against * another record of the same run (a commit log, a second recording) * needs the offset, not just the loss. Before this field, an archived * envelope could say "312 events dropped" and nothing could say where * the retained window starts. */ readonly firstRetainedIndex: number; } /** A bounded tail of one event stream. */ export interface EventTail { /** Append one event, dropping the oldest if the cap is reached. A repeated * announcement is kept without taking a slot — see the module header. */ push(event: AgentfootprintEvent): void; /** How many events are currently retained. */ readonly count: number; /** How many were discarded to stay under the cap. `0` on a normal turn. */ readonly dropped: number; /** Original stream position of the oldest retained event — see * {@link EventTailSnapshot.firstRetainedIndex}. */ readonly firstRetainedIndex: number; /** Freeze the tail — a fresh array plus the drop count beside it. */ snapshot(): EventTailSnapshot; } /** * Start a bounded tail. * * @param maxEvents cap on retained events (default {@link DEFAULT_MAX_EVENTS}). * Non-finite or non-positive values fall back to the default * rather than producing a tail that keeps nothing. */ export declare function eventTail(maxEvents?: number): EventTail; /** * How many events — slots and repeats together — the tail holds per slot of * its cap: the bound on memory a repeat's envelope still costs. See the module * header. */ export declare const REPEAT_ALLOWANCE = 10;