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
JavaScript
;
/**
* 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