@juspay/neurolink 11.2.4 → 11.3.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.
Files changed (55) hide show
  1. package/CHANGELOG.md +5 -1
  2. package/dist/browser/neurolink.min.js +391 -391
  3. package/dist/core/loopEngine.d.ts +23 -0
  4. package/dist/core/loopEngine.js +245 -0
  5. package/dist/core/nativeToolFormat.d.ts +33 -0
  6. package/dist/core/nativeToolFormat.js +30 -0
  7. package/dist/core/streamChannel.d.ts +10 -0
  8. package/dist/core/streamChannel.js +76 -0
  9. package/dist/lib/core/loopEngine.d.ts +23 -0
  10. package/dist/lib/core/loopEngine.js +246 -0
  11. package/dist/lib/core/nativeToolFormat.d.ts +33 -0
  12. package/dist/lib/core/nativeToolFormat.js +31 -0
  13. package/dist/lib/core/streamChannel.d.ts +10 -0
  14. package/dist/lib/core/streamChannel.js +77 -0
  15. package/dist/lib/providers/anthropic/cacheControl.d.ts +12 -0
  16. package/dist/lib/providers/anthropic/cacheControl.js +15 -0
  17. package/dist/lib/providers/anthropic/client.js +12 -45
  18. package/dist/lib/providers/googleAiStudio/client.js +7 -5
  19. package/dist/lib/providers/googleNativeGemini3/utils.d.ts +4 -11
  20. package/dist/lib/providers/googleNativeGemini3/utils.js +1 -75
  21. package/dist/lib/providers/googleVertex/client.js +28 -30
  22. package/dist/lib/providers/openaiChatCompletionsBase.js +10 -12
  23. package/dist/lib/providers/openaiChatCompletionsClient.d.ts +1 -5
  24. package/dist/lib/providers/openaiChatCompletionsClient.js +0 -26
  25. package/dist/lib/types/index.d.ts +3 -0
  26. package/dist/lib/types/index.js +3 -0
  27. package/dist/lib/types/loopEngine.d.ts +78 -0
  28. package/dist/lib/types/loopEngine.js +2 -0
  29. package/dist/lib/types/nativeTools.d.ts +16 -0
  30. package/dist/lib/types/nativeTools.js +2 -0
  31. package/dist/lib/types/openaiCompatible.d.ts +2 -2
  32. package/dist/lib/types/providers.d.ts +0 -13
  33. package/dist/lib/types/streaming.d.ts +15 -0
  34. package/dist/lib/types/streaming.js +2 -0
  35. package/dist/providers/anthropic/cacheControl.d.ts +12 -0
  36. package/dist/providers/anthropic/cacheControl.js +14 -0
  37. package/dist/providers/anthropic/client.js +12 -45
  38. package/dist/providers/googleAiStudio/client.js +7 -5
  39. package/dist/providers/googleNativeGemini3/utils.d.ts +4 -11
  40. package/dist/providers/googleNativeGemini3/utils.js +1 -75
  41. package/dist/providers/googleVertex/client.js +28 -30
  42. package/dist/providers/openaiChatCompletionsBase.js +10 -12
  43. package/dist/providers/openaiChatCompletionsClient.d.ts +1 -5
  44. package/dist/providers/openaiChatCompletionsClient.js +0 -26
  45. package/dist/types/index.d.ts +3 -0
  46. package/dist/types/index.js +3 -0
  47. package/dist/types/loopEngine.d.ts +78 -0
  48. package/dist/types/loopEngine.js +1 -0
  49. package/dist/types/nativeTools.d.ts +16 -0
  50. package/dist/types/nativeTools.js +1 -0
  51. package/dist/types/openaiCompatible.d.ts +2 -2
  52. package/dist/types/providers.d.ts +0 -13
  53. package/dist/types/streaming.d.ts +15 -0
  54. package/dist/types/streaming.js +1 -0
  55. package/package.json +3 -1
