UNPKG

agentfootprint

Version:

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

107 lines • 5.32 kB
/** * NormalizeThinking sub-subflow — wraps a consumer's `ThinkingHandler` * (function-pair contract) in a real footprintjs subflow at chart * build time. Mounted as a stage AFTER CallLLM inside `sf-call-llm` * when (and only when) a handler is configured. * * runtimeStageId format: `sf-call-llm/thinking-{handler.id}#N` * * Two-layer architecture (per Phase 1 panel decision): * - CONSUMER-FACING: `ThinkingHandler` — simple function-pair * (id, normalize, parseChunk?) * - FRAMEWORK-INTERNAL: this file wraps each handler in a real * footprintjs subflow with own runtimeStageId, * narrative entry, and InOutRecorder boundary. * * Failure isolation: handler `normalize()` throws are caught here. * Framework emits `agentfootprint.agent.thinking_parse_failed`, sets * `scope.thinkingBlocks` to empty, and the subflow exits cleanly. The * agent run continues; the assistant message simply has no thinking * blocks attached. Same graceful pattern as v2.11.6 `tools.discovery_failed`. */ import { flowChart } from 'footprintjs'; import { typedEmit } from '../../recorders/core/typedEmit.js'; /** * Build a thinking-normalization sub-subflow for a configured handler. * Mounted as a single-stage subflow inside `sf-call-llm` AFTER CallLLM. * * The subflow: * 1. Reads `scope.rawThinking` (set by CallLLM from `LLMResponse.rawThinking`) * 2. If rawThinking is undefined → write empty array, exit (early-return) * 3. Calls `handler.normalize(rawThinking)` * 4. On success: writes `scope.thinkingBlocks` + emits `stream.thinking_end` * 5. On throw: writes empty array + emits `agent.thinking_parse_failed` * * The result on `scope.thinkingBlocks` is read by toolCalls.ts and * prepareFinal.ts when constructing the assistant message for * `scope.history` — that's where the Anthropic signature round-trip * actually flows from. */ export function buildThinkingSubflow(handler, providerName) { const handlerId = handler.id; return flowChart('Normalize', (scope) => { const raw = scope.$getValue('rawThinking'); const iteration = scope.$getValue('iteration') ?? 0; // Early-return: no thinking content for this call (most calls). // Write empty array so toolCalls.ts / prepareFinal.ts have a // consistent read shape, even when no thinking is present. if (raw === undefined) { scope.$setValue('thinkingBlocks', []); return; } // Failure isolation — same pattern as v2.11.6 tools.discovery_failed. // Handler throws are caught + emitted as the typed event; agent // run continues with no thinking blocks attached. let blocks; try { blocks = handler.normalize(raw); } catch (err) { const errMsg = err instanceof Error ? err.message : String(err); const errName = err instanceof Error ? err.name : 'Error'; scope.$setValue('thinkingBlocks', []); typedEmit(scope, 'agentfootprint.agent.thinking_parse_failed', { providerName, subflowId: handlerId, error: errMsg, errorName: errName, iteration, }); return; } // Strip providerMeta before persistence — type doc declares // it an "escape hatch" for handler-internal use, NOT the durable // record. Excluding it from scope.thinkingBlocks means it won't // leak into narrative entries (rawValue), scope.history, or // audit-log adapters that read from either. const persisted = blocks.some((b) => b.providerMeta !== undefined) ? blocks.map((b) => { if (b.providerMeta === undefined) return b; // Drop providerMeta — escape hatch is handler-internal, not // persisted. Strip via destructure-and-omit; the discarded // binding is intentionally ignored. const { providerMeta, ...rest } = b; void providerMeta; return rest; }) : blocks; scope.$setValue('thinkingBlocks', persisted); // Per-call summary event. Carries METADATA only — full content // lives on LLMMessage.thinkingBlocks (the durable record). const totalChars = persisted.reduce((sum, b) => sum + b.content.length, 0); // v2.14 — embed the persisted blocks in the typed event so live // consumers can render per-iteration reasoning without // post-walking scope.history. The persisted blocks are already // providerMeta-stripped (Phase 6 invariant), so the event's // `blocks` matches the audit-log bytes exactly. Only included // when non-empty so consumers can branch on field presence. typedEmit(scope, 'agentfootprint.stream.thinking_end', { iteration, blockCount: persisted.length, totalChars, ...(persisted.length > 0 && { blocks: persisted }), }); }, `thinking-${handlerId}`, { description: `Normalize ${handlerId} thinking blocks` }).build(); } //# sourceMappingURL=buildThinkingSubflow.js.map