UNPKG

agentfootprint

Version:

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

418 lines • 16.4 kB
"use strict"; /** * 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 * ``` */ Object.defineProperty(exports, "__esModule", { value: true }); exports.liveStateRecorder = exports.LiveStateRecorder = exports.LiveAgentTurnTracker = exports.LiveToolTracker = exports.LiveLLMTracker = void 0; const trace_1 = require("footprintjs/trace"); const observeRunId_js_1 = require("./observeRunId.js"); // ─── LiveLLMTracker ───────────────────────────────────────────────── /** * 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. */ class LiveLLMTracker { id = 'live-llm'; /** Composition: bracket-scoped storage primitive. */ store = new trace_1.BoundaryStateStore(); /** Wipes the store when a fresh run reuses identical runtimeStageId keys. */ runIdGuard = (0, observeRunId_js_1.createRunIdObserver)(() => this.store.clear()); observeRunId(runId) { this.runIdGuard.observe(runId); } /** Subscribe to a runner's dispatcher. Returns an Unsubscribe. */ subscribe(runner) { const offs = []; offs.push(runner.on('agentfootprint.stream.llm_start', (event) => { this.observeRunId(event.meta.runId); const p = event.payload; this.store.start(event.meta.runtimeStageId, { partial: '', tokens: 0, iteration: p.iteration, provider: p.provider, model: p.model, startedAtMs: event.meta.wallClockMs, }); })); offs.push(runner.on('agentfootprint.stream.token', (event) => { this.observeRunId(event.meta.runId); this.store.update(event.meta.runtimeStageId, (s) => ({ ...s, partial: s.partial + event.payload.content, tokens: s.tokens + 1, })); })); offs.push(runner.on('agentfootprint.stream.llm_end', (event) => { this.observeRunId(event.meta.runId); this.store.stop(event.meta.runtimeStageId); })); // Terminal failure: a thrown LLM call never emits llm_end, so the // in-flight boundary would stay open forever ("Chatbot is thinking…" // stuck). The ErrorBridge emits error.fatal on a failed run — clear // all active boundaries so live consumers stop showing in-flight. offs.push(runner.on('agentfootprint.error.fatal', (event) => { this.observeRunId(event.meta.runId); this.store.clear(); })); return () => offs.forEach((off) => off()); } /** Reset all transient state. Called by `LiveStateRecorder.clear()`. */ clear() { this.store.clear(); this.runIdGuard.reset(); } /** True if any LLM call is currently in flight. */ isInFlight() { return this.store.hasActive; } /** Same as `store.hasActive` — exposed for parity with the v4 API. */ get hasActive() { return this.store.hasActive; } /** Number of currently-active boundaries. */ get activeCount() { return this.store.activeCount; } /** Currently-active boundary state for one runtimeStageId. */ getActive(runtimeStageId) { return this.store.get(runtimeStageId); } /** All currently-active boundaries. */ getAllActive() { return this.store.getAll(); } /** 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() { if (!this.store.hasActive) return ''; let latest; let latestStart = -Infinity; for (const state of this.store.getAll().values()) { if (state.startedAtMs > latestStart) { latestStart = state.startedAtMs; latest = state; } } return latest?.partial ?? ''; } } exports.LiveLLMTracker = LiveLLMTracker; // ─── LiveToolTracker ──────────────────────────────────────────────── /** * 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). */ class LiveToolTracker { id = 'live-tool'; store = new trace_1.BoundaryStateStore(); runIdGuard = (0, observeRunId_js_1.createRunIdObserver)(() => this.store.clear()); observeRunId(runId) { this.runIdGuard.observe(runId); } subscribe(runner) { const offs = []; offs.push(runner.on('agentfootprint.stream.tool_start', (event) => { this.observeRunId(event.meta.runId); const p = event.payload; // A call the paused batch never dispatched (9.113.0) opens no // boundary: nothing executes between its start and its end. Its // `tool_end` closes nothing either (below). if (p.notDispatched !== undefined) return; this.store.start(p.toolCallId, { toolName: p.toolName, args: p.args, toolCallId: p.toolCallId, startedAtMs: event.meta.wallClockMs, }); })); offs.push(runner.on('agentfootprint.stream.tool_end', (event) => { this.observeRunId(event.meta.runId); if (event.payload.notDispatched !== undefined) return; // settled — opened nothing this.store.stop(event.payload.toolCallId); })); return () => offs.forEach((off) => off()); } clear() { this.store.clear(); this.runIdGuard.reset(); } /** True if any tool is currently executing. */ isExecuting() { return this.store.hasActive; } get hasActive() { return this.store.hasActive; } get activeCount() { return this.store.activeCount; } getActive(toolCallId) { return this.store.get(toolCallId); } getAllActive() { return this.store.getAll(); } /** Names of tools currently executing. Empty when none. */ getExecutingToolNames() { return [...this.store.getAll().values()].map((s) => s.toolName); } } exports.LiveToolTracker = LiveToolTracker; // ─── LiveAgentTurnTracker ─────────────────────────────────────────── /** * 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. */ class LiveAgentTurnTracker { id = 'live-agent-turn'; store = new trace_1.BoundaryStateStore(); runIdGuard = (0, observeRunId_js_1.createRunIdObserver)(() => this.store.clear()); observeRunId(runId) { this.runIdGuard.observe(runId); } subscribe(runner) { const offs = []; offs.push(runner.on('agentfootprint.agent.turn_start', (event) => { this.observeRunId(event.meta.runId); const p = event.payload; this.store.start(String(p.turnIndex), { turnIndex: p.turnIndex, userPrompt: p.userPrompt, startedAtMs: event.meta.wallClockMs, }); })); offs.push(runner.on('agentfootprint.agent.turn_end', (event) => { this.observeRunId(event.meta.runId); this.store.stop(String(event.payload.turnIndex)); })); return () => offs.forEach((off) => off()); } clear() { this.store.clear(); this.runIdGuard.reset(); } /** True if the agent is currently inside a turn. */ isInTurn() { return this.store.hasActive; } get hasActive() { return this.store.hasActive; } get activeCount() { return this.store.activeCount; } getActive(turnIndex) { return this.store.get(turnIndex); } getAllActive() { return this.store.getAll(); } /** Index of the most-recently started active turn (-1 if none). */ getCurrentTurnIndex() { if (!this.store.hasActive) return -1; let latest = -1; let latestStart = -Infinity; for (const state of this.store.getAll().values()) { if (state.startedAtMs > latestStart) { latestStart = state.startedAtMs; latest = state.turnIndex; } } return latest; } } exports.LiveAgentTurnTracker = LiveAgentTurnTracker; // ─── LiveStateRecorder — façade composing the three trackers ──────── /** * 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. */ class LiveStateRecorder { id = 'live-state'; /** LLM call live state. */ llm; /** Tool execution live state. */ tool; /** Agent turn live state. */ turn; /** Active subscription disposer, if `subscribe()` is called. */ active; constructor() { this.llm = new LiveLLMTracker(); this.tool = new LiveToolTracker(); this.turn = new LiveAgentTurnTracker(); } /** 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) { this.unsubscribe(); // Each tracker observes runId in its own per-event handler. A // facade-level wildcard would fire AFTER the per-event handler // (dispatcher order: byType → domainWildcards → allWildcards), // wiping state the tracker just stored. Per-tracker observation is // sufficient: a tracker that stores data necessarily ran its // handler, which set lastRunId. A tracker that holds no data // doesn't need a reset. const offs = [ this.llm.subscribe(runner), this.tool.subscribe(runner), this.turn.subscribe(runner), ]; this.active = () => offs.forEach((off) => off()); return this.active; } /** Detach all three trackers from the current runner. Idempotent. */ unsubscribe() { if (this.active) { this.active(); this.active = undefined; } } /** Reset transient state across all three trackers. Called by the * executor / consumer between runs. */ clear() { this.llm.clear(); this.tool.clear(); this.turn.clear(); } // ── Convenience reads (O(1)) ────────────────────────────────────── /** True if any LLM call is currently in flight. */ isLLMInFlight() { return this.llm.isInFlight(); } /** Accumulated partial content of the most-recently started LLM call. */ getPartialLLM() { return this.llm.getLatestPartial(); } /** True if any tool is currently executing. */ isToolExecuting() { return this.tool.isExecuting(); } /** Names of tools currently executing. */ getExecutingToolNames() { return this.tool.getExecutingToolNames(); } /** True if the agent is currently inside a turn. */ isAgentInTurn() { return this.turn.isInTurn(); } /** Current turn index (-1 if not in a turn). */ getCurrentTurnIndex() { return this.turn.getCurrentTurnIndex(); } } exports.LiveStateRecorder = LiveStateRecorder; /** Convenience factory — same shape as `boundaryRecorder()` / * `topologyRecorder()` / `inOutRecorder()` in footprintjs. */ function liveStateRecorder() { return new LiveStateRecorder(); } exports.liveStateRecorder = liveStateRecorder; //# sourceMappingURL=LiveStateRecorder.js.map