@@ -0,0 +1,246 @@
1
+ import { createStreamChannel } from "./streamChannel.js";
2
+ import { logger } from "../utils/logger.js";
3
+ import { withProviderRetry } from "../utils/providerRetry.js";
4
+ /**
5
+ * Marks a step error that occurred AFTER at least one chunk had already
6
+ * been streamed to the consumer for this step. Retrying at that point
7
+ * would duplicate or interleave already-emitted output, so this wrapper
8
+ * deliberately carries none of the original error's status/retry
9
+ * metadata (`.statusCode`/`.status`, no APICallError/NeuroLinkError
10
+ * branding) — that makes `withProviderRetry`'s internal
11
+ * `isRetryableProviderError()` check return false via its duck-typed
12
+ * fallback, which ends the retry loop on the very next classification
13
+ * instead of sleeping and re-invoking `adapter.executeStep`. The engine
14
+ * unwraps back to the original `cause` before it ever reaches the
15
+ * caller — see the try/catch around the `withProviderRetry` call below.
16
+ */
17
+ class PostEmissionStepError extends Error {
18
+ cause;
19
+ constructor(cause) {
20
+ super(cause instanceof Error ? cause.message : String(cause));
21
+ this.cause = cause;
22
+ }
23
+ }
24
+ function sumUsage(a, b) {
25
+ return {
26
+ inputTokens: a.inputTokens + b.inputTokens,
27
+ outputTokens: a.outputTokens + b.outputTokens,
28
+ cacheReadTokens: (a.cacheReadTokens ?? 0) + (b.cacheReadTokens ?? 0) || undefined,
29
+ cacheWriteTokens: (a.cacheWriteTokens ?? 0) + (b.cacheWriteTokens ?? 0) || undefined,
30
+ reasoningTokens: (a.reasoningTokens ?? 0) + (b.reasoningTokens ?? 0) || undefined,
31
+ };
32
+ }
33
+ /**
34
+ * Run one adapter-parameterized agentic tool-calling turn. Owns the
35
+ * maxSteps-bounded loop, generic tool dispatch (with an opt-in
36
+ * TOOL_NOT_FOUND/failure-strike breaker — see AgenticLoopAdapter.toolFailureBreaker),
37
+ * per-step usage accumulation, a single optional malformed-call retry,
38
+ * chunk emission through streamChannel, and a pre-first-chunk 429/5xx
39
+ * retry (via withProviderRetry) around every adapter.executeStep() call.
40
+ * The retry wrap is unconditional and adapter-agnostic — every migrated
41
+ * provider gets it for free, not just the ones that had a hand-rolled
42
+ * version before migration (see Verified Fact 4-adjacent note in Task 4
43
+ * Step 1 and the Risks & Rollback "Deliberate behavior changes" list for
44
+ * which families are gaining this for the first time). Everything
45
+ * wire-format-specific (building the request, parsing the SDK response,
46
+ * serializing tool results back into the conversation, mapping the raw
47
+ * stop reason) is delegated to `adapter`.
48
+ */
49
+ export function runAgenticLoop(adapter, initialConversation, options) {
50
+ const channel = createStreamChannel();
51
+ const internalAbort = new AbortController();
52
+ const onCallerAbort = () => internalAbort.abort();
53
+ options.abortSignal?.addEventListener("abort", onCallerAbort);
54
+ if (options.abortSignal?.aborted) {
55
+ internalAbort.abort();
56
+ }
57
+ const failedTools = new Map();
58
+ let malformedRetryUsed = false;
59
+ const resultPromise = (async () => {
60
+ let conversation = initialConversation;
61
+ let usage = { inputTokens: 0, outputTokens: 0 };
62
+ let finalText = "";
63
+ let rawStopReason;
64
+ const allToolCalls = [];
65
+ const allToolExecutions = [];
66
+ let hadToolCallsAtCap = false;
67
+ try {
68
+ for (let step = 0; step < adapter.maxSteps; step++) {
69
+ if (internalAbort.signal.aborted) {
70
+ break;
71
+ }
72
+ if (adapter.planReclaim) {
73
+ const reclaimed = adapter.planReclaim(conversation, step);
74
+ if (reclaimed) {
75
+ conversation = reclaimed.conversation;
76
+ }
77
+ }
78
+ const request = adapter.buildStepRequest(conversation, step);
79
+ // Pre-first-chunk 429/5xx retry: watch whether THIS attempt of
80
+ // THIS step pushes anything to the shared channel before it
81
+ // throws. `hasEmitted` resets at the top of every attempt
82
+ // withProviderRetry makes; the instant an attempt emits and then
83
+ // throws, the thrown error is rewrapped as a PostEmissionStepError
84
+ // (no status/branding info survives the rewrap), which
85
+ // isRetryableProviderError() duck-types as non-retryable — so
86
+ // withProviderRetry gives up immediately instead of sleeping and
87
+ // re-invoking executeStep, which would duplicate/interleave
88
+ // output already sent to the consumer. The original error (not
89
+ // the wrapper) is what the caller of runAgenticLoop ultimately
90
+ // sees, via the unwrap in the catch below.
91
+ let hasEmitted = false;
92
+ const watchedChannel = {
93
+ push: (chunk) => {
94
+ hasEmitted = true;
95
+ channel.push(chunk);
96
+ },
97
+ };
98
+ let stepResult;
99
+ try {
100
+ stepResult = await withProviderRetry(async () => {
101
+ hasEmitted = false;
102
+ try {
103
+ return await adapter.executeStep(request, watchedChannel, internalAbort.signal);
104
+ }
105
+ catch (err) {
106
+ throw hasEmitted ? new PostEmissionStepError(err) : err;
107
+ }
108
+ }, undefined, // no OTel span threaded through the engine today; adapters instrument their own steps if they need span-level detail
109
+ `${adapter.providerLabel}.step`);
110
+ }
111
+ catch (err) {
112
+ throw err instanceof PostEmissionStepError ? err.cause : err;
113
+ }
114
+ usage = sumUsage(usage, stepResult.usage);
115
+ rawStopReason = stepResult.rawStopReason;
116
+ if (adapter.isMalformedStep?.(stepResult) &&
117
+ !malformedRetryUsed &&
118
+ !internalAbort.signal.aborted) {
119
+ malformedRetryUsed = true;
120
+ logger.warn(`[${adapter.providerLabel}] Malformed function call at step ${step + 1}/${adapter.maxSteps}; retrying once.`);
121
+ conversation =
122
+ adapter.buildMalformedRetryNote?.(conversation) ?? conversation;
123
+ continue;
124
+ }
125
+ if (stepResult.toolCalls.length === 0) {
126
+ finalText = stepResult.text || finalText;
127
+ break;
128
+ }
129
+ if (step === adapter.maxSteps - 1) {
130
+ hadToolCallsAtCap = true;
131
+ }
132
+ const toolResults = [];
133
+ for (const call of stepResult.toolCalls) {
134
+ allToolCalls.push(call);
135
+ const breaker = adapter.toolFailureBreaker;
136
+ const failInfo = breaker ? failedTools.get(call.name) : undefined;
137
+ if (breaker && failInfo && failInfo.count >= breaker.maxRetries) {
138
+ const output = {
139
+ error: `TOOL_PERMANENTLY_FAILED: "${call.name}" has failed ${failInfo.count} times. Last error: ${failInfo.lastError}.`,
140
+ status: "permanently_failed",
141
+ do_not_retry: true,
142
+ };
143
+ toolResults.push({
144
+ ...call,
145
+ output,
146
+ error: output.error,
147
+ permanentlyFailed: true,
148
+ });
149
+ allToolExecutions.push({
150
+ name: call.name,
151
+ input: call.args,
152
+ output,
153
+ });
154
+ continue;
155
+ }
156
+ const tool = options.tools?.[call.name];
157
+ if (!tool?.execute) {
158
+ const output = breaker
159
+ ? {
160
+ error: `TOOL_NOT_FOUND: "${call.name}" does not exist.`,
161
+ status: "permanently_failed",
162
+ do_not_retry: true,
163
+ }
164
+ : { error: `Tool not found: ${call.name}` };
165
+ toolResults.push({
166
+ ...call,
167
+ output,
168
+ error: output.error,
169
+ permanentlyFailed: !!breaker,
170
+ });
171
+ allToolExecutions.push({
172
+ name: call.name,
173
+ input: call.args,
174
+ output,
175
+ });
176
+ continue;
177
+ }
178
+ try {
179
+ const output = await tool.execute(call.args, {
180
+ toolCallId: call.id,
181
+ abortSignal: internalAbort.signal,
182
+ });
183
+ toolResults.push({ ...call, output });
184
+ allToolExecutions.push({
185
+ name: call.name,
186
+ input: call.args,
187
+ output,
188
+ });
189
+ }
190
+ catch (err) {
191
+ const message = err instanceof Error ? err.message : String(err);
192
+ if (breaker) {
193
+ const current = failedTools.get(call.name) ?? {
194
+ count: 0,
195
+ lastError: "",
196
+ };
197
+ current.count++;
198
+ current.lastError = message;
199
+ failedTools.set(call.name, current);
200
+ }
201
+ const output = { error: message, status: "failed" };
202
+ toolResults.push({ ...call, output, error: message });
203
+ allToolExecutions.push({
204
+ name: call.name,
205
+ input: call.args,
206
+ output,
207
+ });
208
+ }
209
+ }
210
+ conversation = adapter.buildToolResultMessages(conversation, stepResult, toolResults);
211
+ }
212
+ const finishReason = adapter.mapFinishReason(rawStopReason, hadToolCallsAtCap);
213
+ return {
214
+ text: finalText,
215
+ toolCalls: allToolCalls,
216
+ toolExecutions: allToolExecutions,
217
+ usage,
218
+ finishReason,
219
+ rawStopReason,
220
+ conversation,
221
+ };
222
+ }
223
+ catch (err) {
224
+ // Must run before the finally block's channel.close(): a consumer
225
+ // parked in the channel's iterable is woken by whichever of
226
+ // error()/close() runs first, and close() alone would let a
227
+ // stream-only consumer observe a clean end of stream for a turn that
228
+ // actually failed. Calling error() here — synchronously, inside this
229
+ // catch — guarantees it lands before close(), with no dependence on
230
+ // microtask scheduling (unlike an outer promise-chained catch handler
231
+ // on this IIFE, which would run in a later microtask after `finally`
232
+ // already closed the channel).
233
+ channel.error(err);
234
+ throw err;
235
+ }
236
+ finally {
237
+ // close() after error() is harmless: it only flips `done`, and
238
+ // streamChannel.error() keeps its error state intact regardless of a
239
+ // later close() call.
240
+ channel.close();
241
+ options.abortSignal?.removeEventListener("abort", onCallerAbort);
242
+ }
243
+ })();
244
+ return { stream: channel.iterable, resultPromise };
245
+ }
246
+ //# sourceMappingURL=loopEngine.js.map
@@ -0,0 +1,33 @@
1
+ import type { NativeAnthropicToolDeclaration, NativeToolDeclarationsResult, Tool } from "../types/index.js";
2
+ /**
3
+ * Convert a NeuroLink tool record into the wire-format a native
4
+ * (non-AI-SDK) provider SDK expects. Absorbs the direct-Anthropic
5
+ * provider's `toolsToAnthropic` — previously duplicated between the
6
+ * streaming loop's pre-loop snapshot and its mid-turn discovery-hydration
7
+ * call site (both in anthropic/client.ts). Gemini's `functionDeclarations`
8
+ * shape was already centralized in `buildNativeToolDeclarations`; this
9
+ * function is a thin facade over it so every native provider calls one
10
+ * entry point.
11
+ *
12
+ * Scope note: Vertex's Claude-on-Vertex `input_schema` builder
13
+ * (`buildAnthropicToolDeclaration` in googleVertex/client.ts) is
14
+ * deliberately NOT routed through this function. Despite the superficial
15
+ * similarity, it is not a byte-for-byte duplicate of `toolsToAnthropic`: it
16
+ * strips the converted schema down to `{type, properties, required}` only
17
+ * (dropping any other JSON-Schema keywords `convertZodToJsonSchema` may
18
+ * produce), it always runs `inlineJsonSchema` (Anthropic-direct never has),
19
+ * it prefers `parameters` over `inputSchema` (the opposite fallback order
20
+ * from Anthropic-direct), and it has no `cache_control` support of its own
21
+ * because Vertex applies cache breakpoints later via the separate
22
+ * `applyVertexAnthropicCacheBreakpoints` pass. Collapsing these into one
23
+ * shared implementation would risk a live tool-calling regression on one
24
+ * provider or the other; see the Task 2 report for the full comparison.
25
+ *
26
+ * These are genuine TS overload declarations, not redeclarations: the base
27
+ * `no-redeclare` ESLint rule doesn't understand the overload-signatures +
28
+ * implementation pattern (the TS-aware `@typescript-eslint/no-redeclare`
29
+ * variant would, but isn't enabled project-wide), so each signature below
30
+ * is individually exempted.
31
+ */
32
+ export declare function toNativeToolDeclarations(tools: Record<string, Tool>, format: "input_schema"): NativeAnthropicToolDeclaration[] | undefined;
33
+ export declare function toNativeToolDeclarations(tools: Record<string, Tool>, format: "functionDeclarations"): NativeToolDeclarationsResult;
@@ -0,0 +1,31 @@
1
+ import { buildNativeToolDeclarations } from "../providers/googleNativeGemini3/utils.js";
2
+ import { cacheControlOf } from "../providers/anthropic/cacheControl.js";
3
+ import { convertZodToJsonSchema } from "../utils/schemaConversion.js";
4
+ // eslint-disable-next-line no-redeclare -- TS overload implementation signature, not a redeclaration
5
+ export function toNativeToolDeclarations(tools, format) {
6
+ if (format === "functionDeclarations") {
7
+ return buildNativeToolDeclarations(tools);
8
+ }
9
+ const entries = Object.entries(tools ?? {});
10
+ if (entries.length === 0) {
11
+ return undefined;
12
+ }
13
+ return entries.map(([name, tool]) => {
14
+ const t = tool;
15
+ const rawSchema = t.inputSchema ?? t.parameters;
16
+ const input_schema = (rawSchema
17
+ ? convertZodToJsonSchema(rawSchema)
18
+ : { type: "object", properties: {} });
19
+ // GenerationHandler marks the last tool definition with a cache
20
+ // breakpoint when prompt caching is active — keep honoring it.
21
+ const cc = cacheControlOf(tool);
22
+ const declaration = {
23
+ name,
24
+ ...(t.description ? { description: t.description } : {}),
25
+ input_schema,
26
+ ...(cc ? { cache_control: cc } : {}),
27
+ };
28
+ return declaration;
29
+ });
30
+ }
31
+ //# sourceMappingURL=nativeToolFormat.js.map
@@ -0,0 +1,10 @@
1
+ import type { StreamChannel } from "../types/index.js";
2
+ /**
3
+ * Create a push-based channel bridging a background producer (an agentic
4
+ * tool-calling loop) with an async-iterable consumer, enabling truly
5
+ * incremental streaming: values are yielded to the caller as they arrive
6
+ * rather than being buffered until the producer finishes.
7
+ */
8
+ export declare function createStreamChannel<T = {
9
+ content: string;
10
+ }>(): StreamChannel<T>;
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Create a push-based channel bridging a background producer (an agentic
3
+ * tool-calling loop) with an async-iterable consumer, enabling truly
4
+ * incremental streaming: values are yielded to the caller as they arrive
5
+ * rather than being buffered until the producer finishes.
6
+ */
7
+ export function createStreamChannel() {
8
+ const queue = [];
9
+ let done = false;
10
+ let fatalError = undefined;
11
+ // Tracked separately from `fatalError`: a producer can legitimately
12
+ // reject with `undefined` (e.g. `throw undefined`), and `fatalError !==
13
+ // undefined` would then be indistinguishable from "no error occurred",
14
+ // closing the stream cleanly instead of surfacing the failure.
15
+ let hasError = false;
16
+ let notify = null;
17
+ function wake() {
18
+ if (notify) {
19
+ const fn = notify;
20
+ notify = null;
21
+ fn();
22
+ }
23
+ }
24
+ function push(value) {
25
+ if (done) {
26
+ return;
27
+ }
28
+ queue.push(value);
29
+ wake();
30
+ }
31
+ function close() {
32
+ done = true;
33
+ wake();
34
+ }
35
+ function error(err) {
36
+ done = true;
37
+ fatalError = err;
38
+ hasError = true;
39
+ wake();
40
+ }
41
+ let readIndex = 0;
42
+ async function* iterable() {
43
+ try {
44
+ while (true) {
45
+ if (readIndex < queue.length) {
46
+ yield queue[readIndex++];
47
+ // Periodically compact consumed entries to avoid unbounded retention.
48
+ if (readIndex > 1024 && readIndex * 2 >= queue.length) {
49
+ queue.splice(0, readIndex);
50
+ readIndex = 0;
51
+ }
52
+ }
53
+ else if (done) {
54
+ if (hasError) {
55
+ throw fatalError instanceof Error
56
+ ? fatalError
57
+ : new Error(String(fatalError));
58
+ }
59
+ return;
60
+ }
61
+ else {
62
+ await new Promise((resolve) => {
63
+ notify = resolve;
64
+ });
65
+ }
66
+ }
67
+ }
68
+ finally {
69
+ // Consumer stopped reading (disconnect/cancel): stop buffering.
70
+ done = true;
71
+ queue.length = 0;
72
+ notify?.();
73
+ }
74
+ }
75
+ return { push, close, error, iterable: iterable() };
76
+ }
77
+ //# sourceMappingURL=streamChannel.js.map
@@ -0,0 +1,12 @@
1
+ import type Anthropic from "@anthropic-ai/sdk";
2
+ /**
3
+ * Read an Anthropic cache breakpoint from a message/part/tool carrier.
4
+ * MessageBuilder marks system messages (and GenerationHandler marks the last
5
+ * tool definition) with `providerOptions.anthropic.cacheControl` — the
6
+ * AI-SDK-era prompt-caching contract this native path must keep honoring.
7
+ *
8
+ * Extracted from anthropic/client.ts so `src/lib/core/nativeToolFormat.ts`
9
+ * can share it without importing the provider client (which would create a
10
+ * circular import: client.ts -> core/nativeToolFormat.ts -> client.ts).
11
+ */
12
+ export declare const cacheControlOf: (carrier: unknown) => Anthropic.Messages.CacheControlEphemeral | undefined;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Read an Anthropic cache breakpoint from a message/part/tool carrier.
3
+ * MessageBuilder marks system messages (and GenerationHandler marks the last
4
+ * tool definition) with `providerOptions.anthropic.cacheControl` — the
5
+ * AI-SDK-era prompt-caching contract this native path must keep honoring.
6
+ *
7
+ * Extracted from anthropic/client.ts so `src/lib/core/nativeToolFormat.ts`
8
+ * can share it without importing the provider client (which would create a
9
+ * circular import: client.ts -> core/nativeToolFormat.ts -> client.ts).
10
+ */
11
+ export const cacheControlOf = (carrier) => {
12
+ const cc = carrier?.providerOptions?.anthropic?.cacheControl;
13
+ return cc?.type === "ephemeral" ? { type: "ephemeral" } : undefined;
14
+ };
15
+ //# sourceMappingURL=cacheControl.js.map
@@ -33,8 +33,11 @@ import { resolveClaudeMaxTokens } from "../../utils/tokenLimits.js";
33
33
  import { withProviderRetry } from "../../utils/providerRetry.js";
