@juspay/neurolink 11.16.0 → 11.16.1

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,7 +16,8 @@
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 { collectStreamChunksIncremental, extractTextFromParts, mapGeminiFinishReason, pushModelResponseToHistory, refreshNativeToolDeclarations, } from "../providers/googleNativeGemini3/utils.js";
19
+ import { resolveLiveTool } from "../tools/toolDiscovery.js";
20
+ import { collectStreamChunksIncremental, guardToolExecutor, extractTextFromParts, mapGeminiFinishReason, pushModelResponseToHistory, refreshNativeToolDeclarations, } from "../providers/googleNativeGemini3/utils.js";
20
21
  export function createGeminiLoopAdapter(config) {
21
22
  /**
22
23
  * Sanitized wire name -> the name the caller registered. Rebuilt per step
@@ -87,17 +88,34 @@ export function createGeminiLoopAdapter(config) {
87
88
  }
88
89
  : {}),
89
90
  resolveToolOnMiss: (name) => {
90
- const tool = config.liveTools?.[name];
91
+ // resolveLiveTool, NOT a plain record lookup. A deferred-catalog tool is
92
+ // not a key on the record at all — it lives behind a symbol-keyed
93
+ // resolver — so `liveTools[name]` returns undefined for exactly the
94
+ // tools this hook exists to hydrate, and the model gets TOOL_NOT_FOUND
95
+ // for a tool it was told about.
96
+ const tool = resolveLiveTool(config.liveTools, name);
91
97
  const execute = tool?.execute;
92
98
  if (!execute) {
93
99
  return undefined;
94
100
  }
95
- // Wrapped because the hook types `opts` as `unknown`, and a function
96
- // declaring a narrower options type is not assignable to one accepting
97
- // `unknown`. One assertion at the boundary, never a double assertion.
98
- return {
99
- execute: async (args, opts) => execute(args, opts),
100
- };
101
+ // Registered into the turn's DedupExecuteMap and taken back through
102
+ // `.get()`, so a hydrated tool gets the same per-turn result cache as a
103
+ // declared one. A tool the model just discovered is the one most likely
104
+ // to be re-requested with identical arguments.
105
+ const executeMap = config.declarations?.executeMap;
106
+ let resolved = execute;
107
+ if (executeMap) {
108
+ executeMap.set(name, execute);
109
+ resolved = executeMap.get(name) ?? execute;
110
+ }
111
+ const guarded = config.toolGuards
112
+ ? guardToolExecutor(name, resolved, config.toolGuards)
113
+ : // Wrapped because the hook types `opts` as `unknown`, and a function
114
+ // declaring a narrower options type is not assignable to one
115
+ // accepting `unknown`. One assertion at the boundary, never a double
116
+ // assertion.
117
+ async (args, opts) => resolved(args, opts);
118
+ return { execute: guarded };
101
119
  },
102
120
  async executeStep(request, channel, signal) {
103
121
  const rawStream = await config.sendStep(request.raw, signal);
@@ -87,9 +87,19 @@ async function dispatchStepTools(params) {
87
87
  // a deferred-catalog placeholder is exactly that shape, and it is
88
88
  // precisely what hydration exists to resolve.
89
89
  const declaredTool = tools?.[call.name];
90
- const tool = declaredTool?.execute
91
- ? declaredTool
92
- : (adapter.resolveToolOnMiss?.(call.name) ?? declaredTool);
90
+ let tool = declaredTool;
91
+ if (!declaredTool?.execute) {
92
+ const hydrated = adapter.resolveToolOnMiss?.(call.name);
93
+ if (hydrated) {
94
+ tool = hydrated;
95
+ // It resolves NOW, so any strikes standing against this name were
96
+ // recorded while it genuinely did not resolve — snapshot artifacts of
97
+ // a deferred catalog, not failures of a tool that exists. Leaving them
98
+ // in place would disable the tool at the exact moment it became
99
+ // usable.
100
+ failedTools.delete(call.name);
101
+ }
102
+ }
93
103
  if (!tool?.execute) {
94
104
  const output = breaker
95
105
  ? {
@@ -91,6 +91,22 @@ export declare function buildNativeToolDeclarations(tools: Record<string, Tool>,
91
91
  * TOOL_NOT_FOUND. Mutates the snapshot in place — the request config holds
92
92
  * `toolsConfig` by reference — and returns true when anything was added.
93
93
  */
94
+ /**
95
+ * Everything a native Gemini loop wraps around a tool call that the shared
96
+ * engine does not do itself.
97
+ *
98
+ * Order matters. `raceWithAbort` sits INSIDE `withTimeout` so a turn-level
99
+ * abort is observed the moment it fires rather than after the tool settles,
100
+ * and the timeout still bounds a tool that neither settles nor honours its
101
+ * signal. The progress pings bracket the await because the stall watchdog is
102
+ * a whole-turn interval comparing wall-clock against the last progress mark —
103
+ * without them a legitimately slow tool reads as a stalled turn and is killed.
104
+ *
105
+ * Exported because a tool hydrated MID-TURN has to be wrapped the same way as
106
+ * one declared up front; keeping this inline made the discovered tool the one
107
+ * executor in the system that ran raw.
108
+ */
109
+ export declare function guardToolExecutor(name: string, execute: NonNullable<Tool["execute"]>, guards: GeminiToolExecutionGuards): (args: Record<string, unknown>, opts: unknown) => Promise<unknown>;
94
110
  /**
95
111
  * Build the tool record handed to `runAgenticLoop`, routed through the turn's
96
112
  * DedupExecuteMap.
@@ -456,6 +456,41 @@ export function buildNativeToolDeclarations(tools, reservedNames) {
456
456
  * TOOL_NOT_FOUND. Mutates the snapshot in place — the request config holds
457
457
  * `toolsConfig` by reference — and returns true when anything was added.
458
458
  */
459
+ /**
460
+ * Everything a native Gemini loop wraps around a tool call that the shared
461
+ * engine does not do itself.
462
+ *
463
+ * Order matters. `raceWithAbort` sits INSIDE `withTimeout` so a turn-level
464
+ * abort is observed the moment it fires rather than after the tool settles,
465
+ * and the timeout still bounds a tool that neither settles nor honours its
466
+ * signal. The progress pings bracket the await because the stall watchdog is
467
+ * a whole-turn interval comparing wall-clock against the last progress mark —
468
+ * without them a legitimately slow tool reads as a stalled turn and is killed.
469
+ *
470
+ * Exported because a tool hydrated MID-TURN has to be wrapped the same way as
471
+ * one declared up front; keeping this inline made the discovered tool the one
472
+ * executor in the system that ran raw.
473
+ */
474
+ export function guardToolExecutor(name, execute, guards) {
475
+ return async (args, opts) => {
476
+ const call = () => Promise.resolve(execute(args, opts));
477
+ guards.onProgress?.();
478
+ try {
479
+ const raced = guards.abortSignal
480
+ ? raceWithAbort(call(), guards.abortSignal)
481
+ : call();
482
+ return await (guards.toolTimeoutMs === undefined
483
+ ? raced
484
+ : withTimeout(raced, guards.toolTimeoutMs, `Tool "${name}" execution timed out after ${guards.toolTimeoutMs}ms`));
485
+ }
486
+ finally {
487
+ // In `finally`, not after a successful await: a tool that times out or
488
+ // throws has still consumed real time, and skipping the mark there would
489
+ // leave the watchdog measuring from before the call.
490
+ guards.onProgress?.();
491
+ }
492
+ };
493
+ }
459
494
  /**
460
495
  * Build the tool record handed to `runAgenticLoop`, routed through the turn's
461
496
  * DedupExecuteMap.
@@ -487,29 +522,9 @@ export function buildDedupedEngineTools(declarations, tools, guards) {
487
522
  * mark — without them a legitimately slow tool reads as a stalled turn and
488
523
  * gets killed.
489
524
  */
490
- const guard = (name, execute) => {
491
- return async (args, opts) => {
492
- const call = () => Promise.resolve(execute(args, opts));
493
- if (!guards) {
494
- return call();
495
- }
496
- guards.onProgress?.();
497
- try {
498
- const raced = guards.abortSignal
499
- ? raceWithAbort(call(), guards.abortSignal)
500
- : call();
501
- return await (guards.toolTimeoutMs === undefined
502
- ? raced
503
- : withTimeout(raced, guards.toolTimeoutMs, `Tool "${name}" execution timed out after ${guards.toolTimeoutMs}ms`));
504
- }
505
- finally {
506
- // In `finally`, not after a successful await: a tool that times out or
507
- // throws has still consumed real time, and skipping the mark there
508
- // would leave the watchdog measuring from before the call.
509
- guards.onProgress?.();
510
- }
511
- };
512
- };
525
+ const guard = (name, execute) => guards
526
+ ? guardToolExecutor(name, execute, guards)
527
+ : async (args, opts) => execute(args, opts);
513
528
  if (declarations) {
514
529
  for (const [safeName, originalName] of declarations.originalNameMap) {
515
530
  const execute = declarations.executeMap.get(safeName);
@@ -1613,6 +1613,13 @@ export class GoogleVertexProvider extends BaseProvider {
1613
1613
  classifyResultFailure: (output) => extractToolFailureText(output) ?? undefined,
1614
1614
  },
1615
1615
  liveTools: options.tools ?? {},
1616
+ // A tool hydrated mid-turn gets the SAME bound, abort race and stall
1617
+ // ping as one declared up front — see guardToolExecutor.
1618
+ toolGuards: {
1619
+ toolTimeoutMs: toolExecTimeoutMs,
1620
+ abortSignal: effectiveSignal,
1621
+ onProgress: () => turnClock.noteProgress(),
1622
+ },
1616
1623
  ...(declarations ? { declarations } : {}),
1617
1624
  ...(useFinalResultTool
1618
1625
  ? {
@@ -2401,6 +2408,13 @@ export class GoogleVertexProvider extends BaseProvider {
2401
2408
  classifyResultFailure: (output) => extractToolFailureText(output) ?? undefined,
2402
2409
  },
2403
2410
  liveTools: options.tools ?? {},
2411
+ // A tool hydrated mid-turn gets the SAME bound, abort race and stall
2412
+ // ping as one declared up front — see guardToolExecutor.
2413
+ toolGuards: {
2414
+ toolTimeoutMs: toolExecTimeoutMs,
2415
+ abortSignal: effectiveSignal,
2416
+ onProgress: () => turnClock.noteProgress(),
2417
+ },
2404
2418
  ...(declarations ? { declarations } : {}),
2405
2419
  ...(useFinalResultTool
2406
2420
  ? {
@@ -326,6 +326,16 @@ export type GeminiLoopAdapterCoreConfig = {
326
326
  * step with that step's real token counts.
327
327
  */
328
328
  noteUsage?: (inputTokens: number, outputTokens: number) => void;
329
+ /**
330
+ * The same guards `buildDedupedEngineTools` wraps declared tools in, applied
331
+ * to one hydrated mid-turn.
332
+ *
333
+ * Without this a tool discovered during a turn is the ONE executor that runs
334
+ * raw: no per-turn dedup, no execution timeout, no stall-clock ping. That is
335
+ * the opposite of what discovery is for — the tool the model just found is
336
+ * the one most likely to be called repeatedly with the same arguments.
337
+ */
338
+ toolGuards?: GeminiToolExecutionGuards;
329
339
  /**
330
340
  * Name of the terminal structured-output tool when one is in play. A call
331
341
  * 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.16.0",
3
+ "version": "11.16.1",
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": {
@@ -22,9 +22,8 @@
22
22
  "url": "https://github.com/sponsors/juspay"
23
23
  },
24
24
  "engines": {
25
- "node": ">=20.19.0",
26
- "npm": ">=10.0.0",
27
- "pnpm": ">=8.0.0"
25
+ "node": ">=22.0.0",
26
+ "pnpm": ">=10.0.0"
28
27
  },
29
28
  "scripts": {
30
29
  "dev": "vite dev",
@@ -444,7 +443,7 @@
444
443
  "koa": "^3.1.1",
445
444
  "koa-bodyparser": "^4.4.1",
446
445
  "livekit-server-sdk": "^2.15.4",
447
- "mammoth": "^1.11.0",
446
+ "mammoth": "1.12.0",
448
447
  "mediabunny": "^1.40.1",
449
448
  "music-metadata": "^11.11.2",
450
449
  "pdf-parse": "^2.4.5",
@@ -460,9 +459,7 @@
460
459
  "@changesets/cli": "^2.29.8",
461
460
  "@electric-sql/pglite": "^0.4.6",
462
461
  "@eslint/js": "^10.0.1",
463
- "@juspay/hippocampus": ">=0.1.7",
464
462
  "@opentelemetry/api": "^1.9.0",
465
- "@opentelemetry/sdk-trace-base": "^2.6.0",
466
463
  "@opentelemetry/sdk-trace-node": "^2.6.0",
467
464
  "@semantic-release/changelog": "^6.0.3",
468
465
  "@semantic-release/commit-analyzer": "^13.0.1",
@@ -481,7 +478,7 @@
481
478
  "@types/koa": "^3.0.1",
482
479
  "@types/koa-bodyparser": "^4.3.13",
483
480
  "@types/koa__cors": "^5.0.1",
484
- "@types/node": "^25.3.3",
481
+ "@types/node": "^22.20.0",
485
482
  "@types/react": "^19.2.10",
486
483
  "@types/tar-stream": "^3.1.4",
487
484
  "@types/ws": "^8.18.1",
@@ -571,7 +568,6 @@
571
568
  "esbuild",
572
569
  "protobufjs",
573
570
  "puppeteer",
574
- "sqlite3",
575
571
  "ffmpeg-static",
576
572
  "sharp"
577
573
  ],