@juspay/neurolink 11.8.1 → 11.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,313 @@
1
+ /**
2
+ * Direct Anthropic's adapter onto the shared agentic loop engine.
3
+ *
4
+ * Everything here is the wire-format half of the turn: building one Messages
5
+ * request, folding an SSE event sequence into content blocks, serializing tool
6
+ * results back into the conversation, and mapping the stop reason. The turn
7
+ * loop, the step cap, tool dispatch, retry and usage accumulation belong to
8
+ * `runAgenticLoop`.
9
+ *
10
+ * Two behaviours here are easy to lose in a migration and are called out
11
+ * because losing either is silent:
12
+ *
13
+ * - Thinking deltas ride the channel as `{ content: "", reasoning }`. The
14
+ * engine's chunk type carries `reasoning` for exactly this reason; a
15
+ * channel that only understood `content` would drop every thinking delta
16
+ * while the text path kept working.
17
+ * - `resolveToolOnMiss` is wired to real deferred-catalog hydration, not as
18
+ * interface decoration. The pre-migration loop resolves every tool call as
19
+ * `toolsRecord[name] ?? resolveDeferredTool(toolsRecord, name)`, which is
20
+ * how a cataloged tool the model calls without having loaded it via
21
+ * `search_tools` gets found. Dropping it would break those tools with a
22
+ * "Tool not found" that looks like a hallucination.
23
+ */
24
+ import { resolveDeferredTool } from "../../tools/toolDiscovery.js";
25
+ import { stringifyAnthropicToolOutput } from "./toolOutput.js";
26
+ /** Map Anthropic's stop_reason onto the unified finish reason. */
27
+ function mapAnthropicFinishReason(rawStopReason, hadToolCallsAtCap) {
28
+ switch (rawStopReason) {
29
+ case "max_tokens":
30
+ return "length";
31
+ case "tool_use":
32
+ return "tool-calls";
33
+ case "refusal":
34
+ return "content-filter";
35
+ default:
36
+ return hadToolCallsAtCap ? "tool-calls" : "stop";
37
+ }
38
+ }
39
+ export function createAnthropicLoopAdapter(config) {
40
+ return {
41
+ providerLabel: "anthropic",
42
+ maxSteps: config.maxSteps,
43
+ ...(config.toolFailureBreaker
44
+ ? { toolFailureBreaker: config.toolFailureBreaker }
45
+ : {}),
46
+ /**
47
+ * The engine decides WHEN to reclaim; the provider decides HOW, in its own
48
+ * concrete message types. Not optional dressing: this loop appends an
49
+ * assistant tool_use message and a user tool_result message every step,
50
+ * and nothing else bounds that growth.
51
+ */
52
+ ...(config.planReclaim
53
+ ? {
54
+ planReclaim: (conversation, step) => {
55
+ const reclaimed = config.planReclaim?.(conversation, step);
56
+ return reclaimed ? { conversation: reclaimed } : undefined;
57
+ },
58
+ }
59
+ : {}),
60
+ resolveToolOnMiss: (name) => {
61
+ const hydrated = resolveDeferredTool(config.toolsRecord, name);
62
+ const execute = hydrated?.execute;
63
+ if (!execute) {
64
+ return undefined;
65
+ }
66
+ // Wrapped rather than handed over directly: the engine's hook types
67
+ // `opts` as `unknown`, and a function declaring a narrower options type
68
+ // is not assignable to one accepting `unknown`. One assertion at the
69
+ // boundary, never a double assertion through `unknown`.
70
+ return {
71
+ execute: async (args, opts) => execute(args, opts),
72
+ };
73
+ },
74
+ buildStepRequest(conversation, step) {
75
+ return { raw: config.buildParams(conversation, step) };
76
+ },
77
+ async executeStep(request, channel, signal) {
78
+ const params = request.raw;
79
+ // Single assertion, not a double: `messages.create` returns a union of
80
+ // Message and Stream, and Stream<RawMessageStreamEvent> already IS an
81
+ // AsyncIterable of that event, so the two types overlap and the
82
+ // compiler still checks the narrowing.
83
+ const events = (await config.client.messages.create({ ...params, stream: true },
84
+ // The engine's signal, not config's: runAgenticLoop already derives
85
+ // it from the caller's abortSignal, so preferring config's would
86
+ // ignore engine-initiated cancellation entirely.
87
+ { signal }));
88
+ const textByIndex = new Map();
89
+ const toolByIndex = new Map();
90
+ const thinkingByIndex = new Map();
91
+ const redactedByIndex = new Map();
92
+ let text = "";
93
+ let reasoning = "";
94
+ let rawStopReason;
95
+ let inputTokens = 0;
96
+ let outputTokens = 0;
97
+ let cacheReadTokens = 0;
98
+ let cacheWriteTokens = 0;
99
+ let stepOutputTokens = 0;
100
+ for await (const rawEvent of events) {
101
+ if (signal.aborted) {
102
+ break;
103
+ }
104
+ // Narrowed through the SDK's own discriminated union rather than
105
+ // re-declared: `RawMessageStreamEvent` already describes every event
106
+ // shape, so `event.type` checks below are compiler-checked instead of
107
+ // asserted.
108
+ const event = rawEvent;
109
+ if (event.type === "message_start") {
110
+ const usage = event.message?.usage;
111
+ inputTokens += usage?.input_tokens ?? 0;
112
+ const startOutput = usage?.output_tokens ?? 0;
113
+ outputTokens += startOutput - stepOutputTokens;
114
+ stepOutputTokens = startOutput;
115
+ // Anthropic reports cache reads/writes separately from input_tokens
116
+ // on this same event; without these the stream drops all cache
117
+ // accounting.
118
+ cacheReadTokens += usage?.cache_read_input_tokens ?? 0;
119
+ cacheWriteTokens += usage?.cache_creation_input_tokens ?? 0;
120
+ // The guard calibrates from the FULL prompt size, not input_tokens
121
+ // alone: on a cache hit the uncached remainder is tiny and using it
122
+ // would let the guard drift far under the real cost.
123
+ config.noteObservedPromptTokens?.((usage?.input_tokens ?? 0) +
124
+ (usage?.cache_read_input_tokens ?? 0) +
125
+ (usage?.cache_creation_input_tokens ?? 0));
126
+ continue;
127
+ }
128
+ if (event.type === "content_block_start") {
129
+ const index = event.index ?? 0;
130
+ // A redacted_thinking block carries its whole payload here and
131
+ // produces no deltas, so if it is not captured on this event it is
132
+ // never seen again. Anthropic validates the thinking chain when
133
+ // extended thinking continues across a tool-use turn, so a missing
134
+ // one fails the NEXT request — and only for accounts where safety
135
+ // redaction actually triggers, which is why it survives testing.
136
+ if (event.content_block?.type === "redacted_thinking") {
137
+ const data = event.content_block.data;
138
+ if (typeof data === "string") {
139
+ redactedByIndex.set(index, data);
140
+ }
141
+ }
142
+ if (event.content_block?.type === "tool_use") {
143
+ toolByIndex.set(index, {
144
+ id: event.content_block.id ?? "",
145
+ name: event.content_block.name ?? "",
146
+ inputJson: "",
147
+ });
148
+ }
149
+ continue;
150
+ }
151
+ if (event.type === "content_block_delta") {
152
+ const index = event.index ?? 0;
153
+ const delta = event.delta;
154
+ if (delta?.type === "text_delta" && delta.text) {
155
+ text += delta.text;
156
+ textByIndex.set(index, (textByIndex.get(index) ?? "") + delta.text);
157
+ channel.push({ content: delta.text });
158
+ }
159
+ else if (delta?.type === "thinking_delta" && delta.thinking) {
160
+ const acc = thinkingByIndex.get(index) ?? {
161
+ text: "",
162
+ signature: "",
163
+ };
164
+ acc.text += delta.thinking;
165
+ thinkingByIndex.set(index, acc);
166
+ reasoning += delta.thinking;
167
+ // Reasoning rides its own field; `content` stays a present string
168
+ // so plain-text consumers are unaffected.
169
+ channel.push({ content: "", reasoning: delta.thinking });
170
+ }
171
+ else if (delta?.type === "signature_delta" && delta.signature) {
172
+ const acc = thinkingByIndex.get(index) ?? {
173
+ text: "",
174
+ signature: "",
175
+ };
176
+ acc.signature += delta.signature;
177
+ thinkingByIndex.set(index, acc);
178
+ }
179
+ else if (delta?.type === "input_json_delta" && delta.partial_json) {
180
+ const pending = toolByIndex.get(index);
181
+ if (pending) {
182
+ pending.inputJson += delta.partial_json;
183
+ }
184
+ }
185
+ continue;
186
+ }
187
+ if (event.type === "message_delta") {
188
+ rawStopReason = event.delta?.stop_reason ?? rawStopReason;
189
+ const cumulative = event.usage?.output_tokens ?? stepOutputTokens;
190
+ outputTokens += cumulative - stepOutputTokens;
191
+ stepOutputTokens = cumulative;
192
+ }
193
+ }
194
+ // JSON.parse happily yields null, an array or a primitive, and an
195
+ // assertion converts none of them. Both `tool_use.input` and
196
+ // AgenticLoopToolCall.args require an object, so anything else becomes
197
+ // {} rather than being passed through as invalid wire content.
198
+ const parseArgs = (json) => {
199
+ if (!json) {
200
+ return {};
201
+ }
202
+ try {
203
+ const parsed = JSON.parse(json);
204
+ return typeof parsed === "object" &&
205
+ parsed !== null &&
206
+ !Array.isArray(parsed)
207
+ ? parsed
208
+ : {};
209
+ }
210
+ catch {
211
+ return {};
212
+ }
213
+ };
214
+ // Rebuild the assistant turn in wire order so it can be replayed as the
215
+ // assistant message on the next step. Text that arrived before a tool
216
+ // call has to survive here, or the model stops seeing its own reasoning
217
+ // mid-turn.
218
+ const blocks = [];
219
+ const indices = new Set([
220
+ ...textByIndex.keys(),
221
+ ...toolByIndex.keys(),
222
+ ...thinkingByIndex.keys(),
223
+ ...redactedByIndex.keys(),
224
+ ]);
225
+ for (const index of [...indices].sort((a, b) => a - b)) {
226
+ // Thinking blocks are replayed with their signature. Anthropic
227
+ // validates that signature when extended thinking continues across
228
+ // turns, so dropping the block — or keeping the text without the
229
+ // signature — breaks the next step of a thinking turn.
230
+ const redacted = redactedByIndex.get(index);
231
+ if (redacted) {
232
+ blocks.push({ type: "redacted_thinking", data: redacted });
233
+ }
234
+ const thinking = thinkingByIndex.get(index);
235
+ // Both halves required: Anthropic's thinking block carries a
236
+ // mandatory signature, and replaying one with an empty string is
237
+ // rejected outright. A thinking block that never received a
238
+ // signature_delta is dropped rather than sent unsigned.
239
+ if (thinking?.text && thinking.signature) {
240
+ blocks.push({
241
+ type: "thinking",
242
+ thinking: thinking.text,
243
+ signature: thinking.signature,
244
+ });
245
+ }
246
+ const blockText = textByIndex.get(index);
247
+ if (blockText) {
248
+ blocks.push({
249
+ type: "text",
250
+ text: blockText,
251
+ });
252
+ }
253
+ const pending = toolByIndex.get(index);
254
+ if (pending) {
255
+ blocks.push({
256
+ type: "tool_use",
257
+ id: pending.id,
258
+ name: pending.name,
259
+ input: parseArgs(pending.inputJson),
260
+ });
261
+ }
262
+ }
263
+ const allCalls = [...toolByIndex.values()].map((pending) => ({
264
+ id: pending.id,
265
+ name: pending.name,
266
+ args: parseArgs(pending.inputJson),
267
+ }));
268
+ // A terminal structured-output call ends the turn: its arguments are the
269
+ // answer. Reporting it as text and leaving it out of `toolCalls` is what
270
+ // routes it through the engine's ordinary zero-tool-calls exit, so it is
271
+ // never dispatched, never counted against the breaker, and never shows
272
+ // up as a tool execution.
273
+ const terminal = config.finalResultToolName
274
+ ? allCalls.find((call) => call.name === config.finalResultToolName)
275
+ : undefined;
276
+ const toolCalls = terminal ? [] : allCalls;
277
+ const finalText = terminal ? JSON.stringify(terminal.args) : text;
278
+ return {
279
+ text: finalText,
280
+ ...(reasoning ? { reasoning } : {}),
281
+ toolCalls,
282
+ usage: {
283
+ inputTokens,
284
+ outputTokens,
285
+ cacheReadTokens,
286
+ cacheWriteTokens,
287
+ },
288
+ rawStopReason,
289
+ raw: blocks,
290
+ };
291
+ },
292
+ buildToolResultMessages(conversation, stepResult, toolResults) {
293
+ const assistantMessage = {
294
+ role: "assistant",
295
+ content: stepResult.raw,
296
+ };
297
+ const resultMessage = {
298
+ role: "user",
299
+ content: toolResults.map((result) => ({
300
+ type: "tool_result",
301
+ tool_use_id: result.id,
302
+ content: result.error
303
+ ? `Error executing tool ${result.name}: ${result.error}`
304
+ : stringifyAnthropicToolOutput(result.output),
305
+ ...(result.error ? { is_error: true } : {}),
306
+ })),
307
+ };
308
+ return [...conversation, assistantMessage, resultMessage];
309
+ },
310
+ mapFinishReason: mapAnthropicFinishReason,
311
+ };
312
+ }
313
+ //# sourceMappingURL=loopAdapter.js.map
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Tool-result serialization shared by Anthropic's client and its loop adapter.
3
+ *
4
+ * Extracted rather than exported from `client.ts` because the adapter is
5
+ * imported BY the client: importing back the other way would close a cycle.
6
+ * The behaviour is unchanged from the module-private version it replaces.
7
+ */
8
+ /** Serialize a tool-result `output` into text for a tool_result block. */
9
+ export declare const stringifyAnthropicToolOutput: (output: unknown) => string;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Tool-result serialization shared by Anthropic's client and its loop adapter.
3
+ *
4
+ * Extracted rather than exported from `client.ts` because the adapter is
5
+ * imported BY the client: importing back the other way would close a cycle.
6
+ * The behaviour is unchanged from the module-private version it replaces.
7
+ */
8
+ /** Serialize a tool-result `output` into text for a tool_result block. */
9
+ export const stringifyAnthropicToolOutput = (output) => {
10
+ if (output === null || output === undefined) {
11
+ return "";
12
+ }
13
+ if (typeof output === "string") {
14
+ return output;
15
+ }
16
+ const o = output;
17
+ if (o.type === "text" && typeof o.value === "string") {
18
+ return o.value;
19
+ }
20
+ if (o.type === "json" || o.type === "error-json") {
21
+ try {
22
+ // `?? String(...)`: JSON.stringify returns undefined for an undefined
23
+ // value, which would break this function's string contract and emit an
24
+ // invalid tool_result.content.
25
+ return JSON.stringify(o.value) ?? String(o.value);
26
+ }
27
+ catch {
28
+ return String(o.value);
29
+ }
30
+ }
31
+ try {
32
+ // Same guard: a function or symbol stringifies to undefined.
33
+ return JSON.stringify(output) ?? String(output);
34
+ }
35
+ catch {
36
+ return String(output);
37
+ }
38
+ };
39
+ //# sourceMappingURL=toolOutput.js.map
@@ -657,10 +657,18 @@ export async function collectStreamChunksIncremental(stream, channel) {
657
657
  let outputTokens = 0;
658
658
  let cacheReadTokens = 0;
659
659
  let reasoningTokens = 0;
660
+ // Surfaced so a caller can map SAFETY / MALFORMED_FUNCTION_CALL rather
661
+ // than inferring the turn ended normally. Additive: existing callers that
662
+ // ignore it are unaffected.
663
+ let finishReason;
660
664
  for await (const chunk of stream) {
661
665
  const chunkRecord = chunk;
662
666
  const candidates = chunkRecord.candidates;
663
667
  const firstCandidate = candidates?.[0];
668
+ const candidateFinish = firstCandidate?.finishReason;
669
+ if (typeof candidateFinish === "string") {
670
+ finishReason = candidateFinish;
671
+ }
664
672
  const chunkContent = firstCandidate?.content;
665
673
  if (chunkContent && Array.isArray(chunkContent.parts)) {
666
674
  for (const part of chunkContent.parts) {
@@ -689,6 +697,7 @@ export async function collectStreamChunksIncremental(stream, channel) {
689
697
  return {
690
698
  rawResponseParts,
691
699
  stepFunctionCalls,
700
+ finishReason,
692
701
  inputTokens,
693
702
  outputTokens,
694
703
  cacheReadTokens,
@@ -1,3 +1,6 @@
1
+ import type Anthropic from "@anthropic-ai/sdk";
2
+ import type { Tool } from "./tools.js";
3
+ import type { NativeFunctionCall, NativeToolDeclarationsResult } from "./providers.js";
1
4
  /**
2
5
  * One chunk on the engine's stream.
3
6
  *
@@ -121,6 +124,144 @@ export type AgenticLoopAdapter<TConversation = unknown, TRaw = unknown> = {
121
124
  isMalformedStep?(stepResult: AgenticLoopStepResult<TRaw>): boolean;
122
125
  buildMalformedRetryNote?(conversation: TConversation): TConversation;
123
126
  };
127
+ /**
128
+ * Construction input for `createAnthropicLoopAdapter`, shared by direct
129
+ * Anthropic and Vertex+Claude.
130
+ *
131
+ * `toolFailureBreaker` is the one field the two call sites must differ on:
132
+ * Vertex+Claude ports the Gemini loops' failure-strike breaker, native
133
+ * Anthropic has never had one, and setting it for both would change native
134
+ * Anthropic's behaviour under the guise of a shared refactor.
135
+ */
136
+ export type AnthropicLoopAdapterConfig = {
137
+ client: Pick<Anthropic, "messages">;
138
+ maxSteps: number;
139
+ /**
140
+ * Build one step's request. A closure so the per-turn work the caller
141
+ * already does — system prompt, tool declarations, sampling, thinking
142
+ * config, cache breakpoints — stays where it is rather than moving here.
143
+ */
144
+ buildParams: (conversation: Anthropic.Messages.MessageParam[], step: number) => Anthropic.Messages.MessageCreateParams;
145
+ /** The turn's live tool record, used for deferred-catalog resolution. */
146
+ toolsRecord: Record<string, Tool>;
147
+ /**
148
+ * Name of the terminal structured-output tool when one is in play. A call
149
+ * to it ends the turn: its arguments ARE the answer, so it is reported as
150
+ * text and omitted from `toolCalls`, which routes it through the engine's
151
+ * ordinary zero-tool-calls exit.
152
+ */
153
+ finalResultToolName?: string;
154
+ toolFailureBreaker?: AgenticLoopToolFailureBreaker;
155
+ /**
156
+ * In-turn context reclaim, run once per step before the request is built.
157
+ * Returns the rebuilt conversation when it reclaimed, undefined while the
158
+ * request still fits — leaving history byte-identical in the common case so
159
+ * the rolling prompt-cache prefix stays valid.
160
+ *
161
+ * Provider-supplied because the guard decides and the caller mutates in its
162
+ * own concrete types: dropping an assistant tool_use message together with
163
+ * its paired user tool_result is what keeps blocks paired. The loop appends
164
+ * both every step with nothing else bounding growth, so a migration that
165
+ * drops this overflows the window mid-turn.
166
+ */
167
+ planReclaim?: (conversation: Anthropic.Messages.MessageParam[], step: number) => Anthropic.Messages.MessageParam[] | undefined;
168
+ /**
169
+ * Calibration feedback for the provider's reclaim guard: the FULL prompt
170
+ * size for the step just made — uncached input plus both cache tiers.
171
+ * Passing input_tokens alone reads a cache-hit step as tiny and lets the
172
+ * guard drift far under the real cost.
173
+ */
174
+ noteObservedPromptTokens?: (promptTokens: number) => void;
175
+ abortSignal?: AbortSignal;
176
+ };
177
+ /** What one Gemini step produced, carried to `buildToolResultMessages`. */
178
+ export type GeminiStepRaw = {
179
+ rawResponseParts: unknown[];
180
+ stepFunctionCalls: NativeFunctionCall[];
181
+ };
182
+ /** One turn entry in a Gemini conversation: a role plus its content parts. */
183
+ export type GeminiTurnContent = {
184
+ role: string;
185
+ parts: unknown[];
186
+ };
187
+ /**
188
+ * Construction input for `createGeminiLoopAdapter`, shared by Google AI Studio
189
+ * and Vertex Gemini. Both issue `models.generateContentStream` and consume the
190
+ * same response shape, so one adapter serves four hand-rolled loops.
191
+ */
192
+ export type GeminiLoopAdapterCoreConfig = {
193
+ /** Used in log lines and generated tool-call ids. */
194
+ providerLabel: string;
195
+ maxSteps: number;
196
+ /** Build one step's request object (model, contents, config). */
197
+ buildRequest: (conversation: GeminiTurnContent[], step: number) => unknown;
198
+ /** Issue the request. Kept injectable so each provider keeps its own client. */
199
+ sendStep: (request: unknown, signal: AbortSignal) => Promise<AsyncIterable<{
200
+ functionCalls?: NativeFunctionCall[];
201
+ [key: string]: unknown;
202
+ }>>;
203
+ /**
204
+ * The turn's live tool record. Mid-turn `search_tools` discovery hydrates
205
+ * into this, which is what both the declaration refresh and
206
+ * `resolveToolOnMiss` read.
207
+ */
208
+ liveTools: Record<string, Tool>;
209
+ /**
210
+ * Declarations built for this turn. Carries `originalNameMap`, which keeps
211
+ * Google's function-name sanitization on the adapter side of the engine
212
+ * boundary.
213
+ */
214
+ declarations?: NativeToolDeclarationsResult;
215
+ toolFailureBreaker?: AgenticLoopToolFailureBreaker;
216
+ /**
217
+ * In-turn context reclaim, run once per step before the request is built.
218
+ * Returns the rebuilt conversation when it reclaimed, undefined when the
219
+ * request still fits.
220
+ *
221
+ * Provider-supplied rather than engine-owned because the two Gemini
222
+ * providers reclaim differently (reclaimAiStudioContext vs
223
+ * reclaimVertexLoopContext) while the engine only decides WHEN to ask. The
224
+ * loops append a model turn plus a tool turn every step with nothing else
225
+ * bounding growth, so a migration that drops this overflows the context
226
+ * window mid-turn and loses every completed step.
227
+ */
228
+ planReclaim?: (conversation: GeminiTurnContent[], step: number) => GeminiTurnContent[] | undefined;
229
+ /**
230
+ * Usage feedback for the provider's own context guard, called after each
231
+ * step with that step's real token counts.
232
+ */
233
+ noteUsage?: (inputTokens: number, outputTokens: number) => void;
234
+ };
235
+ /**
236
+ * Opt in to the single MALFORMED_FUNCTION_CALL retry.
237
+ *
238
+ * Vertex Gemini only. AI Studio has no such retry today (confirmed: zero
239
+ * MALFORMED_FUNCTION_CALL handling in its client), and turning it on there
240
+ * would be a behaviour change disguised as a shared refactor. The engine owns
241
+ * the one-retry budget; this only says whether to ask.
242
+ *
243
+ * A union rather than two independent optional fields because the retry is
244
+ * only worth spending a step on if the re-issued request differs from the one
245
+ * that just failed. `runAgenticLoop` falls back to the unchanged conversation
246
+ * when no note builder is supplied (`buildMalformedRetryNote?.(…) ??
247
+ * conversation`), so enabling the retry without one re-sends a byte-identical
248
+ * request and most often reproduces the same malformed call — a step burned
249
+ * for nothing. Requiring the builder here makes that combination unsayable
250
+ * instead of merely discouraged.
251
+ */
252
+ export type GeminiMalformedRetryConfig = {
253
+ enableMalformedRetry: true;
254
+ /**
255
+ * Append the corrective turn that the retry re-issues with.
256
+ * Provider-supplied because the note is written in the provider's own
257
+ * content shape.
258
+ */
259
+ buildMalformedRetryNote: (conversation: GeminiTurnContent[]) => GeminiTurnContent[];
260
+ } | {
261
+ enableMalformedRetry?: false;
262
+ buildMalformedRetryNote?: never;
263
+ };
264
+ export type GeminiLoopAdapterConfig = GeminiLoopAdapterCoreConfig & GeminiMalformedRetryConfig;
124
265
  export type AgenticLoopOptions = {
125
266
  tools?: Record<string, {
126
267
  execute?: (args: Record<string, unknown>, opts: unknown) => Promise<unknown>;
@@ -842,6 +842,12 @@ export type BedrockContentBlock = {
842
842
  export type BedrockPendingContentBlock = BedrockContentBlock & {
843
843
  _inputBuffer?: string;
844
844
  };
845
+ /** A tool_use block being assembled across Anthropic `input_json_delta` events. */
846
+ export type AnthropicPendingToolUse = {
847
+ id: string;
848
+ name: string;
849
+ inputJson: string;
850
+ };
845
851
  /**
846
852
  * Bedrock message structure
847
853
  */
@@ -1732,6 +1738,8 @@ export type NativeFunctionResponse = {
1732
1738
  export type CollectedChunkResult = {
1733
1739
  rawResponseParts: unknown[];
1734
1740
  stepFunctionCalls: NativeFunctionCall[];
1741
+ /** Raw `Candidate.finishReason` from the last chunk that carried one. */
1742
+ finishReason?: string;
1735
1743
  inputTokens: number;
1736
1744
  outputTokens: number;
1737
1745
  /**
@@ -22,6 +22,7 @@ import { redactUrlCredentials } from "../../utils/logSanitize.js";
22
22
  import { ANTHROPIC_MAX_CACHE_BREAKPOINTS, applyAnthropicHistoryCacheBreakpoints, countAnthropicCacheMarkers, } from "../../utils/anthropicCacheBreakpoints.js";
23
23
  import { calculateCost } from "../../utils/pricing.js";
24
24
  import { resolveDeferredTool } from "../../tools/toolDiscovery.js";
25
+ import { stringifyAnthropicToolOutput } from "./toolOutput.js";
25
26
  import { createAnthropicConfig, getProviderModel, validateApiKey, } from "../../utils/providerConfig.js";
26
27
  import { composeAbortSignals, createTimeoutController, mergeAbortSignals, } from "../../utils/timeout.js";
27
28
  import { resolveToolChoice } from "../../utils/toolChoice.js";
@@ -183,33 +184,6 @@ const detectAuthMethod = (oauthToken) => {
183
184
  // ───────────────────────────────────────────────────────────────────────────
184
185
  // Native Messages-API conversion helpers (NeuroLink/V3 shapes → Anthropic)
185
186
  // ───────────────────────────────────────────────────────────────────────────
186
- /** Serialize a tool-result `output` into text for a tool_result block. */
187
- const stringifyAnthropicToolOutput = (output) => {
188
- if (output === null || output === undefined) {
189
- return "";
190
- }
191
- if (typeof output === "string") {
192
- return output;
193
- }
194
- const o = output;
195
- if (o.type === "text" && typeof o.value === "string") {
196
- return o.value;
197
- }
198
- if (o.type === "json" || o.type === "error-json") {
199
- try {
200
- return JSON.stringify(o.value);
201
- }
202
- catch {
203
- return String(o.value);
204
- }
205
- }
206
- try {
207
- return JSON.stringify(output);
208
- }
209
- catch {
210
- return String(output);
211
- }
212
- };
213
187
  /**
214
188
  * Convert NeuroLink/V3-shaped messages (the shape produced by
215
189
  * buildMessagesForStream and by the AI-SDK prompt on the V3 doGenerate path)
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Direct Anthropic's adapter onto the shared agentic loop engine.
3
+ *
4
+ * Everything here is the wire-format half of the turn: building one Messages
5
+ * request, folding an SSE event sequence into content blocks, serializing tool
6
+ * results back into the conversation, and mapping the stop reason. The turn
7
+ * loop, the step cap, tool dispatch, retry and usage accumulation belong to
8
+ * `runAgenticLoop`.
9
+ *
10
+ * Two behaviours here are easy to lose in a migration and are called out
11
+ * because losing either is silent:
12
+ *
13
+ * - Thinking deltas ride the channel as `{ content: "", reasoning }`. The
14
+ * engine's chunk type carries `reasoning` for exactly this reason; a
15
+ * channel that only understood `content` would drop every thinking delta
16
+ * while the text path kept working.
17
+ * - `resolveToolOnMiss` is wired to real deferred-catalog hydration, not as
18
+ * interface decoration. The pre-migration loop resolves every tool call as
19
+ * `toolsRecord[name] ?? resolveDeferredTool(toolsRecord, name)`, which is
20
+ * how a cataloged tool the model calls without having loaded it via
21
+ * `search_tools` gets found. Dropping it would break those tools with a
22
+ * "Tool not found" that looks like a hallucination.
23
+ */
24
+ import type Anthropic from "@anthropic-ai/sdk";
25
+ import type { AgenticLoopAdapter, AnthropicLoopAdapterConfig } from "../../types/index.js";
26
+ export declare function createAnthropicLoopAdapter(config: AnthropicLoopAdapterConfig): AgenticLoopAdapter<Anthropic.Messages.MessageParam[], Anthropic.Messages.ContentBlockParam[]>;