34
34
  import { toAnthropicImageBlock, fileToAnthropicBlock, } from "../anthropicImageBlocks.js";
35
35
  import { resolveSamplingParams } from "../../models/modelRegistry.js";
36
- import { createChunkQueue, createDeferredAnalytics, stringifyToolInput, } from "../openaiChatCompletionsClient.js";
36
+ import { createDeferredAnalytics, stringifyToolInput, } from "../openaiChatCompletionsClient.js";
37
+ import { createStreamChannel } from "../../core/streamChannel.js";
38
+ import { toNativeToolDeclarations } from "../../core/nativeToolFormat.js";
37
39
  import { ANTHROPIC_BETA_HEADERS } from "./constants.js";
40
+ import { cacheControlOf } from "./cacheControl.js";
38
41
  import { appendFinalResultInstruction, appendFinalResultTool, FINAL_RESULT_TOOL_NAME, stringifyFinalResultInput, } from "./structuredOutput.js";
39
42
  // AnthropicProviderConfig is imported from types/providers.ts
40
43
  // Re-export for backward compatibility
@@ -180,16 +183,6 @@ const detectAuthMethod = (oauthToken) => {
180
183
  // ───────────────────────────────────────────────────────────────────────────
181
184
  // Native Messages-API conversion helpers (NeuroLink/V3 shapes → Anthropic)
182
185
  // ───────────────────────────────────────────────────────────────────────────
183
- /**
184
- * Read an Anthropic cache breakpoint from a message/part/tool carrier.
185
- * MessageBuilder marks system messages (and GenerationHandler marks the last
186
- * tool definition) with `providerOptions.anthropic.cacheControl` — the
187
- * AI-SDK-era prompt-caching contract this native path must keep honoring.
188
- */
189
- const cacheControlOf = (carrier) => {
190
- const cc = carrier?.providerOptions?.anthropic?.cacheControl;
191
- return cc?.type === "ephemeral" ? { type: "ephemeral" } : undefined;
192
- };
193
186
  /** Serialize a tool-result `output` into text for a tool_result block. */
194
187
  const stringifyAnthropicToolOutput = (output) => {
195
188
  if (output === null || output === undefined) {
@@ -400,29 +393,6 @@ const messagesToAnthropic = (msgs) => {
400
393
  messages,
401
394
  };
402
395
  };
403
- /** Convert a NeuroLink tool record into Anthropic tool definitions. */
404
- const toolsToAnthropic = (tools) => {
405
- const entries = Object.entries(tools);
406
- if (entries.length === 0) {
407
- return undefined;
408
- }
409
- return entries.map(([name, tool]) => {
410
- const t = tool;
411
- const rawSchema = t.inputSchema ?? t.parameters;
412
- const input_schema = (rawSchema
413
- ? convertZodToJsonSchema(rawSchema)
414
- : { type: "object", properties: {} });
415
- // GenerationHandler marks the last tool definition with a cache
416
- // breakpoint when prompt caching is active — keep honoring it.
417
- const cc = cacheControlOf(tool);
418
- return {
419
- name,
420
- ...(t.description ? { description: t.description } : {}),
421
- input_schema,
422
- ...(cc ? { cache_control: cc } : {}),
423
- };
424
- });
425
- };
426
396
  /** Map a NeuroLink tool choice onto Anthropic's tool_choice shape. */
427
397
  const toolChoiceToAnthropic = (choice) => {
428
398
  if (!choice || choice === "auto") {
@@ -1423,7 +1393,7 @@ export class AnthropicProvider extends BaseProvider {
1423
1393
  ? options.tools || (await this.getAllTools())
1424
1394
  : {};
1425
1395
  anthropicTools = shouldUseTools
1426
- ? toolsToAnthropic(toolsRecord)
1396
+ ? toNativeToolDeclarations(toolsRecord, "input_schema")
1427
1397
  : undefined;
1428
1398
  // Build message array from options with multimodal support, then
1429
1399
  // convert to the Anthropic Messages payload (system + content blocks).
@@ -1471,7 +1441,8 @@ export class AnthropicProvider extends BaseProvider {
1471
1441
  });
1472
1442
  const maxSteps = options.maxSteps || DEFAULT_MAX_STEPS;
1473
1443
  const emitter = this.neurolink?.getEventEmitter();
1474
- const { pushChunk, nextChunk } = createChunkQueue();
1444
+ const channel = createStreamChannel();
1445
+ const { push: pushChunk } = channel;
1475
1446
  const { usagePromise, finishPromise, resolveUsage, resolveFinish } = createDeferredAnalytics();
1476
1447
  usagePromise
1477
1448
  .then((usage) => {
@@ -1566,7 +1537,7 @@ export class AnthropicProvider extends BaseProvider {
1566
1537
  const declared = new Set(anthropicTools.map((t) => t.name));
1567
1538
  const hydrated = Object.fromEntries(Object.entries(toolsRecord).filter(([name]) => !declared.has(name)));
1568
1539
  if (Object.keys(hydrated).length > 0) {
1569
- anthropicTools.push(...(toolsToAnthropic(hydrated) ?? []));
1540
+ anthropicTools.push(...(toNativeToolDeclarations(hydrated, "input_schema") ?? []));
1570
1541
  logger.info(`[Anthropic] ${Object.keys(hydrated).length} tool(s) hydrated mid-turn via discovery: ${Object.keys(hydrated).join(", ")}`);
1571
1542
  }
1572
1543
  }
@@ -1932,22 +1903,18 @@ export class AnthropicProvider extends BaseProvider {
1932
1903
  }
1933
1904
  }
1934
1905
  timeoutController?.cleanup();
1935
- pushChunk({ done: true });
1906
+ channel.close();
1936
1907
  });
1937
1908
  loopPromise.catch(() => {
1938
1909
  // Swallowed by design: the generator below surfaces loop errors after
1939
- // draining the queue; this guard only prevents an unhandled-rejection
1910
+ // draining the channel; this guard only prevents an unhandled-rejection
1940
1911
  // crash when the consumer abandons the stream early.
1941
1912
  });
1942
1913
  const providerName = this.providerName;
1943
1914
  const transformedStream = async function* () {
1944
1915
  let contentYielded = 0;
1945
1916
  try {
1946
- for (;;) {
1947
- const chunk = await nextChunk();
1948
- if ("done" in chunk) {
1949
- break;
1950
- }
1917
+ for await (const chunk of channel.iterable) {
1951
1918
  if ("content" in chunk &&
1952
1919
  typeof chunk.content === "string" &&
1953
1920
  chunk.content.length > 0) {
@@ -1955,7 +1922,7 @@ export class AnthropicProvider extends BaseProvider {
1955
1922
  }
1956
1923
  yield chunk;
1957
1924
  }
1958
- // Surface any error the loop threw after draining the queue.
1925
+ // Surface any error the loop threw after draining the channel.
1959
1926
  await loopPromise;
1960
1927
  // No-output path: stream completed normally but yielded zero text.
1961
1928
  if (contentYielded === 0 && toolsUsed.length === 0) {
@@ -14,7 +14,9 @@ import { withTimeout } from "../../utils/async/index.js";
14
14
  import { estimateTokens } from "../../utils/tokenEstimation.js";
15
15
  import { transformToolExecutions } from "../../utils/transformationUtils.js";
16
16
  import { resolveToolExecutionRecords } from "../../core/toolExecutionRecorder.js";
17
- import { buildGeminiResponseSchema, buildNativeConfig, buildNativeToolDeclarations, collectStreamChunks, collectStreamChunksIncremental, computeMaxSteps, createContextGuard, createTextChannel, buildUserPartsWithMultimodal, executeNativeToolCalls, extractTextFromParts, extractThoughtSignature, handleMaxStepsTermination, prependConversationMessages, pushModelResponseToHistory, refreshNativeToolDeclarations, DedupExecuteMap, } from "../googleNativeGemini3/index.js";
17
+ import { buildGeminiResponseSchema, buildNativeConfig, collectStreamChunks, collectStreamChunksIncremental, computeMaxSteps, createContextGuard, buildUserPartsWithMultimodal, executeNativeToolCalls, extractTextFromParts, extractThoughtSignature, handleMaxStepsTermination, prependConversationMessages, pushModelResponseToHistory, refreshNativeToolDeclarations, DedupExecuteMap, } from "../googleNativeGemini3/index.js";
18
+ import { createStreamChannel } from "../../core/streamChannel.js";
19
+ import { toNativeToolDeclarations } from "../../core/nativeToolFormat.js";
18
20
  import { createProxyFetch } from "../../proxy/proxyFetch.js";
19
21
  // Google AI Live API types now imported from ../types/providerSpecific.js
20
22
  // Import proper types for multimodal message handling
@@ -653,7 +655,7 @@ export class GoogleAIStudioProvider extends BaseProvider {
653
655
  if (options.tools &&
654
656
  Object.keys(options.tools).length > 0 &&
655
657
  !options.disableTools) {
656
- const result = buildNativeToolDeclarations(options.tools);
658
+ const result = toNativeToolDeclarations(options.tools, "functionDeclarations");
657
659
  declarationsResult = result;
658
660
  toolsConfig = result.toolsConfig;
659
661
  executeMap = result.executeMap;
@@ -686,7 +688,7 @@ export class GoogleAIStudioProvider extends BaseProvider {
686
688
  const composedSignal = composeAbortSignals(options.abortSignal, timeoutController?.controller.signal);
687
689
  // Create a push-based text channel so the caller receives tokens as
688
690
  // they arrive from the network rather than after full buffering.
689
- const channel = createTextChannel();
691
+ const channel = createStreamChannel();
690
692
  // Shared mutable state updated by the background agentic loop.
691
693
  const allToolCalls = [];
692
694
  // Mirror the Vertex Gemini stream path: track tool executions so
@@ -859,7 +861,7 @@ export class GoogleAIStudioProvider extends BaseProvider {
859
861
  const fallback = handleMaxStepsTermination("[GoogleAIStudio]", step, maxSteps, "", // finalText is empty — model didn't stop on its own
860
862
  lastStepText);
861
863
  if (fallback) {
862
- channel.push(fallback);
864
+ channel.push({ content: fallback });
863
865
  }
864
866
  }
865
867
  const responseTime = Date.now() - startTime;
@@ -1002,7 +1004,7 @@ export class GoogleAIStudioProvider extends BaseProvider {
1002
1004
  if (shouldUseTools && !exclusionInForce) {
1003
1005
  const tools = options.tools || {};
1004
1006
  if (Object.keys(tools).length > 0) {
1005
- const result = buildNativeToolDeclarations(tools);
1007
+ const result = toNativeToolDeclarations(tools, "functionDeclarations");
1006
1008
  declarationsResult = result;
1007
1009
  toolsConfig = result.toolsConfig;
1008
1010
  executeMap = result.executeMap;
@@ -8,7 +8,7 @@
8
8
  * This module extracts the functions that are duplicated between the two
9
9
  * providers so they can share a single implementation.
10
10
  */
11
- import type { GenerateStopReason, ThinkingConfig, ChatMessage, CollectedChunkResult, MinimalChatMessage, NativeFunctionCall, NativeFunctionResponse, NativeToolDeclarationsResult, NativeToolsConfig, TextChannel, VertexNativePart, GeminiMultimodalInput, MultimodalAudioEntry } from "../../types/index.js";
11
+ import type { GenerateStopReason, ThinkingConfig, ChatMessage, CollectedChunkResult, MinimalChatMessage, NativeFunctionCall, NativeFunctionResponse, NativeToolDeclarationsResult, NativeToolsConfig, StreamChannel, VertexNativePart, GeminiMultimodalInput, MultimodalAudioEntry } from "../../types/index.js";
12
12
  import type { Tool } from "../../types/index.js";
13
13
  /**
14
14
  * A per-turn tool execute map that deduplicates identical tool calls.
@@ -166,15 +166,6 @@ export declare function collectStreamChunks(stream: AsyncIterable<{
166
166
  functionCalls?: NativeFunctionCall[];
167
167
  [key: string]: unknown;
168
168
  }>): Promise<CollectedChunkResult>;
169
- /**
170
- * Create a push-based text channel that bridges a background producer
171
- * (the agentic tool-calling loop) with an async-iterable consumer.
172
- *
173
- * This enables truly incremental streaming: text parts are yielded to the
174
- * caller as they arrive from the network, rather than being buffered until
175
- * the model finishes generating.
176
- */
177
- export declare function createTextChannel(): TextChannel;
178
169
  /**
179
170
  * Iterate a single stream step incrementally, pushing text parts to `channel`
180
171
  * as they arrive from the network while simultaneously accumulating the full
@@ -189,7 +180,9 @@ export declare function createTextChannel(): TextChannel;
189
180
  export declare function collectStreamChunksIncremental(stream: AsyncIterable<{
190
181
  functionCalls?: NativeFunctionCall[];
191
182
  [key: string]: unknown;
192
- }>, channel: TextChannel): Promise<CollectedChunkResult>;
183
+ }>, channel: StreamChannel<{
184
+ content: string;
185
+ }>): Promise<CollectedChunkResult>;
193
186
  /**
194
187
  * Extract the thoughtSignature token from raw response parts.
195
188
  * Returns the last thoughtSignature found (each step may produce one).