UNPKG

agentfootprint

Version:

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

292 lines • 14.6 kB
"use strict"; /** * BrowserAnthropicProvider — fetch-based Anthropic adapter for browsers. * * Pattern: Adapter (GoF). Zero peer dependencies — uses global `fetch`. * Role: Outer ring. Same `LLMProvider` contract as `AnthropicProvider`, * but skips `@anthropic-ai/sdk` (which doesn't bundle cleanly * for browser). Aimed at playgrounds / prototypes where the * user supplies their own key. * Emits: N/A. * * Anthropic requires the `anthropic-dangerous-direct-browser-access: true` * header for direct browser-to-API calls. This is intentional — production * apps should proxy through a backend. * * ─── Limitations ──────────────────────────────────────────────────── * * • Multi-modal NOT supported. * • Browser CORS — works because Anthropic explicitly allows the * dangerous-direct header. Future API changes could require a proxy. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.BrowserAnthropicProvider = exports.browserAnthropic = void 0; const contextWindow_js_1 = require("../../provider-contract/contextWindow.js"); const abort_js_1 = require("../../lib/abort.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 AnthropicThinkingHandler_js_1 = require("../../thinking/AnthropicThinkingHandler.js"); const anthropicMessagesWire_js_1 = require("./anthropicMessagesWire.js"); const wireManifest_js_1 = require("./wireManifest.js"); const ANTHROPIC_API_URL = 'https://api.anthropic.com/v1/messages'; const ANTHROPIC_API_VERSION = '2023-06-01'; /** * 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']); function browserAnthropic(options) { const apiKey = options.apiKey; if (!apiKey) { throw new Error('BrowserAnthropicProvider requires `apiKey`. Browser providers do not read environment variables.'); } const apiUrl = options.apiUrl ?? ANTHROPIC_API_URL; const defaultModel = options.defaultModel ?? 'claude-sonnet-4-5-20250929'; const defaultMaxTokens = options.defaultMaxTokens ?? 4096; const parallelToolCalls = options.parallelToolCalls; const fetchImpl = options._fetch ?? fetch; /** The ONE mode function: declared below and used for every body. */ const modeOf = (0, anthropicThinkingWire_js_1.thinkingModeWith)(options.thinkingMode); const headers = { 'content-type': 'application/json', 'x-api-key': apiKey, 'anthropic-version': ANTHROPIC_API_VERSION, 'anthropic-dangerous-direct-browser-access': 'true', }; const provider = { name: 'browser-anthropic', carriesInMessages: CARRIES_IN_MESSAGES, carriesForcedToolChoice: true, supportsForcedToolChoice: (model) => (0, anthropicModels_js_1.anthropicTakesForcedToolChoice)(modelOf(model, defaultModel)), // `output_config.format` — the `'native'` output strategy — per model, from // the same table (anthropicModels.ts) the body is built with. carriesNativeOutputSchema: true, supportsNativeOutputSchema: (model) => (0, anthropicModels_js_1.anthropicTakesNativeOutputSchema)(modelOf(model, defaultModel)), supportsAutoNativeOutputSchema: (model) => (0, anthropicModels_js_1.anthropicModelFacts)(modelOf(model, defaultModel))?.nativeOutputSchema === true, // The same body `anthropic()` builds — `buildMessagesBody` applies the // markers — so the same declaration (see anthropicCacheWire.ts). promptCaching: anthropicCacheWire_js_1.ANTHROPIC_PROMPT_CACHING, thinkingHandler: AnthropicThinkingHandler_js_1.anthropicThinkingHandler, // …and the same per-model thinking shapes (anthropicThinkingWire.ts). thinkingMode: (model) => modeOf(modelOf(model, defaultModel)), 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, defaultModel), 'browser-anthropic'); const body = { ...buildBody(preparation.request, defaultModel, defaultMaxTokens, modeOf, parallelToolCalls), }; const bindingFactory = (0, anthropicThinkingReplay_js_1.thinkingReplayBinding)(body, body.model); const serializedBody = JSON.stringify(body); if (preparation.omissions.length > 0) hooks?.onReplayPrepared?.(preparation); let response; try { // The signal rides the fetch, so an abort cancels the request itself; // every wait after it (headers, body) is raced against it as well, so // an answer that lands after the abort is refused (lib/abort.ts). response = await (0, abort_js_1.untilAborted)(fetchImpl(apiUrl, { method: 'POST', headers, body: serializedBody, ...(req.signal && { signal: req.signal }), }), req.signal); } catch (err) { throw wrapError(err, req.signal); } if (!response.ok) throw await (0, abort_js_1.untilAborted)(wrapStatus(response), req.signal); const json = (await (0, abort_js_1.untilAborted)(response.json(), req.signal)); // Manifest read from the FINAL body — the very object JSON.stringify // sent — after every transform (wireManifest.ts). return { ...(0, anthropicMessagesWire_js_1.fromAnthropicResponse)(json, json.id ? bindingFactory(json.id) : undefined), wireManifest: (0, wireManifest_js_1.toolManifestOf)(body.tools, body.output_config), ...(preparation.omissions.length > 0 && { replayOmissions: preparation.omissions }), }; }, async *stream(req, hooks) { (0, abort_js_1.throwIfAborted)(req.signal); const preparation = (0, anthropicThinkingReplay_js_1.prepareAnthropicReplay)(req, modelOf(req.model, defaultModel), 'browser-anthropic'); const body = { ...buildBody(preparation.request, defaultModel, defaultMaxTokens, modeOf, parallelToolCalls), stream: true, }; const bindingFactory = (0, anthropicThinkingReplay_js_1.thinkingReplayBinding)(body, body.model); const serializedBody = JSON.stringify(body); if (preparation.omissions.length > 0) hooks?.onReplayPrepared?.(preparation); let response; try { response = await (0, abort_js_1.untilAborted)(fetchImpl(apiUrl, { method: 'POST', headers, body: serializedBody, ...(req.signal && { signal: req.signal }), }), req.signal); } catch (err) { throw wrapError(err, req.signal); } if (!response.ok) throw await (0, abort_js_1.untilAborted)(wrapStatus(response), req.signal); if (!response.body) throw new Error('[browser-anthropic] response has no body'); // Malformed tool-argument JSON becomes `{}` here — this adapter's // behaviour since it shipped (the gateway adapter refuses instead). // The signal rides INTO the parser: each read is raced against it, and // the body is cancelled on the way out (lib/abort.ts · bodyChunks). yield* (0, anthropicMessagesWire_js_1.assembleAnthropicStream)(parseSSE(response.body, req.signal), { bindingFactory, replayOmissions: preparation.omissions, wireManifest: (0, wireManifest_js_1.toolManifestOf)(body.tools, body.output_config), onMalformedToolArgs: () => ({}), }); }, }; return provider; } exports.browserAnthropic = browserAnthropic; class BrowserAnthropicProvider { name = 'browser-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 = browserAnthropic(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'); return this.inner.stream(req, hooks); } } exports.BrowserAnthropicProvider = BrowserAnthropicProvider; // ─── Internals ────────────────────────────────────────────────────── /** The model a request goes to — either shorthand is the default model. */ function modelOf(model, defaultModel) { return model === 'anthropic' || model === 'browser-anthropic' ? defaultModel : model; } function buildBody(req, defaultModel, defaultMaxTokens, thinkingMode, parallelToolCalls) { const model = modelOf(req.model, defaultModel); // `model` first, then the shared Messages body (anthropicMessagesWire.ts). return { model, ...(0, anthropicMessagesWire_js_1.buildMessagesBody)(req, { model, thinkingMode, provider: 'browser-anthropic', maxTokensDefault: defaultMaxTokens, ...(parallelToolCalls !== undefined && { parallelToolCalls }), nativeOutputSchema: anthropicModels_js_1.anthropicTakesNativeOutputSchema, }), }; } /** * Parse Anthropic's SSE event stream from a fetch ReadableStream — read under * `signal` through `bodyChunks`, which races every read against it and * CANCELS the body on every way out (the end, an early stop, an error, an * abort), so a stream nobody reads never holds its socket open. */ async function* parseSSE(body, signal) { const decoder = new TextDecoder(); let buf = ''; for await (const value of (0, abort_js_1.bodyChunks)(body, signal)) { buf += decoder.decode(value, { stream: true }); let idx; // Events are separated by \n\n. while ((idx = buf.indexOf('\n\n')) >= 0) { (0, abort_js_1.throwIfAborted)(signal); const raw = buf.slice(0, idx); buf = buf.slice(idx + 2); let event = 'message'; const dataLines = []; for (const line of raw.split('\n')) { if (line.startsWith('event:')) event = line.slice(6).trim(); else if (line.startsWith('data:')) dataLines.push(line.slice(5).trim()); } // Belt-and-suspenders: a malformed SSE chunk should not throw out of // the async generator and tear down the whole stream. v1 wrapped // this; an upstream proxy or a partial flush could in principle // produce a non-JSON `data:` line. let data = {}; if (dataLines.length > 0) { try { data = JSON.parse(dataLines.join('\n')); } catch { data = {}; } } yield { event, data }; } } } async function wrapStatus(response) { let bodyText = ''; try { bodyText = await response.text(); } catch { /* ignore */ } // Same as the browser OpenAI adapter: an HTTP refusal never reaches // `wrapError`, and the body is where "prompt is too long" is written. const tooBig = (0, contextWindow_js_1.asContextWindowExceeded)(new Error(bodyText), { provider: 'browser-anthropic', bodyText, status: response.status, }); if (tooBig) return tooBig; return Object.assign(new Error(`[browser-anthropic] ${response.status} ${response.statusText} — ${bodyText.slice(0, 200)}`), { name: 'BrowserAnthropicProviderError', status: response.status, }); } function wrapError(err, signal) { // A cancelled call fails AS the cancellation, never as a provider error that // a retry would re-send (lib/abort.ts). if (signal?.aborted) return (0, abort_js_1.abortErrorOf)(signal); const tooBig = (0, contextWindow_js_1.asContextWindowExceeded)(err, { provider: 'browser-anthropic' }); if (tooBig) return tooBig; if (err instanceof Error) { return Object.assign(new Error(`[browser-anthropic] ${err.message}`), { name: 'BrowserAnthropicProviderError', cause: err, }); } return new Error(`[browser-anthropic] ${String(err)}`); } //# sourceMappingURL=BrowserAnthropicProvider.js.map