@juspay/neurolink 11.15.1 → 11.15.3

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.
@@ -107,8 +107,11 @@ export function runAgenticLoop(adapter, initialConversation, options) {
107
107
  catch (err) {
108
108
  throw hasEmitted ? new PostEmissionStepError(err) : err;
109
109
  }
110
- }, undefined, // no OTel span threaded through the engine today; adapters instrument their own steps if they need span-level detail
111
- `${adapter.providerLabel}.step`);
110
+ },
111
+ // The caller's span, when it passes one. withProviderRetry writes
112
+ // gen_ai.provider.total_attempts here, so a loop that threaded a
113
+ // span before it moved onto this engine keeps emitting it.
114
+ options.span, `${adapter.providerLabel}.step`);
112
115
  }
113
116
  catch (err) {
114
117
  throw err instanceof PostEmissionStepError ? err.cause : err;
@@ -1697,9 +1697,17 @@ export class AnthropicProvider extends BaseProvider {
1697
1697
  },
1698
1698
  };
1699
1699
  }
1700
+ // The active span goes with it. Before this loop moved onto the shared
1701
+ // engine it called
1702
+ // withProviderRetry(fn, trace.getActiveSpan() ?? undefined, label)
1703
+ // and that span is where gen_ai.provider.total_attempts is recorded.
1704
+ // The engine passed `undefined` in its place, so the attribute silently
1705
+ // stopped being emitted for every native Anthropic turn.
1706
+ const activeSpan = trace.getActiveSpan();
1700
1707
  const { stream, resultPromise } = runAgenticLoop(adapter, payload.messages.slice(), {
1701
1708
  tools: engineTools,
1702
1709
  ...(abortSignal ? { abortSignal } : {}),
1710
+ ...(activeSpan ? { span: activeSpan } : {}),
1703
1711
  });
1704
1712
  // Structured turns buffer their text rather than streaming it: a caller
1705
1713
  // that passed a schema needs parseable JSON, and deltas emitted before
@@ -17,7 +17,7 @@ import { withTimeout } from "../../utils/async/index.js";
17
17
  import { estimateTokens } from "../../utils/tokenEstimation.js";
18
18
  import { transformToolExecutions } from "../../utils/transformationUtils.js";
19
19
  import { resolveToolExecutionRecords } from "../../core/toolExecutionRecorder.js";
20
- import { buildGeminiResponseSchema, buildNativeConfig, computeMaxSteps, createContextGuard, buildUserPartsWithMultimodal, extractThoughtSignature, handleMaxStepsTermination, prependConversationMessages, } from "../googleNativeGemini3/index.js";
20
+ import { buildDedupedEngineTools, buildGeminiResponseSchema, buildNativeConfig, computeMaxSteps, createContextGuard, buildUserPartsWithMultimodal, extractThoughtSignature, handleMaxStepsTermination, prependConversationMessages, } from "../googleNativeGemini3/index.js";
21
21
  import { createStreamChannel } from "../../core/streamChannel.js";
22
22
  import { toNativeToolDeclarations } from "../../core/nativeToolFormat.js";
23
23
  import { warnGoogleSdkIgnoresProxy } from "../../proxy/proxyFetch.js";
@@ -835,16 +835,10 @@ export class GoogleAIStudioProvider extends BaseProvider {
835
835
  return next;
836
836
  },
837
837
  };
