UNPKG

agentfootprint

Version:

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

96 lines • 4.37 kB
/** * StatusRecorder — Claude Code-style live status line for Agent runs. * * Pattern: Facade over EventDispatcher's wildcard subscription. * Role: Tier 3 observability — the low-level helper behind * `attachStatus(dispatcher, { onStatus })` (exported from * `agentfootprint/observe`). For the high-level, uniform path use * `agent.enable.liveStatus({ strategy: chatBubbleLiveStatus({ onLine }) })`. * One callback receives a human-readable status string at every * meaningful moment. * Emits: Does NOT emit; READS core events via the dispatcher and calls * `onStatus`. */ import { progressMessageOf } from './status/statusTemplates.js'; const RELEVANT = new Set([ 'agentfootprint.agent.turn_start', 'agentfootprint.agent.turn_end', 'agentfootprint.agent.iteration_start', 'agentfootprint.agent.route_decided', 'agentfootprint.stream.tool_start', 'agentfootprint.stream.tool_progress', 'agentfootprint.stream.tool_end', ]); /** * Attach a thinking-status subscription to the event dispatcher. * Returns an Unsubscribe — call to detach. * * Holds one small piece of state: how many mid-call reports each in-flight * tool call has filed (9.54.0), so the generic progress line can say "3 so * far" instead of repeating one indistinguishable sentence. The tally is * per-`toolCallId` — parallel calls count separately — and is dropped when * that call ends, so nothing accumulates across a long-lived run. */ export function attachStatus(dispatcher, options) { const progressCounts = new Map(); const format = options.format ?? ((e) => defaultFormatter(e, progressCounts)); return dispatcher.on('*', (event) => { if (!RELEVANT.has(event.type)) return; if (event.type === 'agentfootprint.stream.tool_progress') { const id = event.payload.toolCallId; progressCounts.set(id, (progressCounts.get(id) ?? 0) + 1); } const status = format(event); if (event.type === 'agentfootprint.stream.tool_end') { progressCounts.delete(event.payload.toolCallId); } if (status !== null) options.onStatus(status); }); } /** * Default renderer. Humanizes each supported event into a short status line. * * `tool_progress` keeps the same display contract the projection keeps * (`progressMessageOf`): a top-level string `message` is shown verbatim, * length-capped; anything else gets the tool's name, the fact that it * reported, and the count. The author's payload is never dumped into prose. */ function defaultFormatter(event, progressCounts) { switch (event.type) { case 'agentfootprint.agent.turn_start': return 'Thinking...'; case 'agentfootprint.agent.iteration_start': return `Iteration ${event.payload.iterIndex}`; // A bracket carrying `notDispatched` (9.113.0) is a call the paused batch // never dispatched: "Calling …" and "… failed" would both be false, and a // live status line reports what the agent is doing, so it says nothing. case 'agentfootprint.stream.tool_start': if (event.payload.notDispatched !== undefined) return null; return `Calling ${event.payload.toolName}(…)`; case 'agentfootprint.stream.tool_progress': { const message = progressMessageOf(event.payload.payload); if (message !== null) return message; const n = progressCounts?.get(event.payload.toolCallId) ?? 1; return `${event.payload.toolName} reported progress (${String(n)} so far)`; } case 'agentfootprint.stream.tool_end': if (event.payload.notDispatched !== undefined) return null; return event.payload.error ? `Tool ${event.payload.toolCallId} failed` : `Got result from ${event.payload.toolCallId}`; case 'agentfootprint.agent.route_decided': return event.payload.chosen === 'final' ? 'Composing answer...' : 'Continuing with tool calls...'; case 'agentfootprint.agent.turn_end': return 'Done'; default: return null; } } //# sourceMappingURL=StatusRecorder.js.map