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) • 7.47 kB
TypeScript
/**
* anthropicMessagesWire — the Anthropic Messages body, spoken by more than one
* transport.
*
* Pattern: shared pure helpers (no I/O). Role: outer ring, below the adapters.
*
* Two adapters put the SAME body on different wires:
* - `browserAnthropic()` — POST api.anthropic.com/v1/messages, with a `model`
* field and an `anthropic-version` header;
* - `invokeModelGateway()` — POST {gateway}/model/{id}/invoke, with NO `model`
* field (the id is in the path) and `anthropic_version` in the body.
*
* Everything else — `system` as a top-level field, content blocks, `tool_use` /
* `tool_result`, thinking blocks, cache markers, the SSE event sequence — is one
* shape, so it has ONE owner here instead of a copy per adapter. The functions
* were moved out of `BrowserAnthropicProvider.ts` unchanged; its tests pin them.
*
* Deliberately NOT here: the transport (URL, headers, fetch, status handling)
* and the SSE framing (the browser adapter reads `event:` lines; a gateway that
* decoded AWS's binary framing may send bare `data:` lines). Each adapter turns
* its bytes into {@link AnthropicStreamEvent}s and hands them to
* {@link assembleAnthropicStream}.
*/
import type { LLMChunk, LLMRequest, LLMResponse } from '../../provider-contract/types.js';
import { type AnthropicThinkingParam } from './anthropicThinkingWire.js';
import type { WireToolManifest } from './wireManifest.js';
import type { ThinkingMode } from '../../thinking/types.js';
import type { MessageReplay, ThinkingReplayOmission } from '../../thinking/replay.js';
export { toAnthropicMessages, toAnthropicTool } from './anthropicMessageProjection.js';
/** The Messages body WITHOUT `model` — each transport adds its own framing. */
export interface AnthropicMessagesBody {
max_tokens: number;
messages: AnthropicMessageParam[];
system?: string;
tools?: AnthropicTool[];
temperature?: number;
stop_sequences?: string[];
thinking?: AnthropicThinkingParam;
tool_choice?: {
type: 'auto';
disable_parallel_tool_use: true;
} | {
type: 'tool';
name: string;
};
output_config?: {
format: {
type: 'json_schema';
schema: Record<string, unknown>;
};
};
}
export interface AnthropicMessageParam {
role: 'user' | 'assistant';
content: string | AnthropicContentBlock[];
}
export type AnthropicContentBlock = {
type: 'text';
text: string;
citations?: null | readonly [];
} | {
type: 'tool_use';
id: string;
name: string;
input: Record<string, unknown>;
caller?: {
readonly type: 'direct';
};
toolset_name?: null;
} | {
type: 'tool_result';
tool_use_id: string;
content: string;
is_error?: boolean;
} | {
type: 'thinking';
thinking: string;
signature?: string;
} | {
type: 'redacted_thinking';
data: string;
};
export interface AnthropicTool {
name: string;
description: string;
input_schema: Record<string, unknown>;
}
export interface AnthropicMessage {
id: string;
model: string;
role: 'assistant';
content: AnthropicContentBlock[];
stop_reason: string;
usage: {
input_tokens: number;
output_tokens: number;
cache_read_input_tokens?: number;
cache_creation_input_tokens?: number;
};
}
/** One decoded stream event: its name (the SSE `event:` line, or the JSON's `type`) and its data. */
export interface AnthropicStreamEvent {
readonly event: string;
readonly data: unknown;
}
/** How one adapter wants its Messages body built. */
export interface MessagesBodyOptions {
/** The model the request goes to, the adapter's shorthand already resolved. */
readonly model: string;
/**
* The adapter's ONE mode function — the very one it declares as
* `LLMProvider.thinkingMode` (`thinkingModeWith`, anthropicThinkingWire.ts) —
* so the shape sent is the shape the agent checked.
*/
readonly thinkingMode: (model: string) => ThinkingMode;
/** The adapter's name, for the refusals the thinking plan raises. */
readonly provider: string;
/** `max_tokens` when the request sets none. */
readonly maxTokensDefault: number;
/** `false` caps a reply at one tool call. */
readonly parallelToolCalls?: boolean;
/**
* The adapter's declared `supportsNativeOutputSchema` — whether `model`
* takes `output_config.format`. ABSENT means the adapter does not carry the
* format at all (`invokeModelGateway()` in this release), so a request that
* asks for one is refused by name rather than sent without it.
*/
readonly nativeOutputSchema?: (model: string) => boolean;
}
/**
* Build the Messages body (minus `model`) from a framework request.
*
* ONE owner: `anthropic()`, `browserAnthropic()` and `invokeModelGateway()`
* all build their body here, so a rule about the body — like which thinking
* shape a model takes — is written once.
*
* @throws UnsupportedThinkingError before anything is sent, when the request
* asks to think in a way the model cannot take (see `anthropicThinkingPlan`).
*/
export declare function buildMessagesBody(req: LLMRequest, options: MessagesBodyOptions): AnthropicMessagesBody;
export declare function fromAnthropicResponse(message: AnthropicMessage, binding?: MessageReplay['binding'], argumentFailures?: ReadonlyMap<number, NonNullable<LLMResponse['toolCalls'][number]['argumentsFailure']>>): LLMResponse;
export declare function normalizeStopReason(raw: string): string;
/** A tool call whose streamed argument JSON did not parse. */
export interface MalformedToolArgs {
readonly id: string;
readonly name: string;
readonly index: number;
/** The accumulated `partial_json` text. */
readonly raw: string;
}
export interface StreamAssemblyOptions {
/** Capture binding from the actual sent body once the response identity is known. */
readonly bindingFactory?: (id: string) => MessageReplay['binding'];
/** Omissions already reported by the leaf before this attempt was sent. */
readonly replayOmissions?: readonly ThinkingReplayOmission[];
/** Read off the request body that was sent — see wireManifest.ts. */
readonly wireManifest: WireToolManifest;
/**
* What to do with tool arguments that do not parse: return the args to use,
* or throw. A returned placeholder is always accompanied by finite failure
* evidence, so it cannot authorize execution. The gateway keeps its stronger
* whole-response refusal.
*/
readonly onMalformedToolArgs: (call: MalformedToolArgs) => Record<string, unknown>;
}
/**
* Anthropic's streaming event sequence → `LLMChunk`s.
*
* message_start (input usage) → content_block_start (tool_use id + name, or a
* thinking block) → content_block_delta (`text_delta` yielded as a chunk;
* `input_json_delta` / `thinking_delta` / `signature_delta` accumulated per
* content-block INDEX, since blocks can interleave) → content_block_stop (the
* block is finished and parsed) → message_delta (stop reason, output usage).
*
* The TERMINAL chunk (`done: true`) carries the assembled `response` — always,
* because consumers read `chunk.response` for tool calls and usage.
*/
export declare function assembleAnthropicStream(events: AsyncIterable<AnthropicStreamEvent>, options: StreamAssemblyOptions): AsyncIterable<LLMChunk>;