UNPKG

agentfootprint

Version:

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

123 lines • 5.66 kB
"use strict"; /** * AnthropicThinkingHandler — normalizes Anthropic's extended-thinking * response shape into the framework's `ThinkingBlock[]` contract. * * Anthropic emits thinking via blocks in `response.content`: * * ```ts * { type: 'thinking', thinking: 'reasoning text', signature: 'opaque-base64' } * { type: 'redacted_thinking', data: 'opaque-encrypted' } * { type: 'text', text: 'visible content' } * { type: 'tool_use', id, name, input } * ``` * * The handler filters for `'thinking'` + `'redacted_thinking'` blocks, * preserves the `signature` field BYTE-EXACT (Anthropic validates * signatures server-side on the next turn — any modification = HTTP 400), * and ignores other block types (visible text + tool calls flow through * the existing `LLMResponse.content` / `LLMResponse.toolCalls` paths). * * The same shapes in both thinking modes (budget and adaptive). Two * adaptive-mode facts the echo already honours: a turn may carry NO * thinking block (the model chose not to think), and a block's `thinking` * text may be EMPTY — `display: 'omitted'`, the default on Claude 4.7 and * later when the request does not ask to think (`.thinking()` asks for * `'summarized'`), and the progress notes some models write between tool * calls. An empty block is still complete — its `signature` carries the * reasoning — so it is kept and echoed like any other, never dropped. * * **Critical invariant:** signature pass-through is byte-exact. The * handler MUST NOT trim, normalize encoding, JSON-roundtrip, or * otherwise touch the signature string. Tests verify this explicitly. * * **Three input shapes** Anthropic produces (per Phase 4a panel review): * 1. Non-streaming response: full `response.content` array * 2. Streaming aggregated: AnthropicProvider accumulates chunks + * calls handler with the same array shape * 3. Bedrock-via-Anthropic: deferred to Phase 5+ (separate handler * or extension of this one) * * Streaming + non-streaming converge on shape #1 because the provider * handles the distinction — handler only sees the assembled content * array. * * **`parseChunk` is OPTIONAL** — Phase 3's framework path populates * `LLMChunk.thinkingDelta` from inside AnthropicProvider's `stream()` * directly, bypassing handler.parseChunk. We still implement it for * consumer integrations that want to use the handler on raw Anthropic * chunks directly (e.g., custom transports). */ Object.defineProperty(exports, "__esModule", { value: true }); exports.anthropicThinkingHandler = void 0; function isAnthropicContentArray(raw) { return Array.isArray(raw); } function isThinkingBlock(b) { return b.type === 'thinking'; } function isRedactedThinkingBlock(b) { return b.type === 'redacted_thinking'; } function isAnthropicChunk(chunk) { return typeof chunk === 'object' && chunk !== null; } exports.anthropicThinkingHandler = { id: 'anthropic', // 'browser-anthropic' shares the same response shape (raw Anthropic // wire format) — both providers route through fromAnthropicResponse // which sets `rawThinking` to `message.content`. Bedrock Claude // would also fit here but ships as a separate handler if/when its // shape diverges. normalize(raw) { if (!isAnthropicContentArray(raw)) return []; const out = []; for (const block of raw) { if (isThinkingBlock(block)) { // Byte-exact signature preservation: pass through as-is. // No String() coercion that could normalize encoding; if // Anthropic ever emits a non-string signature it would be a // wire-protocol violation we want to surface, not silently // coerce. out.push({ type: 'thinking', // Don't touch content — pass through. Anthropic guarantees // it's a string when the field is present. content: block.thinking, ...(block.signature !== undefined && { signature: block.signature }), }); } else if (isRedactedThinkingBlock(block)) { // Redacted blocks have no readable content; the encrypted `data` // is what must round-trip, so it rides `signature` — the // ThinkingBlock's round-trip slot — byte-exact, and the wire // writes it back as `data`. Dropping it made the echo a 400. out.push({ type: 'redacted_thinking', content: '', ...(block.data !== undefined && { signature: block.data }), }); } // Other block types (text, tool_use, etc.) flow through the // existing LLMResponse paths — ignore here. } return out; }, parseChunk(chunk) { // Optional escape hatch — framework path doesn't call this // (AnthropicProvider populates LLMChunk.thinkingDelta directly). // Provided for consumer integrations using the handler on raw // Anthropic chunks directly. if (!isAnthropicChunk(chunk)) return {}; const delta = chunk.delta; if (!delta) return {}; if (delta.type === 'thinking_delta' && typeof delta.thinking === 'string') { return { thinkingDelta: delta.thinking }; } return {}; }, }; //# sourceMappingURL=AnthropicThinkingHandler.js.map