agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
253 lines (252 loc) • 10.7 kB
TypeScript
/**
* LiveStateRecorder — domain trackers built on the footprintjs
* `BoundaryStateStore<TState>` storage primitive (v4.17.2+).
*
* **What this answers:** "Right now, mid-run, what's happening?"
*
* - Is an LLM call in flight? What's the partial answer so far?
* - Is a tool executing? Which tool? What args?
* - Is the agent in a turn? Which turn index?
*
* All reads are O(1) — the trackers maintain incremental state via
* the framework's bracket-scoped storage primitive. No event-log fold,
* no walking arrays per render.
*
* **Mental model — observers vs. bookkeepers:**
*
* `BoundaryStateStore<TState>` (footprintjs) = STORAGE shelf.
* `EventDispatcher.on(...)` (agentfootprint) = OBSERVER source.
*
* Each domain tracker (`LiveLLMTracker`, `LiveToolTracker`,
* `LiveAgentTurnTracker`) extends the storage shelf AND subscribes
* to the dispatcher. The composition `LiveStateRecorder` bundles
* all three so a consumer only attaches once.
*
* **Tier 1 (live) only.** Past states are not stored — when a boundary
* closes, its transient state clears. For time-travel queries, snapshot
* to a `SequenceStore<TState>` instead. See the BoundaryStateStore
* JSDoc for the rationale.
*
* @example Use the bundled façade — one attach, three live views:
*
* ```typescript
* import { LiveStateRecorder } from 'agentfootprint';
*
* const liveState = new LiveStateRecorder();
* liveState.subscribe(runner);
*
* await runner.run({ input });
*
* // Read at any moment during the run (e.g., from another async task):
* liveState.isLLMInFlight(); // true between llm_start ↔ llm_end
* liveState.getPartialLLM(); // accumulated tokens so far
* liveState.isToolExecuting(); // true between tool_start ↔ tool_end
* liveState.isAgentInTurn(); // true between turn_start ↔ turn_end
*
* liveState.unsubscribe();
* ```
*
* @example Use a single tracker directly when you only need one slice:
*
* ```typescript
* import { LiveLLMTracker } from 'agentfootprint';
*
* const llm = new LiveLLMTracker();
* llm.subscribe(runner);
* await runner.run({ input });
*
* llm.isInFlight(); // O(1)
* llm.getLatestPartial(); // most recent active call's partial
* llm.getActive(rid)?.tokens; // tokens accumulated for one call
* ```
*/
import type { Unsubscribe } from '../../events/dispatcher.js';
import type { AgentfootprintEvent, AgentfootprintEventType } from '../../events/registry.js';
/** Minimal Runner shape this recorder needs — only the public `on(...)`
* subscription method, so the same trackers can attach to a real Runner
* (Agent, etc.) OR to a test mock without exposing the protected
* internal dispatcher. */
export interface LiveStateRunnerLike {
on<K extends AgentfootprintEventType>(type: K, listener: (event: Extract<AgentfootprintEvent, {
type: K;
}>) => void): Unsubscribe;
}
/** Live transient state of one in-flight LLM call. */
export interface LLMLiveState {
/** Accumulated content from `stream.token` events since `llm_start`. */
readonly partial: string;
/** Number of tokens received so far. */
readonly tokens: number;
/** Iteration index (from the LLMStartPayload). */
readonly iteration: number;
/** Provider name (e.g., 'anthropic', 'openai'). */
readonly provider: string;
/** Model id. */
readonly model: string;
/** Wall-clock ms when llm_start fired. */
readonly startedAtMs: number;
}
/** Live transient state of one in-flight tool call. */
export interface ToolLiveState {
readonly toolName: string;
readonly args: Readonly<Record<string, unknown>>;
readonly toolCallId: string;
readonly startedAtMs: number;
}
/** Live transient state of one in-flight agent turn. */
export interface AgentTurnLiveState {
readonly turnIndex: number;
readonly userPrompt: string;
readonly startedAtMs: number;
}
/**
* Tracks the in-flight state of LLM calls. Subscribes to:
* - `agentfootprint.stream.llm_start` → opens a boundary
* - `agentfootprint.stream.token` → appends to partial
* - `agentfootprint.stream.llm_end` → closes the boundary
*
* Boundary key: `runtimeStageId` of the call-llm stage. Parallel LLM
* calls (Parallel composition with multiple branches) get distinct
* keys and are tracked independently.
*/
export declare class LiveLLMTracker {
readonly id = "live-llm";
/** Composition: bracket-scoped storage primitive. */
private readonly store;
/** Wipes the store when a fresh run reuses identical runtimeStageId keys. */
private readonly runIdGuard;
private observeRunId;
/** Subscribe to a runner's dispatcher. Returns an Unsubscribe. */
subscribe(runner: LiveStateRunnerLike): Unsubscribe;
/** Reset all transient state. Called by `LiveStateRecorder.clear()`. */
clear(): void;
/** True if any LLM call is currently in flight. */
isInFlight(): boolean;
/** Same as `store.hasActive` — exposed for parity with the v4 API. */
get hasActive(): boolean;
/** Number of currently-active boundaries. */
get activeCount(): number;
/** Currently-active boundary state for one runtimeStageId. */
getActive(runtimeStageId: string): LLMLiveState | undefined;
/** All currently-active boundaries. */
getAllActive(): ReadonlyMap<string, LLMLiveState>;
/** Accumulated partial content of the MOST RECENTLY started active
* LLM call. Empty string when no call is active. Useful for the
* classic "Chatbot is responding: …" live commentary line. */
getLatestPartial(): string;
}
/**
* Tracks in-flight tool calls. Subscribes to:
* - `agentfootprint.stream.tool_start` → opens a boundary
* - `agentfootprint.stream.tool_end` → closes the boundary
* (a bracket carrying `notDispatched` — a call a paused batch never
* dispatched, 9.113.0 — does neither: nothing executed)
*
* Boundary key: `toolCallId` (more granular than `runtimeStageId` —
* parallel tools share one calling stage but have distinct toolCallIds).
*/
export declare class LiveToolTracker {
readonly id = "live-tool";
private readonly store;
private readonly runIdGuard;
private observeRunId;
subscribe(runner: LiveStateRunnerLike): Unsubscribe;
clear(): void;
/** True if any tool is currently executing. */
isExecuting(): boolean;
get hasActive(): boolean;
get activeCount(): number;
getActive(toolCallId: string): ToolLiveState | undefined;
getAllActive(): ReadonlyMap<string, ToolLiveState>;
/** Names of tools currently executing. Empty when none. */
getExecutingToolNames(): readonly string[];
}
/**
* Tracks in-flight agent turns. Subscribes to:
* - `agentfootprint.agent.turn_start` → opens a boundary
* - `agentfootprint.agent.turn_end` → closes the boundary
*
* Boundary key: stringified `turnIndex` from the payload — survives
* across runner instances because turnIndex resets per-session.
*/
export declare class LiveAgentTurnTracker {
readonly id = "live-agent-turn";
private readonly store;
private readonly runIdGuard;
private observeRunId;
subscribe(runner: LiveStateRunnerLike): Unsubscribe;
clear(): void;
/** True if the agent is currently inside a turn. */
isInTurn(): boolean;
get hasActive(): boolean;
get activeCount(): number;
getActive(turnIndex: string): AgentTurnLiveState | undefined;
getAllActive(): ReadonlyMap<string, AgentTurnLiveState>;
/** Index of the most-recently started active turn (-1 if none). */
getCurrentTurnIndex(): number;
}
/**
* One-stop façade bundling `LiveLLMTracker` + `LiveToolTracker` +
* `LiveAgentTurnTracker`. Consumers attach this once and get O(1)
* reads across all three live-state slices.
*
* Use the bundled façade unless you ONLY need one slice — using a
* single tracker directly avoids subscribing to events you don't read.
*
* **Lifecycle**: call `subscribe(runner)` to wire all three trackers,
* then `unsubscribe()` to detach. `clear()` resets all transient state
* across the three (called automatically by consumers like Lens between
* runs).
*
* **What this is NOT for:**
* - Time-travel queries (Tier 1 only — live state)
* - Aggregations (use SequenceStore.aggregate)
* - Stage-level observation (use Recorder.onStageStart/End)
*
* **Composition over inheritance:** the façade does NOT extend
* `BoundaryStateStore` itself — different boundary kinds need
* separate active maps to avoid key collisions between LLM and tool
* boundaries. Each sub-tracker keeps its own state.
*/
export declare class LiveStateRecorder {
readonly id = "live-state";
/** LLM call live state. */
readonly llm: LiveLLMTracker;
/** Tool execution live state. */
readonly tool: LiveToolTracker;
/** Agent turn live state. */
readonly turn: LiveAgentTurnTracker;
/** Active subscription disposer, if `subscribe()` is called. */
private active;
constructor();
/** Subscribe all three trackers to one runner. Idempotent — calling
* twice on the same recorder unsubscribes the prior subscription
* first to avoid double-counting.
*
* Adds a wildcard `*` listener that observes runId on EVERY event
* (regardless of which tracker subscribes to it) and calls
* `clear()` on all three trackers when the runId changes. This
* closes the gap where a tracker that never saw events in run 1
* would fail to reset in run 2. */
subscribe(runner: LiveStateRunnerLike): Unsubscribe;
/** Detach all three trackers from the current runner. Idempotent. */
unsubscribe(): void;
/** Reset transient state across all three trackers. Called by the
* executor / consumer between runs. */
clear(): void;
/** True if any LLM call is currently in flight. */
isLLMInFlight(): boolean;
/** Accumulated partial content of the most-recently started LLM call. */
getPartialLLM(): string;
/** True if any tool is currently executing. */
isToolExecuting(): boolean;
/** Names of tools currently executing. */
getExecutingToolNames(): readonly string[];
/** True if the agent is currently inside a turn. */
isAgentInTurn(): boolean;
/** Current turn index (-1 if not in a turn). */
getCurrentTurnIndex(): number;
}
/** Convenience factory — same shape as `boundaryRecorder()` /
* `topologyRecorder()` / `inOutRecorder()` in footprintjs. */
export declare function liveStateRecorder(): LiveStateRecorder;