838
- const engineTools = {};
839
- for (const [name, tool] of Object.entries(options.tools ?? {})) {
840
- const execute = tool?.execute;
841
- if (!execute) {
842
- continue;
843
- }
844
- engineTools[name] = {
845
- execute: async (args, opts) => execute(args, opts),
846
- };
847
- }
838
+ // Through the turn's DedupExecuteMap, NOT the raw executors:
839
+ // `.get()` returns the dedup wrapper that answers an identical
840
+ // repeated {name, args} from the per-turn cache (BZ-3327).
841
+ const engineTools = buildDedupedEngineTools(declarationsResult, options.tools);
848
842
  const { stream: engineStream, resultPromise } = runAgenticLoop(adapter, currentContents, {
849
843
  tools: engineTools,
850
844
  ...(composedSignal ? { abortSignal: composedSignal } : {}),
@@ -1151,16 +1145,8 @@ export class GoogleAIStudioProvider extends BaseProvider {
1151
1145
  return next;
1152
1146
  },
1153
1147
  };
1154
- const engineTools = {};
1155
- for (const [name, tool] of Object.entries(options.tools ?? {})) {
1156
- const execute = tool?.execute;
1157
- if (!execute) {
1158
- continue;
1159
- }
1160
- engineTools[name] = {
1161
- execute: async (args, opts) => execute(args, opts),
1162
- };
1163
- }
1148
+ // Same dedup routing as the streaming twin above.
1149
+ const engineTools = buildDedupedEngineTools(declarationsResult, options.tools);
1164
1150
  const { stream: engineStream, resultPromise } = runAgenticLoop(adapter, currentContents, {
1165
1151
  tools: engineTools,
1166
1152
  ...(composedSignal ? { abortSignal: composedSignal } : {}),
@@ -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, StreamChannel, VertexNativePart, GeminiMultimodalInput, MultimodalAudioEntry } from "../../types/index.js";
11
+ import type { GenerateStopReason, ThinkingConfig, AgenticLoopOptions, 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.
@@ -91,6 +91,24 @@ 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
+ * Build the tool record handed to `runAgenticLoop`, routed through the turn's
96
+ * DedupExecuteMap.
97
+ *
98
+ * The engine looks tools up by the name the adapter reports, which is the
99
+ * ORIGINAL caller-facing name; `executeMap` is keyed by the SANITIZED wire
100
+ * name Google actually declares. `originalNameMap` is the bridge, and it
101
+ * carries an entry for every converted tool (identity mappings included), so
102
+ * iterating it yields exactly the declared, executable set.
103
+ *
104
+ * Going through `executeMap.get()` rather than the raw `tool.execute` is the
105
+ * entire point: `.get()` returns the dedup wrapper, so an identical
106
+ * {name, args} repeated within one turn is answered from the per-turn cache
107
+ * instead of running the tool again (BZ-3327). Passing the raw executor looks
108
+ * identical in every test that calls a tool once, and silently reintroduces
109
+ * duplicate side effects the moment the model repeats itself.
110
+ */
111
+ export declare function buildDedupedEngineTools(declarations: NativeToolDeclarationsResult | undefined, tools: Record<string, Tool> | undefined): NonNullable<AgenticLoopOptions["tools"]>;
94
112
  export declare function refreshNativeToolDeclarations(liveTools: Record<string, Tool> | undefined, current: NativeToolDeclarationsResult): boolean;
95
113
  /**
96
114
  * Build the native @google/genai config object shared by stream and generate.
@@ -455,6 +455,51 @@ export function buildNativeToolDeclarations(tools, reservedNames) {
455
455
  * TOOL_NOT_FOUND. Mutates the snapshot in place — the request config holds
456
456
  * `toolsConfig` by reference — and returns true when anything was added.
457
457
  */
458
+ /**
459
+ * Build the tool record handed to `runAgenticLoop`, routed through the turn's
460
+ * DedupExecuteMap.
461
+ *
462
+ * The engine looks tools up by the name the adapter reports, which is the
463
+ * ORIGINAL caller-facing name; `executeMap` is keyed by the SANITIZED wire
464
+ * name Google actually declares. `originalNameMap` is the bridge, and it
465
+ * carries an entry for every converted tool (identity mappings included), so
466
+ * iterating it yields exactly the declared, executable set.
467
+ *
468
+ * Going through `executeMap.get()` rather than the raw `tool.execute` is the
469
+ * entire point: `.get()` returns the dedup wrapper, so an identical
470
+ * {name, args} repeated within one turn is answered from the per-turn cache
471
+ * instead of running the tool again (BZ-3327). Passing the raw executor looks
472
+ * identical in every test that calls a tool once, and silently reintroduces
473
+ * duplicate side effects the moment the model repeats itself.
474
+ */
475
+ export function buildDedupedEngineTools(declarations, tools) {
476
+ const engineTools = {};
477
+ if (declarations) {
478
+ for (const [safeName, originalName] of declarations.originalNameMap) {
479
+ const execute = declarations.executeMap.get(safeName);
480
+ if (!execute) {
481
+ continue;
482
+ }
483
+ engineTools[originalName] = {
484
+ execute: async (args, opts) => execute(args, opts),
485
+ };
486
+ }
487
+ return engineTools;
488
+ }
489
+ // No declarations were built (no tools, or a path that skips the snapshot).
490
+ // Fall back to the caller's own executors so this helper can never REMOVE a
491
+ // tool that would otherwise have been callable.
492
+ for (const [name, tool] of Object.entries(tools ?? {})) {
493
+ const execute = tool?.execute;
494
+ if (!execute) {
495
+ continue;
496
+ }
497
+ engineTools[name] = {
498
+ execute: async (args, opts) => execute(args, opts),
499
+ };
500
+ }
501
+ return engineTools;
502
+ }
458
503
  export function refreshNativeToolDeclarations(liveTools, current) {
459
504
  if (!liveTools) {
460
505
  return false;
@@ -1,4 +1,5 @@
1
1
  import type Anthropic from "@anthropic-ai/sdk";
2
+ import type { Span } from "@opentelemetry/api";
2
3
  import type { Tool } from "./tools.js";
3
4
  import type { CollectedChunkResult, NativeFunctionCall, NativeToolDeclarationsResult } from "./providers.js";
4
5
  /**
@@ -292,6 +293,19 @@ export type AgenticLoopOptions = {
292
293
  execute?: (args: Record<string, unknown>, opts: unknown) => Promise<unknown>;
293
294
  }>;
294
295
  abortSignal?: AbortSignal;
296
+ /**
297
+ * Span the per-step provider retry annotates, via
298
+ * `withProviderRetry(..., span, ...)` — it records
299
+ * `gen_ai.provider.total_attempts` on every completed step, retried or not.
300
+ *
301
+ * Caller-supplied rather than read from the ambient context inside the
302
+ * engine. Reading it here would hand the attribute to every provider on the
303
+ * engine, including ones whose hand-rolled loops never emitted it, and a
304
+ * refactor that silently ADDS observable behaviour is the same defect as one
305
+ * that silently drops it. Today only the direct Anthropic loops set this,
306
+ * because only they threaded a span before moving onto the engine.
307
+ */
308
+ span?: Span;
295
309
  };
296
310
  export type AgenticLoopResult<TConversation> = {
297
311
  text: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "11.15.1",
3
+ "version": "11.15.3",
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": {