/** * LiveStateRecorder — domain trackers built on the footprintjs * `BoundaryStateStore` 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` (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` 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(type: K, listener: (event: Extract) => 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>; 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; /** 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 * * 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; /** 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; /** 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;