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 • 8.42 kB
TypeScript
/**
* 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;
};
}
//# sourceMappingURL=types.d.ts.map