agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
159 lines (158 loc) • 7.65 kB
TypeScript
/**
* AnthropicProvider — wraps `@anthropic-ai/sdk` as an `LLMProvider`.
*
* Pattern: Adapter (GoF) + Ports-and-Adapters (Cockburn 2005).
* Role: Outer ring — translates `LLMRequest`/`LLMResponse` to/from
* Anthropic's Messages API. Knows nothing about agents,
* recorders, or compositions.
* Emits: N/A — providers don't emit; recorders observe via Agent.
*
* ─── Limitations ────────────────────────────────────────────────────
*
* • Multi-modal content (images, video) NOT supported — the framework's
* `LLMMessage.content` is `string`. The adapter accepts text-only.
* May extend in a future release the message shape; this provider will be updated
* in lockstep.
* • `responseFormat` (JSON-Schema-coerced output) NOT exposed
* — consumers can pass schema instructions via `systemPrompt`.
*/
import type { LLMCallHooks, LLMChunk, LLMProvider, LLMRequest, LLMResponse, WireRole } from '../../provider-contract/types.js';
import type { ThinkingMode } from '../../thinking/types.js';
import { type AnthropicMessage, type AnthropicMessagesBody } from './anthropicMessagesWire.js';
interface AnthropicClient {
messages: {
create(params: AnthropicCreateParams, options?: AnthropicRequestOptions): Promise<AnthropicMessage>;
stream(params: AnthropicCreateParams, options?: AnthropicRequestOptions): AnthropicStream;
};
}
/** The SDK's per-request options — the one this adapter sends is the signal,
* so an aborted run cancels the HTTP request itself (`lib/abort.ts`). */
interface AnthropicRequestOptions {
signal?: AbortSignal;
}
/** The shared Messages body (anthropicMessagesWire.ts) plus the `model` the
* SDK sends — `system`, `tools`, `thinking`, `tool_choice` and the cache
* markers are all decided there. */
interface AnthropicCreateParams extends AnthropicMessagesBody {
model: string;
}
interface AnthropicStream {
finalMessage(): Promise<AnthropicMessage>;
[Symbol.asyncIterator](): AsyncIterator<AnthropicStreamEvent>;
/** The SDK's own close: aborts the request and ends the stream. */
abort?(): void;
}
interface AnthropicStreamEvent {
type: string;
delta?: {
type: string;
text?: string;
};
}
export interface AnthropicProviderOptions {
/** API key. Defaults to `ANTHROPIC_API_KEY` env var. */
readonly apiKey?: string;
/**
* Default model used when `LLMRequest.model` is `'anthropic'` (the
* shorthand). When the request specifies a full model id, that wins.
*/
readonly defaultModel?: string;
/** Default max tokens when the request doesn't set it. Default 4096. */
readonly defaultMaxTokens?: number;
/**
* Per-request timeout in milliseconds, passed to the Anthropic client.
* Long non-streaming turns (slow models, long conversations, agent loops)
* can exceed the SDK default and surface as "Request timed out" — raise
* this for such workloads. Omit to use the SDK default.
*/
readonly timeout?: number;
/**
* How many times the Anthropic client retries a failed request (timeouts,
* connection errors, 429/5xx) before giving up. Omit to use the SDK default.
*/
readonly maxRetries?: number;
/**
* May the model ask for several tools at once in a single reply?
*
* Anthropic's default is `true`: one assistant message can carry many
* `tool_use` blocks, and the agent runs them all inside ONE loop
* iteration. Set `false` to cap it at one tool per reply — the model
* still chooses which tool (or none), it just cannot batch.
*
* Set `false` when the SHAPE of the loop is part of what you are
* measuring, not only its answer. A batched reply collapses several
* tool results into one iteration, so per-iteration analysis
* (`localizeContextBug` seeds one `'tool'` suspect per iteration from
* `lastToolResult`, and `removableSources` de-duplicates by tool name)
* attributes that iteration to the LAST tool of the batch — the others
* never appear as separate influence rows and cannot be ablated
* individually. One tool per iteration keeps every source separately
* attributable, at the cost of one extra round trip per tool.
*
* Prompting for it is not equivalent: "call one tool at a time" in the
* system prompt is a request the model may ignore, whereas this is a
* request parameter the API enforces.
*
* `true` (and omitting the option) sends nothing — Anthropic's own
* default already allows batching. Only `false` puts `tool_choice` on
* the wire, and only on requests that actually carry tools.
*
* @default undefined (Anthropic's default — batching allowed)
*/
readonly parallelToolCalls?: boolean;
/**
* Which thinking request a model takes, where the built-in table is not
* the answer you want: an id it cannot read (a proxy's alias), or a model
* that takes both shapes and that you want adaptive (Opus 4.6 and Sonnet
* 4.6 get a budget by default; in budget mode Opus 4.6 does not think
* between tool calls). Return `undefined` to use the table, which reads an
* unknown id as adaptive. The provider declares the result
* (`provider.thinkingMode`) AND sends that shape, so the agent's build
* check and the wire cannot disagree.
*
* @example
* anthropic({ thinkingMode: (id) => (id.startsWith('claude-opus-4-6') ? 'adaptive' : undefined) })
*/
readonly thinkingMode?: (model: string) => ThinkingMode | undefined;
/** @internal Pre-built client for testing. Skips SDK import. */
readonly _client?: AnthropicClient;
}
/**
* Build an `LLMProvider` backed by Anthropic's Messages API.
*
* @example
* import { Agent } from 'agentfootprint';
* import { anthropic } from 'agentfootprint/providers';
*
* const agent = Agent.create({
* provider: anthropic({ defaultModel: 'claude-sonnet-4-5-20250929' }),
* model: 'anthropic',
* })
* .tool(weatherTool)
* .build();
*/
export declare function anthropic(options?: AnthropicProviderOptions): LLMProvider;
/**
* Class form for consumers who prefer `new AnthropicProvider(...)` over
* the `anthropic(...)` factory. Identical behavior; trivial wrapper.
*/
export declare class AnthropicProvider implements LLMProvider {
readonly name = "anthropic";
readonly carriesInMessages: readonly WireRole[];
readonly carriesForcedToolChoice = true;
/** The inner declaration resolves this adapter's shorthand before looking up the model. */
readonly supportsForcedToolChoice: (model: string) => boolean;
readonly carriesNativeOutputSchema = true;
/** The inner declaration resolves this adapter's shorthand before looking up the model. */
readonly supportsNativeOutputSchema: (model: string) => boolean;
readonly supportsAutoNativeOutputSchema: (model: string) => boolean;
readonly promptCaching: import("../../cache/types.js").PromptCaching;
readonly thinkingHandler: import("../../thinking/types.js").ThinkingHandler;
/** The inner provider's own function — a closure, safe to forward unbound. */
readonly thinkingMode: (model: string) => ThinkingMode;
private readonly inner;
constructor(options?: AnthropicProviderOptions);
complete(req: LLMRequest, hooks?: LLMCallHooks): Promise<LLMResponse>;
stream(req: LLMRequest, hooks?: LLMCallHooks): AsyncIterable<LLMChunk>;
}
export {};