UNPKG

agentfootprint

Version:

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

179 lines (178 loc) • 8.38 kB
/** * Thinking — public types for the v2.14 extended-thinking subsystem. * * Mental model — TWO-LAYER architecture: * * • CONSUMER-FACING: `ThinkingHandler` — a simple function-pair * (id, normalize, parseChunk?). * Provider authors and custom-LLM consumers * implement THIS shape. * * • FRAMEWORK-INTERNAL: each `ThinkingHandler` is auto-wrapped in a * real footprintjs subflow at chart build time. * The subflow gets its own `runtimeStageId`, * narrative entry, and InOutRecorder boundary * — full trace observability for free without * the consumer writing flowchart code. * * Same pattern as how consumers write a `Tool` and the framework wraps * dispatch in a tool-call subflow, or how consumers write a * `ToolProvider` and the framework wraps `list()` in the Tools slot * subflow. * * @see thinkingHandlerFor — the agent uses the handler the PROVIDER * declares (`LLMProvider.thinkingHandler`), never one looked up by name. * @see MockThinkingHandler for the canonical example demonstrating * both Anthropic-shape (signed blocks) and OpenAI-shape (multi- * block summary) inputs. */ /** * One thinking block from an LLM response, normalized into a provider- * agnostic shape. A response may contain multiple blocks (e.g. OpenAI's * structured reasoning_summary emits one block per summary step). * * Discriminator `type`: * - `'thinking'` — content is the model's reasoning * - `'redacted_thinking'` — Anthropic emits this when reasoning * trips a safety filter; content is * EMPTY but `signature` is still required * for round-trip on the next turn */ export interface ThinkingBlock { /** * Block type discriminator. Required so consumers can distinguish * normal thinking blocks from server-redacted ones (Anthropic-only * today; other providers may produce only `'thinking'`). */ readonly type: 'thinking' | 'redacted_thinking'; /** * Reasoning content as plain text. EMPTY string when `type === * 'redacted_thinking'` (the model's reasoning was redacted by the * provider's safety layer; only the signature remains). */ readonly content: string; /** * Provider cryptographic signature for round-trip integrity. * * **Anthropic:** REQUIRED for any block emitted in a response that * also contained tool_use. The signed block MUST be echoed * byte-exact in the assistant message of subsequent tool_result * turns — Anthropic validates the signature server-side and rejects * (HTTP 400) requests where signed blocks are missing or modified. * * **OpenAI:** not used (their reasoning_summary doesn't sign blocks). * * **Future providers:** opaque to the framework; preserved as-is. * * The framework persists this field in `scope.history` so the * provider's serialization layer (Phase 4b) can echo it back on the * next request. */ readonly signature?: string; /** * `true` when this block is a structured-summary step rather than * raw thinking content. Set by `OpenAIThinkingHandler` for each step * of `reasoning_summary`; never set by Anthropic. Consumers * displaying thinking can render summary blocks differently * (e.g. as numbered steps) from raw blocks (continuous prose). */ readonly summary?: boolean; /** * Provider-specific metadata escape hatch for fields the normalized * shape doesn't model. * * **ANTI-PATTERN:** providers MUST NOT include sensitive raw data * here (PII, internal IDs, request tokens, customer data). Use the * dedicated `signature` field for cryptographic material; nothing * else identity-bearing. The framework excludes `providerMeta` from * `getNarrative()` by default to avoid accidental audit-log leakage. */ readonly providerMeta?: Readonly<Record<string, unknown>>; } /** * How one model takes a request to think — declared by the provider adapter * per model (`LLMProvider.thinkingMode`), because models on the same wire * disagree and the wrong shape is refused by the vendor on every call. * * - `'budget'` — a token budget for the thinking (Anthropic: * `thinking: { type: 'enabled', budget_tokens }`). The * `.thinking({ budget })` number is sent as-is. * - `'adaptive'` — thinking on, no budget: the model decides whether and how * much to think (Anthropic: `thinking: { type: 'adaptive' }`). * The budget is NOT sent — the wire has nowhere to put it. * - `'none'` — the model cannot think. `.thinking()` is refused, by name, * with an `UnsupportedThinkingError`. * * @example * ```ts * import { anthropic } from 'agentfootprint/providers'; * * const provider = anthropic(); * provider.thinkingMode?.('claude-opus-5-5'); // 'adaptive' * provider.thinkingMode?.('claude-haiku-4-5'); // 'budget' * ``` */ export type ThinkingMode = 'adaptive' | 'budget' | 'none'; /** * The consumer-facing contract for thinking normalization. Provider * authors and custom-LLM consumers implement this shape; the framework * auto-wraps each handler in a real footprintjs subflow at chart build * time so the trace shows it as a discrete `runtimeStageId` (e.g. * `sf-call-llm/thinking-{id}#5`). * * The provider DECLARES its handler (`LLMProvider.thinkingHandler`) and the * agent uses it — never chosen by `provider.name`, so a wrapper that forwards * the field (`withRetry`, an app's own) keeps it. Override per agent with * `.thinkingHandler(customHandler)`; `.thinkingHandler(null)` opts out. */ export interface ThinkingHandler { /** * Stable identifier used for `runtimeStageId`, telemetry, narrative * entries, and the `agentfootprint.agent.thinking_parse_failed` event * payload's `subflowId` field. Convention: lowercase + dash, e.g. * `'anthropic'`, `'openai'`, `'mock'`. */ readonly id: string; /** * Pure: raw provider data → normalized blocks. * * The framework wraps this call in a try/catch — throwing from * `normalize()` does NOT abort the agent run. Instead the framework * emits `agentfootprint.agent.thinking_parse_failed`, drops the * thinking blocks (LLMMessage.thinkingBlocks remains undefined), * and continues. Same graceful-failure pattern as v2.11.6 * `tools.discovery_failed`. * * Sync only in v2.14. Future widening to Promise return is a * separate decision once a real consumer needs network-backed * normalization. * * @param raw Provider-specific raw data — typically pulled from * `LLMResponse.providerRef`. Handler is responsible for * shape-checking; framework passes whatever the provider * stashed. * @returns Normalized blocks in the order they appeared in the * response. Empty array when no thinking is present * (preferred over `undefined` for type stability). */ normalize(raw: unknown): readonly ThinkingBlock[]; /** * Optional streaming hot-path. When provided AND the provider streams, * the framework calls `parseChunk(chunk)` per provider chunk and * emits `agentfootprint.stream.thinking_delta` events for any * `thinkingDelta` returned. Handlers without streaming support omit * this field; the framework still calls `normalize()` on the * response's terminal `LLMResponse.providerRef`. * * @param chunk Provider-specific chunk shape (Anthropic emits * `content_block_delta` events; OpenAI doesn't yet * stream reasoning content). * @returns Object with optional `thinkingDelta` text — when set, * framework fires `stream.thinking_delta` event with the * content. Return `{}` (or omit) for chunks that contain * no thinking content. */ parseChunk?(chunk: unknown): { thinkingDelta?: string; }; }