@juspay/neurolink 12.37.1 → 12.37.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.
@@ -37,6 +37,14 @@ export function createGeminiLoopAdapter(config) {
37
37
  }
38
38
  return original;
39
39
  };
40
+ // Per-turn monotonic counter for `toolCallId` generation. `executeStep` is
41
+ // called once per loop step, and a fresh `createGeminiLoopAdapter` closure
42
+ // is made per turn (per generate()/stream() call), so this resets at the
43
+ // right lifetime while staying unique across every step of one turn —
44
+ // unlike the step-local array index it replaces, which restarted at 0 on
45
+ // every step and let two calls to the same tool in different steps collide
46
+ // on one id once both landed in the same flattened per-turn list.
47
+ let nextCallIndex = 0;
40
48
  return {
41
49
  providerLabel: config.providerLabel,
42
50
  maxSteps: config.maxSteps,
@@ -138,9 +146,11 @@ export function createGeminiLoopAdapter(config) {
138
146
  // projects the next request from.
139
147
  config.noteUsage?.(collected.inputTokens, collected.outputTokens);
140
148
  const text = extractTextFromParts(collected.rawResponseParts);
141
- // Names cross the engine boundary in their ORIGINAL form.
142
- const allCalls = collected.stepFunctionCalls.map((call, index) => ({
143
- id: `${config.providerLabel}_${index}_${call.name}`,
149
+ // Names cross the engine boundary in their ORIGINAL form. `id` uses the
150
+ // turn-scoped counter above, not a step-local map index, so two calls
151
+ // to the same tool in different steps of one turn get distinct ids.
152
+ const allCalls = collected.stepFunctionCalls.map((call) => ({
153
+ id: `${config.providerLabel}_${nextCallIndex++}_${call.name}`,
144
154
  name: originalNameFor(call.name),
145
155
  args: call.args,
146
156
  }));
package/dist/neurolink.js CHANGED
@@ -8774,6 +8774,21 @@ Current user's request: ${currentInput}`;
8774
8774
  let availableTools = await this.getAllAvailableTools();
8775
8775
  // Apply per-call tool filtering for system prompt tool descriptions
8776
8776
  availableTools = this.applyToolInfoFiltering(availableTools, options);
8777
+ // M3: a provider whose descriptor declares toolSupport "none" can never
8778
+ // call a tool in ANY form — not natively, not via prompt injection — so
8779
+ // `availableTools` here is dead weight, not a signal. Left non-empty, it
8780
+ // still drives createToolAwareSystemPrompt()'s prompt-injection branch
8781
+ // (that branch triggers on `!provider.supportsTools()`, which is also
8782
+ // true for these providers) and inflates the stream budget check below
8783
+ // via `toolDefinitions`, so an image-generation-only provider like
8784
+ // Recraft can fail a fabricated "exceeds model budget" check before its
8785
+ // own request-kind routing (BaseProvider.stream() -> resolveRequestKind)
8786
+ // ever runs. Clearing it here restores the provider's real outcome —
8787
+ // its own refusal, a genuine vendor error, or success — as what
8788
+ // surfaces, without touching the budget check for any text provider.
8789
+ if (ProviderFactory.getDescriptor(providerName)?.toolSupport === "none") {
8790
+ availableTools = [];
8791
+ }
8777
8792
  // Skip tool prompt injection if skipToolPromptInjection is true.
8778
8793
  // For providers with native tool calling (instance truth via
8779
8794
  // supportsTools()), the listing is skipped and only the short damping
@@ -2595,7 +2595,11 @@ export class GoogleVertexProvider extends BaseProvider {
2595
2595
  finalText = engineResult.text;
2596
2596
  lastFinishReason = engineResult.rawStopReason ?? lastFinishReason;
2597
2597
  for (const call of engineResult.toolCalls) {
2598
- allToolCalls.push({ toolName: call.name, args: call.args });
2598
+ allToolCalls.push({
2599
+ toolCallId: call.id,
2600
+ toolName: call.name,
2601
+ args: call.args,
2602
+ });
2599
2603
  }
2600
2604
  for (const execution of engineResult.toolExecutions) {
2601
2605
  toolExecutions.push({
@@ -2747,6 +2751,17 @@ export class GoogleVertexProvider extends BaseProvider {
2747
2751
  reasoningTokens: totalReasoningTokens,
2748
2752
  }),
2749
2753
  responseTime,
2754
+ // The complete attempted-call record (name/args/id), independent of
2755
+ // toolsUsed's success-only names — mirrors the native Anthropic direct
2756
+ // / OpenAI-compatible / SageMaker paths (toolCallsFromSummaries) and
2757
+ // the working Gemini-on-Vertex STREAM path just above. Previously
2758
+ // absent here, so a tool the loop ran internally never reached the
2759
+ // caller's result.toolCalls even though toolsUsed/toolExecutions did.
2760
+ toolCalls: externalToolCalls.map((tc) => ({
2761
+ toolCallId: tc.toolCallId,
2762
+ toolName: tc.toolName,
2763
+ args: tc.args,
2764
+ })),
2750
2765
  toolsUsed: externalToolCalls.map((tc) => tc.toolName),
2751
2766
  toolExecutions: resolveToolExecutionRecords(options, externalToolExecutions),
2752
2767
  enhancedWithTools: externalToolCalls.length > 0,
@@ -4699,7 +4714,11 @@ export class GoogleVertexProvider extends BaseProvider {
4699
4714
  },
4700
4715
  buildToolResultMessages: (conversation, stepResult, toolResults, engineStep) => {
4701
4716
  for (const result of toolResults) {
4702
- allToolCalls.push({ toolName: result.name, args: result.args });
4717
+ allToolCalls.push({
4718
+ toolCallId: result.id,
4719
+ toolName: result.name,
4720
+ args: result.args,
4721
+ });
4703
4722
  // Recorded here as well: `toolExecutions` feeds
4704
4723
  // resolveToolExecutionRecords and the result's own
4705
4724
  // toolExecutions, and pushing only to allToolCalls left it empty
@@ -5059,7 +5078,23 @@ export class GoogleVertexProvider extends BaseProvider {
5059
5078
  }
5060
5079
  releaseTurnResources();
5061
5080
  const responseTime = Date.now() - startTime;
5062
- const externalToolCalls = allToolCalls.filter((tc) => tc.toolName !== "final_result");
5081
+ // Sourced from `engineResult.toolCalls` when the turn produced one, not
5082
+ // the local `allToolCalls` (fed only by the `buildToolResultMessages`
5083
+ // hook below). The engine's own accumulator is pushed to unconditionally
5084
+ // per dispatched batch, before the abortedMidBatch check that skips
5085
+ // `buildToolResultMessages` for that step — so a tool that finished
5086
+ // executing in a batch cut short by an abort is present in
5087
+ // `engineResult.toolCalls` but would otherwise never reach
5088
+ // `allToolCalls`. `allToolCalls` remains the fallback for the rare case
5089
+ // where `resultPromise` itself rejects and `engineResult` never resolves.
5090
+ const engineToolCalls = engineResult
5091
+ ? engineResult.toolCalls.map((call) => ({
5092
+ toolCallId: call.id,
5093
+ toolName: call.name,
5094
+ args: call.args,
5095
+ }))
5096
+ : allToolCalls;
5097
+ const externalToolCalls = engineToolCalls.filter((tc) => tc.toolName !== "final_result");
5063
5098
  const externalToolExecutions = toolExecutions.filter((te) => te.name !== "final_result");
5064
5099
  // Absolute backstop — no code path may surface empty content on a turn
5065
5100
  // that ran tools (the consumer-facing "empty response" incident shape).
@@ -5136,6 +5171,17 @@ export class GoogleVertexProvider extends BaseProvider {
5136
5171
  ...(totalReasoningTokens > 0 && { reasoning: totalReasoningTokens }),
5137
5172
  },
5138
5173
  responseTime,
5174
+ // The complete attempted-call record (name/args/id), independent of
5175
+ // toolsUsed's success-only names — mirrors the native Anthropic direct
5176
+ // / OpenAI-compatible / SageMaker paths (toolCallsFromSummaries) and
5177
+ // the working Gemini-on-Vertex STREAM path. Previously absent here, so
5178
+ // a tool the loop ran internally never reached the caller's
5179
+ // result.toolCalls even though toolsUsed/toolExecutions did.
5180
+ toolCalls: externalToolCalls.map((tc) => ({
5181
+ toolCallId: tc.toolCallId,
5182
+ toolName: tc.toolName,
5183
+ args: tc.args,
5184
+ })),
5139
5185
  toolsUsed: externalToolCalls.map((tc) => tc.toolName),
5140
5186
  toolExecutions: resolveToolExecutionRecords(options, externalToolExecutions),
5141
5187
  enhancedWithTools: externalToolCalls.length > 0,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "12.37.1",
3
+ "version": "12.37.3",
4
4
  "packageManager": "pnpm@10.15.1",
5
5
  "description": "The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002/call) for routing, tool selection and context budgeting. MCP-native (4 transports), voice TTS/STT/realtime, RAG, agents, memory, compaction, 9 observability exporters. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · TypeSafe Jev and more.",
6
6
  "author": {