UNPKG

agentfootprint

Version:

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

275 lines • 14.3 kB
"use strict"; /** * 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`. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.AnthropicProvider = exports.anthropic = void 0; const lazyRequire_js_1 = require("../../lib/lazyRequire.js"); const abort_js_1 = require("../../lib/abort.js"); const contextWindow_js_1 = require("../../provider-contract/contextWindow.js"); const retryAfter_js_1 = require("./retryAfter.js"); const anthropicCacheWire_js_1 = require("./anthropicCacheWire.js"); const anthropicThinkingWire_js_1 = require("./anthropicThinkingWire.js"); const anthropicModels_js_1 = require("./anthropicModels.js"); const anthropicThinkingReplay_js_1 = require("./anthropicThinkingReplay.js"); const wireManifest_js_1 = require("./wireManifest.js"); const anthropicToolArguments_js_1 = require("./anthropicToolArguments.js"); const AnthropicThinkingHandler_js_1 = require("../../thinking/AnthropicThinkingHandler.js"); // The request body has ONE owner, shared with browserAnthropic() and // invokeModelGateway() — a private copy here once drifted from it, and a // second copy of the thinking rule would drift the same way. const anthropicMessagesWire_js_1 = require("./anthropicMessagesWire.js"); /** * Which roles this wire carries inside `messages`. * * `'system'` is ABSENT and that is the wire's own rule, not a policy choice: * `toAnthropicMessages` drops a `role: 'system'` message because Anthropic * takes the system prompt as a separate top-level field. Declaring the truth * here is what lets a `slot: 'messages'` injection be refused at run start * rather than silently vanish between the recording and the request. */ const CARRIES_IN_MESSAGES = Object.freeze(['user', 'assistant']); /** * 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(); */ function anthropic(options = {}) { const client = resolveClient(options); const defaultModel = options.defaultModel ?? 'claude-sonnet-4-5-20250929'; const defaultMaxTokens = options.defaultMaxTokens ?? 4096; const parallelToolCalls = options.parallelToolCalls; /** The model a request goes to — the `'anthropic'` shorthand is the default. */ const modelOf = (model) => (model === 'anthropic' ? defaultModel : model); /** The ONE mode function: declared below and used for every body. */ const modeOf = (0, anthropicThinkingWire_js_1.thinkingModeWith)(options.thinkingMode); const buildParams = (req) => { const model = modelOf(req.model); return { model, ...(0, anthropicMessagesWire_js_1.buildMessagesBody)(req, { model, thinkingMode: modeOf, provider: 'anthropic', maxTokensDefault: defaultMaxTokens, ...(parallelToolCalls !== undefined && { parallelToolCalls }), nativeOutputSchema: anthropicModels_js_1.anthropicTakesNativeOutputSchema, }), }; }; const provider = { name: 'anthropic', carriesInMessages: CARRIES_IN_MESSAGES, carriesForcedToolChoice: true, supportsForcedToolChoice: (model) => (0, anthropicModels_js_1.anthropicTakesForcedToolChoice)(modelOf(model)), // `output_config.format` — the `'native'` output strategy — on the models // the structured-outputs page lists (anthropicModels.ts). carriesNativeOutputSchema: true, supportsNativeOutputSchema: (model) => (0, anthropicModels_js_1.anthropicTakesNativeOutputSchema)(modelOf(model)), supportsAutoNativeOutputSchema: (model) => (0, anthropicModels_js_1.anthropicModelFacts)(modelOf(model))?.nativeOutputSchema === true, // Explicit `cache_control` breakpoints, four per request, usage reported — // the agent's cache strategy is chosen from this, never from `name`. promptCaching: anthropicCacheWire_js_1.ANTHROPIC_PROMPT_CACHING, // The signed thinking blocks this wire returns, normalized for the echo. thinkingHandler: AnthropicThinkingHandler_js_1.anthropicThinkingHandler, // Which thinking request each model takes — budget, adaptive or none // (anthropicThinkingWire.ts); `buildMessagesBody` sends that shape, from // the same function. thinkingMode: (model) => modeOf(modelOf(model)), async complete(req, hooks) { // A cancelled call sends nothing (lib/abort.ts, law 1). (0, abort_js_1.throwIfAborted)(req.signal); const preparation = (0, anthropicThinkingReplay_js_1.prepareAnthropicReplay)(req, modelOf(req.model), 'anthropic'); const params = buildParams(preparation.request); const bindingFactory = (0, anthropicThinkingReplay_js_1.thinkingReplayBinding)(params, params.model); if (preparation.omissions.length > 0) hooks?.onReplayPrepared?.(preparation); try { // The signal rides the SDK's request options, so an abort cancels the // HTTP request itself; the race refuses an answer that lands after it. const message = await (0, abort_js_1.untilAborted)(req.signal ? client.messages.create(params, { signal: req.signal }) : client.messages.create(params), req.signal); // Manifest read from the FINAL params — after tool mapping and cache // markers — because the whole point is catching what the body says, // not what the request intended (wireManifest.ts). return { ...(0, anthropicMessagesWire_js_1.fromAnthropicResponse)(message, message.id ? bindingFactory(message.id) : undefined), wireManifest: (0, wireManifest_js_1.toolManifestOf)(params.tools, params.output_config), ...(preparation.omissions.length > 0 && { replayOmissions: preparation.omissions }), }; } catch (err) { throw wrapError(err, req.signal); } }, async *stream(req, hooks) { (0, abort_js_1.throwIfAborted)(req.signal); const preparation = (0, anthropicThinkingReplay_js_1.prepareAnthropicReplay)(req, modelOf(req.model), 'anthropic'); const params = buildParams(preparation.request); const bindingFactory = (0, anthropicThinkingReplay_js_1.thinkingReplayBinding)(params, params.model); if (preparation.omissions.length > 0) hooks?.onReplayPrepared?.(preparation); let stream; try { stream = req.signal ? client.messages.stream(params, { signal: req.signal }) : client.messages.stream(params); } catch (err) { throw wrapError(err, req.signal); } const opened = stream; let tokenIndex = 0; const toolArguments = new anthropicToolArguments_js_1.AnthropicToolArguments(); try { // An abort closes the SDK stream and ends this loop at once, even // when a read is still pending (lib/abort.ts, laws 2–3). for await (const event of (0, abort_js_1.abortableIterable)(opened, req.signal, () => opened.abort?.())) { toolArguments.observe(event); if (event.type === 'content_block_delta' && event.delta?.type === 'text_delta' && event.delta.text) { yield { tokenIndex, content: event.delta.text, done: false }; tokenIndex++; } } const final = await (0, abort_js_1.untilAborted)(opened.finalMessage(), req.signal); toolArguments.assertTerminal(final.content.some((block) => block.type === 'tool_use')); const argumentFailures = new Map(); const checkedMessage = { ...final, content: final.content.map((block, index) => { if (block.type !== 'tool_use') return block; const checked = toolArguments.finish(index, block); if (checked.failure) argumentFailures.set(index, checked.failure); return { ...block, input: checked.input }; }), }; const response = { ...(0, anthropicMessagesWire_js_1.fromAnthropicResponse)(checkedMessage, final.id ? bindingFactory(final.id) : undefined, argumentFailures), wireManifest: (0, wireManifest_js_1.toolManifestOf)(params.tools, params.output_config), ...(preparation.omissions.length > 0 && { replayOmissions: preparation.omissions }), }; yield { tokenIndex, content: '', done: true, response }; } catch (err) { throw wrapError(err, req.signal); } }, }; return provider; } exports.anthropic = anthropic; /** * Class form for consumers who prefer `new AnthropicProvider(...)` over * the `anthropic(...)` factory. Identical behavior; trivial wrapper. */ class AnthropicProvider { name = 'anthropic'; carriesInMessages = CARRIES_IN_MESSAGES; carriesForcedToolChoice = true; /** The inner declaration resolves this adapter's shorthand before looking up the model. */ supportsForcedToolChoice; carriesNativeOutputSchema = true; /** The inner declaration resolves this adapter's shorthand before looking up the model. */ supportsNativeOutputSchema; supportsAutoNativeOutputSchema; promptCaching = anthropicCacheWire_js_1.ANTHROPIC_PROMPT_CACHING; thinkingHandler = AnthropicThinkingHandler_js_1.anthropicThinkingHandler; /** The inner provider's own function — a closure, safe to forward unbound. */ thinkingMode; inner; constructor(options = {}) { this.inner = anthropic(options); this.thinkingMode = this.inner.thinkingMode; this.supportsForcedToolChoice = this.inner.supportsForcedToolChoice; this.supportsNativeOutputSchema = this.inner.supportsNativeOutputSchema; this.supportsAutoNativeOutputSchema = this.inner.supportsAutoNativeOutputSchema ?? (() => false); } // `hooks` is FORWARDED, not dropped — see LLMCallHooks in provider-contract/types.ts. complete(req, hooks) { return this.inner.complete(req, hooks); } stream(req, hooks) { if (!this.inner.stream) { throw new Error('stream() unavailable on inner provider'); } return this.inner.stream(req, hooks); } } exports.AnthropicProvider = AnthropicProvider; // ─── Internals ────────────────────────────────────────────────────── function resolveClient(options) { if (options._client) return options._client; let Anthropic; try { const mod = (0, lazyRequire_js_1.lazyRequire)('@anthropic-ai/sdk'); Anthropic = (mod.default ?? mod); } catch { throw new Error('AnthropicProvider requires @anthropic-ai/sdk.\n' + ' Install: npm install @anthropic-ai/sdk\n' + ' Or pass `_client` for test injection.'); } const apiKey = options.apiKey ?? process.env.ANTHROPIC_API_KEY; return new Anthropic({ apiKey, ...(options.timeout !== undefined ? { timeout: options.timeout } : {}), ...(options.maxRetries !== undefined ? { maxRetries: options.maxRetries } : {}), }); } function wrapError(err, signal) { // A cancelled call fails AS the cancellation, whatever the SDK threw for it // (its own `APIUserAbortError` included) — never as a provider error that a // retry would re-send (lib/abort.ts). if (signal?.aborted) return (0, abort_js_1.abortErrorOf)(signal); // See OpenAIProvider.wrapError — one shared detector, one typed error, so // "prompt is too long" reads the same whichever vendor said it. const tooBig = (0, contextWindow_js_1.asContextWindowExceeded)(err, { provider: 'anthropic' }); if (tooBig) return tooBig; if (err instanceof Error) { // The wait the response stated (retry-after-ms / retry-after), declared // for withRetry — absent when none, so the error shape is unchanged. const retryAfterMs = (0, retryAfter_js_1.retryAfterMsFromError)(err); return Object.assign(new Error(`[anthropic] ${err.message}`), { name: 'AnthropicProviderError', cause: err, // Preserve `status` if the SDK attached one — withRetry uses it. status: err.status, ...(retryAfterMs !== undefined && { retryAfterMs }), }); } return new Error(`[anthropic] ${String(err)}`); } //# sourceMappingURL=AnthropicProvider.js.map