@juspay/neurolink 11.18.3 → 11.18.5

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.
@@ -16,8 +16,9 @@
16
16
  * and original -> sanitized when writing `functionResponse` parts. Neither the
17
17
  * engine nor any other adapter needs to know sanitization happened.
18
18
  */
19
+ import { guardToolExecutor } from "./toolExecutionGuards.js";
19
20
  import { resolveLiveTool } from "../tools/toolDiscovery.js";
20
- import { collectStreamChunksIncremental, guardToolExecutor, extractTextFromParts, mapGeminiFinishReason, pushModelResponseToHistory, refreshNativeToolDeclarations, } from "../providers/googleNativeGemini3/utils.js";
21
+ import { collectStreamChunksIncremental, extractTextFromParts, mapGeminiFinishReason, pushModelResponseToHistory, refreshNativeToolDeclarations, } from "../providers/googleNativeGemini3/utils.js";
21
22
  export function createGeminiLoopAdapter(config) {
22
23
  /**
23
24
  * Sanitized wire name -> the name the caller registered. Rebuilt per step
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Guards applied around a single tool execution inside a native provider loop.
3
+ *
4
+ * Lives here rather than beside any one provider because it is used by all of
5
+ * them: both Vertex+Claude loops call it directly, and the Gemini paths reach
6
+ * it through `buildDedupedEngineTools`. It previously sat in
7
+ * `providers/googleNativeGemini3/utils.ts`, which meant the Anthropic loops
8
+ * imported a tool-execution primitive out of a Gemini module — accurate about
9
+ * where it was written, misleading about what depends on it.
10
+ *
11
+ * Nothing in here is provider-specific: it takes an executor and a guards
12
+ * object and returns a wrapped executor.
13
+ */
14
+ import type { Tool, ToolExecutionGuards } from "../types/index.js";
15
+ /**
16
+ * Mid-turn tool sync for the native Gemini loops that build their snapshot
17
+ * via buildNativeToolDeclarations. `search_tools` (tools.discovery) hydrates
18
+ * discovered tools into the live record between steps; without this refresh
19
+ * they stay invisible to the rest of the turn and every call dies as
20
+ * TOOL_NOT_FOUND. Mutates the snapshot in place — the request config holds
21
+ * `toolsConfig` by reference — and returns true when anything was added.
22
+ */
23
+ /**
24
+ * Everything a native Gemini loop wraps around a tool call that the shared
25
+ * engine does not do itself.
26
+ *
27
+ * Order matters. `raceWithAbort` sits INSIDE `withTimeout` so a turn-level
28
+ * abort is observed the moment it fires rather than after the tool settles,
29
+ * and the timeout still bounds a tool that neither settles nor honours its
30
+ * signal. The progress pings bracket the await because the stall watchdog is
31
+ * a whole-turn interval comparing wall-clock against the last progress mark —
32
+ * without them a legitimately slow tool reads as a stalled turn and is killed.
33
+ *
34
+ * Exported because a tool hydrated MID-TURN has to be wrapped the same way as
35
+ * one declared up front; keeping this inline made the discovered tool the one
36
+ * executor in the system that ran raw.
37
+ */
38
+ export declare function guardToolExecutor(name: string, execute: NonNullable<Tool["execute"]>, guards: ToolExecutionGuards): (args: Record<string, unknown>, opts: unknown) => Promise<unknown>;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Guards applied around a single tool execution inside a native provider loop.
3
+ *
4
+ * Lives here rather than beside any one provider because it is used by all of
5
+ * them: both Vertex+Claude loops call it directly, and the Gemini paths reach
6
+ * it through `buildDedupedEngineTools`. It previously sat in
7
+ * `providers/googleNativeGemini3/utils.ts`, which meant the Anthropic loops
8
+ * imported a tool-execution primitive out of a Gemini module — accurate about
9
+ * where it was written, misleading about what depends on it.
10
+ *
11
+ * Nothing in here is provider-specific: it takes an executor and a guards
12
+ * object and returns a wrapped executor.
13
+ */
14
+ import { raceWithAbort, withTimeout } from "../utils/async/index.js";
15
+ /**
16
+ * Mid-turn tool sync for the native Gemini loops that build their snapshot
17
+ * via buildNativeToolDeclarations. `search_tools` (tools.discovery) hydrates
18
+ * discovered tools into the live record between steps; without this refresh
19
+ * they stay invisible to the rest of the turn and every call dies as
20
+ * TOOL_NOT_FOUND. Mutates the snapshot in place — the request config holds
21
+ * `toolsConfig` by reference — and returns true when anything was added.
22
+ */
23
+ /**
24
+ * Everything a native Gemini loop wraps around a tool call that the shared
25
+ * engine does not do itself.
26
+ *
27
+ * Order matters. `raceWithAbort` sits INSIDE `withTimeout` so a turn-level
28
+ * abort is observed the moment it fires rather than after the tool settles,
29
+ * and the timeout still bounds a tool that neither settles nor honours its
30
+ * signal. The progress pings bracket the await because the stall watchdog is
31
+ * a whole-turn interval comparing wall-clock against the last progress mark —
32
+ * without them a legitimately slow tool reads as a stalled turn and is killed.
33
+ *
34
+ * Exported because a tool hydrated MID-TURN has to be wrapped the same way as
35
+ * one declared up front; keeping this inline made the discovered tool the one
36
+ * executor in the system that ran raw.
37
+ */
38
+ export function guardToolExecutor(name, execute, guards) {
39
+ return async (args, opts) => {
40
+ const invoke = () => Promise.resolve(execute(args, opts));
41
+ // The span wraps the CALL, not the guard: a timeout or an abort is a fact
42
+ // about this tool invocation and belongs inside its observation.
43
+ const wrapInSpan = guards.withToolSpan;
44
+ const call = wrapInSpan ? () => wrapInSpan(name, invoke) : invoke;
45
+ guards.onProgress?.();
46
+ try {
47
+ const raced = guards.abortSignal
48
+ ? raceWithAbort(call(), guards.abortSignal)
49
+ : call();
50
+ return await (guards.toolTimeoutMs === undefined
51
+ ? raced
52
+ : withTimeout(raced, guards.toolTimeoutMs, `Tool "${name}" execution timed out after ${guards.toolTimeoutMs}ms`));
53
+ }
54
+ finally {
55
+ // In `finally`, not after a successful await: a tool that times out or
56
+ // throws has still consumed real time, and skipping the mark there would
57
+ // leave the watchdog measuring from before the call.
58
+ guards.onProgress?.();
59
+ }
60
+ };
61
+ }
@@ -9,7 +9,7 @@
9
9
  * providers so they can share a single implementation.
10
10
  */
11
11
  import type { GenerateStopReason, ThinkingConfig, AgenticLoopOptions, ChatMessage, CollectedChunkResult, MinimalChatMessage, NativeFunctionCall, NativeFunctionResponse, NativeToolDeclarationsResult, NativeToolsConfig, StreamChannel, VertexNativePart, GeminiMultimodalInput, MultimodalAudioEntry } from "../../types/index.js";
12
- import type { Tool, GeminiToolExecutionGuards } from "../../types/index.js";
12
+ import type { Tool, ToolExecutionGuards } from "../../types/index.js";
13
13
  /**
14
14
  * A per-turn tool execute map that deduplicates identical tool calls.
15
15
  *
@@ -34,30 +34,6 @@ export declare class DedupExecuteMap extends Map<string, Tool["execute"]> {
34
34
  * This handles both Zod schemas and plain JSON Schema objects for tool parameters.
35
35
  */
36
36
  export declare function buildNativeToolDeclarations(tools: Record<string, Tool>, reservedNames?: ReadonlySet<string>): NativeToolDeclarationsResult;
37
- /**
38
- * Mid-turn tool sync for the native Gemini loops that build their snapshot
39
- * via buildNativeToolDeclarations. `search_tools` (tools.discovery) hydrates
40
- * discovered tools into the live record between steps; without this refresh
41
- * they stay invisible to the rest of the turn and every call dies as
42
- * TOOL_NOT_FOUND. Mutates the snapshot in place — the request config holds
43
- * `toolsConfig` by reference — and returns true when anything was added.
44
- */
45
- /**
46
- * Everything a native Gemini loop wraps around a tool call that the shared
47
- * engine does not do itself.
48
- *
49
- * Order matters. `raceWithAbort` sits INSIDE `withTimeout` so a turn-level
50
- * abort is observed the moment it fires rather than after the tool settles,
51
- * and the timeout still bounds a tool that neither settles nor honours its
52
- * signal. The progress pings bracket the await because the stall watchdog is
53
- * a whole-turn interval comparing wall-clock against the last progress mark —
54
- * without them a legitimately slow tool reads as a stalled turn and is killed.
55
- *
56
- * Exported because a tool hydrated MID-TURN has to be wrapped the same way as
57
- * one declared up front; keeping this inline made the discovered tool the one
58
- * executor in the system that ran raw.
59
- */
60
- export declare function guardToolExecutor(name: string, execute: NonNullable<Tool["execute"]>, guards: GeminiToolExecutionGuards): (args: Record<string, unknown>, opts: unknown) => Promise<unknown>;
61
37
  /**
62
38
  * Build the tool record handed to `runAgenticLoop`, routed through the turn's
63
39
  * DedupExecuteMap.
@@ -75,7 +51,7 @@ export declare function guardToolExecutor(name: string, execute: NonNullable<Too
75
51
  * identical in every test that calls a tool once, and silently reintroduces
76
52
  * duplicate side effects the moment the model repeats itself.
77
53
  */
78
- export declare function buildDedupedEngineTools(declarations: NativeToolDeclarationsResult | undefined, tools: Record<string, Tool> | undefined, guards?: GeminiToolExecutionGuards): NonNullable<AgenticLoopOptions["tools"]>;
54
+ export declare function buildDedupedEngineTools(declarations: NativeToolDeclarationsResult | undefined, tools: Record<string, Tool> | undefined, guards?: ToolExecutionGuards): NonNullable<AgenticLoopOptions["tools"]>;
79
55
  export declare function refreshNativeToolDeclarations(liveTools: Record<string, Tool> | undefined, current: NativeToolDeclarationsResult): string[];
80
56
  /**
81
57
  * Build the native @google/genai config object shared by stream and generate.
@@ -14,11 +14,11 @@ import { extname } from "node:path";
14
14
  import { DEFAULT_CONTEXT_GUARD_RATIO, DEFAULT_MAX_STEPS, DEFAULT_TOOL_MAX_RETRIES, DEFAULT_WRAPUP_TIME_LEAD_MS, } from "../../core/constants.js";
15
15
  import { needsAudioTranscode, toProviderCompatibleAudio, } from "../../adapters/audioFormatSupport.js";
16
16
  import { logger } from "../../utils/logger.js";
17
+ import { guardToolExecutor } from "../../core/toolExecutionGuards.js";
17
18
  import { resolveSamplingParams } from "../../models/modelRegistry.js";
18
19
  import { convertZodToJsonSchema, ensureNestedSchemaTypes, inlineJsonSchema, isZodSchema, } from "../../utils/schemaConversion.js";
19
20
  import { createNativeThinkingConfig } from "../../utils/thinkingConfig.js";
20
21
  import { resolveLiveTool } from "../../tools/toolDiscovery.js";
21
- import { raceWithAbort, withTimeout } from "../../utils/async/index.js";
22
22
  // ── Functions ──
23
23
  /** Stable, key-order-independent serialization of tool args for the dedup key. */
24
24
  function stableStringifyForDedup(value) {
@@ -324,53 +324,6 @@ export function buildNativeToolDeclarations(tools, reservedNames) {
324
324
  originalNameMap,
325
325
  };
326
326
  }
327
- /**
328
- * Mid-turn tool sync for the native Gemini loops that build their snapshot
329
- * via buildNativeToolDeclarations. `search_tools` (tools.discovery) hydrates
330
- * discovered tools into the live record between steps; without this refresh
331
- * they stay invisible to the rest of the turn and every call dies as
332
- * TOOL_NOT_FOUND. Mutates the snapshot in place — the request config holds
333
- * `toolsConfig` by reference — and returns true when anything was added.
334
- */
335
- /**
336
- * Everything a native Gemini loop wraps around a tool call that the shared
337
- * engine does not do itself.
338
- *
339
- * Order matters. `raceWithAbort` sits INSIDE `withTimeout` so a turn-level
340
- * abort is observed the moment it fires rather than after the tool settles,
341
- * and the timeout still bounds a tool that neither settles nor honours its
342
- * signal. The progress pings bracket the await because the stall watchdog is
343
- * a whole-turn interval comparing wall-clock against the last progress mark —
344
- * without them a legitimately slow tool reads as a stalled turn and is killed.
345
- *
346
- * Exported because a tool hydrated MID-TURN has to be wrapped the same way as
347
- * one declared up front; keeping this inline made the discovered tool the one
348
- * executor in the system that ran raw.
349
- */
350
- export function guardToolExecutor(name, execute, guards) {
351
- return async (args, opts) => {
352
- const invoke = () => Promise.resolve(execute(args, opts));
353
- // The span wraps the CALL, not the guard: a timeout or an abort is a fact
354
- // about this tool invocation and belongs inside its observation.
355
- const wrapInSpan = guards.withToolSpan;
356
- const call = wrapInSpan ? () => wrapInSpan(name, invoke) : invoke;
357
- guards.onProgress?.();
358
- try {
359
- const raced = guards.abortSignal
360
- ? raceWithAbort(call(), guards.abortSignal)
361
- : call();
362
- return await (guards.toolTimeoutMs === undefined
363
- ? raced
364
- : withTimeout(raced, guards.toolTimeoutMs, `Tool "${name}" execution timed out after ${guards.toolTimeoutMs}ms`));
365
- }
366
- finally {
367
- // In `finally`, not after a successful await: a tool that times out or
368
- // throws has still consumed real time, and skipping the mark there would
369
- // leave the watchdog measuring from before the call.
370
- guards.onProgress?.();
371
- }
372
- };
373
- }
374
327
  /**
375
328
  * Build the tool record handed to `runAgenticLoop`, routed through the turn's
376
329
  * DedupExecuteMap.
@@ -1,6 +1,7 @@
1
1
  /* eslint-disable max-lines-per-function */
2
2
  // Native SDK imports - no more @ai-sdk/google-vertex dependency
3
3
  import fs from "fs";
4
+ import { guardToolExecutor } from "../../core/toolExecutionGuards.js";
4
5
  import path from "path";
5
6
  import { ErrorCategory, ErrorSeverity, } from "../../constants/enums.js";
6
7
  import { BaseProvider } from "../../core/baseProvider.js";
@@ -33,7 +34,6 @@ import { appendStepText, buildAbortedTurnMessage, buildContextCapMessage, buildD
33
34
  import { createGeminiLoopAdapter } from "../../core/geminiLoopAdapter.js";
34
35
  import { runAgenticLoop } from "../../core/loopEngine.js";
35
36
  import { createAnthropicLoopAdapter } from "../anthropic/loopAdapter.js";
36
- import { guardToolExecutor } from "../googleNativeGemini3/utils.js";
37
37
  import { extractMcpToolErrorMessage } from "../../utils/mcpErrorText.js";
38
38
  import { createStreamChannel } from "../../core/streamChannel.js";
39
39
  import { toNativeToolDeclarations } from "../../core/nativeToolFormat.js";
@@ -20,6 +20,14 @@
20
20
  */
21
21
  /** Google's role for assistant turns. */
22
22
  const MODEL_ROLE = "model";
23
+ /**
24
+ * Sent as `input.text` when a request's final turn is a model turn.
25
+ *
26
+ * Google lets a client continue generation from the assistant's own last turn;
27
+ * the chat-completions wire format the engine targets has no way to say that,
28
+ * and an empty prompt is rejected before any provider is reached.
29
+ */
30
+ const CONTINUATION_PROMPT = "Continue.";
23
31
  function partsToText(parts) {
24
32
  if (!Array.isArray(parts)) {
25
33
  return "";
@@ -91,11 +99,24 @@ export function parseGeminiRequest(model, body, stream) {
91
99
  // it, which is the same lost-turn bug one case further along.
92
100
  //
93
101
  // A terminal placeholder restores the invariant: the slice removes this
94
- // instead of the model turn. It is never sent anywhere — `prompt` is
95
- // independently "" in exactly this case, so the placeholder only exists to be
96
- // consumed by the slice.
102
+ // instead of the model turn.
103
+ //
104
+ // The prompt needs its own answer, and leaving it "" was a 500. A
105
+ // model-final request carries no user turn to send as `input.text`, and
106
+ // NeuroLink's stream() rejects an empty one outright — "Stream options must
107
+ // include either input.text, input.audio, or stt.audio" — so every continue
108
+ // from a model turn failed at the door rather than reaching a provider. The
109
+ // placeholder above fixed the history slice but never exercised this path,
110
+ // because nothing had sent the request.
111
+ //
112
+ // Google's semantics for a model-final `contents` are "keep going", and the
113
+ // chat-completions shape the engine translates into has no assistant-prefill
114
+ // to express that. An explicit continuation instruction is the closest
115
+ // faithful equivalent: the full conversation still arrives as history, and
116
+ // the model is told to continue it rather than being handed an empty turn.
97
117
  if (turns.length > 0 && turns[turns.length - 1].role !== "user") {
98
- conversationMessages.push({ role: "user", content: "" });
118
+ conversationMessages.push({ role: "user", content: CONTINUATION_PROMPT });
119
+ prompt = CONTINUATION_PROMPT;
99
120
  }
100
121
  const numeric = (v) => typeof v === "number" && Number.isFinite(v) ? v : undefined;
101
122
  const stops = generationConfig.stopSequences;
@@ -289,16 +289,22 @@ export type AnthropicLoopAdapterConfig<TMessage = Anthropic.Messages.MessagePara
289
289
  abortSignal?: AbortSignal;
290
290
  };
291
291
  /**
292
- * The three things a native Gemini loop wraps around every tool call that the
293
- * shared engine does not do itself.
292
+ * The things a native loop wraps around every tool call that the shared engine
293
+ * does not do itself.
294
294
  *
295
- * All optional, and the whole object is optional, because the two Gemini
296
- * providers differ here: Vertex bounds tool execution and runs a stall
297
- * watchdog, AI Studio does neither. Passing nothing leaves an executor exactly
298
- * as the caller supplied it, so this cannot quietly give AI Studio behaviour
299
- * its hand-rolled loops never had.
295
+ * NOT Gemini-specific, despite where this started. Both Vertex+Claude loops
296
+ * apply these guards directly via `guardToolExecutor`, and the Gemini adapter
297
+ * applies them to hydrated tools, so the former `GeminiToolExecutionGuards`
298
+ * name described the first caller rather than the contract and pointed the
299
+ * next reader at the wrong provider family.
300
+ *
301
+ * All optional, and the whole object is optional, because callers differ:
302
+ * Vertex bounds tool execution and runs a stall watchdog, AI Studio does
303
+ * neither. Passing nothing leaves an executor exactly as the caller supplied
304
+ * it, so this cannot quietly give a provider behaviour its hand-rolled loops
305
+ * never had.
300
306
  */
301
- export type GeminiToolExecutionGuards = {
307
+ export type ToolExecutionGuards = {
302
308
  /** Upper bound on a single execute(); omit for no bound. */
303
309
  toolTimeoutMs?: number;
304
310
  /**
@@ -328,6 +334,13 @@ export type GeminiToolExecutionGuards = {
328
334
  */
329
335
  withToolSpan?: <T>(name: string, run: () => Promise<T>) => Promise<T>;
330
336
  };
337
+ /**
338
+ * @deprecated Renamed to `ToolExecutionGuards` — the guards were never
339
+ * Gemini-specific. Kept because this name is re-exported from the package root
340
+ * via the types barrel, so removing it outright would break any consumer that
341
+ * imports it (CLAUDE.md rule 5). Safe to drop at the next major.
342
+ */
343
+ export type GeminiToolExecutionGuards = ToolExecutionGuards;
331
344
  /** What one Gemini step produced, carried to `buildToolResultMessages`. */
332
345
  export type GeminiStepRaw = {
333
346
  rawResponseParts: unknown[];
@@ -394,7 +407,7 @@ export type GeminiLoopAdapterCoreConfig = {
394
407
  * the opposite of what discovery is for — the tool the model just found is
395
408
  * the one most likely to be called repeatedly with the same arguments.
396
409
  */
397
- toolGuards?: GeminiToolExecutionGuards;
410
+ toolGuards?: ToolExecutionGuards;
398
411
  /**
399
412
  * Name of the terminal structured-output tool when one is in play. A call
400
413
  * to it ends the turn: its arguments ARE the answer, so it is reported as
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "11.18.3",
3
+ "version": "11.18.5",
4
4
  "packageManager": "pnpm@10.15.1",
5
5
  "description": "TypeScript AI SDK with 24+ LLM providers behind one consistent API. MCP-native (connect any MCP server), voice TTS/STT/realtime, RAG, agents, memory, context compaction. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · DeepSeek · NVIDIA NIM and more.",
6
6
  "author": {