@juspay/neurolink 11.11.2 → 11.11.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.
@@ -6,6 +6,9 @@ import { ATTR, tracers, withClientSpan, withClientStreamSpan, withSpan, } from "
6
6
  import { AuthenticationError, InvalidModelError, NetworkError, ProviderError, RateLimitError, } from "../../types/index.js";
7
7
  import { ERROR_CODES, NeuroLinkError } from "../../utils/errorHandling.js";
8
8
  import { logger } from "../../utils/logger.js";
9
+ import { createGeminiLoopAdapter } from "../../core/geminiLoopAdapter.js";
10
+ import { runAgenticLoop } from "../../core/loopEngine.js";
11
+ import { DEFAULT_TOOL_MAX_RETRIES } from "../../core/constants.js";
9
12
  import { isToolsSchemaExclusionInForce } from "../../core/modules/structuredOutputPolicy.js";
10
13
  import { GEMINI_ELISION_NOTE, planGeminiLoopReclaim, previewGeminiToolResponseText, } from "../../context/geminiLoopGuard.js";
11
14
  import { getAvailableInputTokens, getContextWindowSize, } from "../../constants/contextWindows.js";
@@ -14,7 +17,7 @@ import { withTimeout } from "../../utils/async/index.js";
14
17
  import { estimateTokens } from "../../utils/tokenEstimation.js";
15
18
  import { transformToolExecutions } from "../../utils/transformationUtils.js";
16
19
  import { resolveToolExecutionRecords } from "../../core/toolExecutionRecorder.js";
17
- import { buildGeminiResponseSchema, buildNativeConfig, collectStreamChunks, collectStreamChunksIncremental, computeMaxSteps, createContextGuard, buildUserPartsWithMultimodal, executeNativeToolCalls, extractTextFromParts, extractThoughtSignature, handleMaxStepsTermination, prependConversationMessages, pushModelResponseToHistory, refreshNativeToolDeclarations, DedupExecuteMap, } from "../googleNativeGemini3/index.js";
20
+ import { buildGeminiResponseSchema, buildNativeConfig, collectStreamChunks, computeMaxSteps, createContextGuard, buildUserPartsWithMultimodal, executeNativeToolCalls, extractTextFromParts, extractThoughtSignature, handleMaxStepsTermination, prependConversationMessages, pushModelResponseToHistory, refreshNativeToolDeclarations, DedupExecuteMap, } from "../googleNativeGemini3/index.js";
18
21
  import { createStreamChannel } from "../../core/streamChannel.js";
19
22
  import { toNativeToolDeclarations } from "../../core/nativeToolFormat.js";
20
23
  import { createProxyFetch } from "../../proxy/proxyFetch.js";
@@ -649,8 +652,6 @@ export class GoogleAIStudioProvider extends BaseProvider {
649
652
  });
650
653
  // Convert tools
651
654
  let toolsConfig;
652
- let executeMap = new DedupExecuteMap();
653
- let originalNameMap = new Map();
654
655
  let declarationsResult;
655
656
  if (options.tools &&
656
657
  Object.keys(options.tools).length > 0 &&
@@ -658,8 +659,6 @@ export class GoogleAIStudioProvider extends BaseProvider {
658
659
  const result = toNativeToolDeclarations(options.tools, "functionDeclarations");
659
660
  declarationsResult = result;
660
661
  toolsConfig = result.toolsConfig;
661
- executeMap = result.executeMap;
662
- originalNameMap = result.originalNameMap;
663
662
  logger.debug("[GoogleAIStudio] Converted tools for native SDK", {
664
663
  toolCount: toolsConfig[0].functionDeclarations.length,
665
664
  toolNames: toolsConfig[0].functionDeclarations.map((t) => t.name),
@@ -720,8 +719,6 @@ export class GoogleAIStudioProvider extends BaseProvider {
720
719
  let totalCacheReadTokens = 0;
721
720
  let totalReasoningTokens = 0;
722
721
  let step = 0;
723
- let completedWithFinalAnswer = false;
724
- const failedTools = new Map();
725
722
  // Cheap trigger for the in-turn reclaim, mirroring the Vertex twin.
726
723
  // Planning serializes the WHOLE accumulated history to estimate it,
727
724
  // so running it unconditionally charges that once per step for the
@@ -731,98 +728,88 @@ export class GoogleAIStudioProvider extends BaseProvider {
731
728
  const contextGuard = createContextGuard(getContextWindowSize("googleAiStudio", modelName));
732
729
  try {
733
730
  // Agentic loop for tool calling
734
- while (step < maxSteps) {
735
- // In-turn context guard: this loop appends a model turn plus a
736
- // tool turn every step with nothing bounding growth. No-op
737
- // while the request still fits, so a loop that fits never pays
738
- // a cache invalidation. Step 0 still plans unconditionally —
739
- // the guard has no usage to go on yet, and the incoming history
740
- // can already be oversized before the first call.
741
- if (step === 0 || contextGuard.shouldStop()) {
742
- if (reclaimAiStudioContext(currentContents, modelName, contextGuard.projectedNextPromptTokens)) {
743
- contextGuard.resetAfterReclaim();
731
+ // The turn itself now runs on the shared engine: the step cap,
732
+ // tool dispatch, the failure breaker and usage accumulation are
733
+ // engine-owned. Everything below is the provider half building
734
+ // one request, and the per-step side effects the old loop
735
+ // performed inline.
736
+ const baseAdapter = createGeminiLoopAdapter({
737
+ providerLabel: "GoogleAIStudio",
738
+ maxSteps,
739
+ // Same threshold the hand-rolled dispatcher used, which is
740
+ // what makes an always-failing tool dispatch exactly twice.
741
+ toolFailureBreaker: { maxRetries: DEFAULT_TOOL_MAX_RETRIES },
742
+ liveTools: options.tools ?? {},
743
+ ...(declarationsResult
744
+ ? { declarations: declarationsResult }
745
+ : {}),
746
+ buildRequest: (contents) => ({
747
+ model: modelName,
748
+ contents,
749
+ config,
750
+ ...(composedSignal
751
+ ? { httpOptions: { signal: composedSignal } }
752
+ : {}),
753
+ }),
754
+ sendStep: async (request) => client.models.generateContentStream(request),
755
+ noteUsage: (inputTokens, outputTokens) => {
756
+ contextGuard.noteUsage(inputTokens, outputTokens);
757
+ },
758
+ // Pure: the engine assigns what this returns. The old loop
759
+ // reclaimed in place because it owned `currentContents`.
760
+ planReclaim: (contents, stepIndex) => {
761
+ if (stepIndex !== 0 && !contextGuard.shouldStop()) {
762
+ return undefined;
744
763
  }
745
- }
746
- if (composedSignal?.aborted) {
747
- throw composedSignal.reason instanceof Error
748
- ? composedSignal.reason
749
- : new Error("Request aborted");
750
- }
751
- step++;
752
- // Mid-turn discovery sync: advertise tools hydrated into the
753
- // live record by search_tools during the previous step.
754
- if (declarationsResult) {
755
- refreshNativeToolDeclarations(options.tools, declarationsResult);
756
- }
757
- logger.debug(`[GoogleAIStudio] Native SDK step ${step}/${maxSteps}`);
758
- try {
759
- const rawStream = await client.models.generateContentStream({
760
- model: modelName,
761
- contents: currentContents,
762
- config,
763
- ...(composedSignal
764
- ? { httpOptions: { signal: composedSignal } }
765
- : {}),
766
- });
767
- // For every step, use incremental collection so text parts
768
- // are pushed to the channel as they arrive. For intermediate
769
- // steps (those that produce function calls) we still need the
770
- // complete rawResponseParts for pushModelResponseToHistory,
771
- // which collectStreamChunksIncremental provides at stream end.
772
- const chunkResult = await collectStreamChunksIncremental(rawStream, channel);
773
- totalInputTokens += chunkResult.inputTokens;
774
- totalOutputTokens += chunkResult.outputTokens;
775
- totalCacheReadTokens += chunkResult.cacheReadTokens ?? 0;
776
- totalReasoningTokens += chunkResult.reasoningTokens ?? 0;
777
- // `inputTokens` is this step's promptTokenCount — the FULL
778
- // prompt size for the request just made, which is what the
779
- // guard projects the next request from.
780
- contextGuard.noteUsage(chunkResult.inputTokens, chunkResult.outputTokens);
781
- const stepText = extractTextFromParts(chunkResult.rawResponseParts);
782
- // If no function calls, this was the final step — channel
783
- // already received all text parts incrementally.
784
- if (chunkResult.stepFunctionCalls.length === 0) {
785
- completedWithFinalAnswer = true;
786
- break;
764
+ const working = [...contents];
765
+ if (!reclaimAiStudioContext(working, modelName, contextGuard.projectedNextPromptTokens)) {
766
+ return undefined;
787
767
  }
788
- lastStepText = stepText;
789
- // Record tool call events on the span
790
- for (const fc of chunkResult.stepFunctionCalls) {
768
+ contextGuard.resetAfterReclaim();
769
+ return working;
770
+ },
771
+ });
772
+ // Wrapped because these fire once PER STEP in the loop this
773
+ // replaces, and buildToolResultMessages is the only hook that
774
+ // runs per step with exactly that step's results. Reading them
775
+ // off the turn's final result would batch every step into one
776
+ // late write and lose the per-step thought signature.
777
+ const adapter = {
778
+ ...baseAdapter,
779
+ buildToolResultMessages: (contents, stepResult, toolResults) => {
780
+ step++;
781
+ for (const call of stepResult.toolCalls) {
791
782
  span.addEvent("gen_ai.tool_call", {
792
- "tool.name": fc.name,
783
+ "tool.name": call.name,
793
784
  "tool.step": step,
794
785
  });
795
786
  }
796
- logger.debug(`[GoogleAIStudio] Executing ${chunkResult.stepFunctionCalls.length} function calls`);
797
- // Add model response with ALL parts (including thoughtSignature) to history
798
- pushModelResponseToHistory(currentContents, chunkResult.rawResponseParts, chunkResult.stepFunctionCalls);
799
- const toolCallsBefore = allToolCalls.length;
800
- const toolExecsBefore = toolExecutions.length;
801
- const functionResponses = await executeNativeToolCalls("[GoogleAIStudio]", chunkResult.stepFunctionCalls, executeMap, failedTools, allToolCalls, {
802
- abortSignal: composedSignal,
803
- originalNameMap,
804
- toolExecutions,
805
- liveTools: options.tools,
806
- declarations: declarationsResult,
807
- });
808
- // Persist this step's tool calls/results into conversation
809
- // memory. Without this, tool_call / tool_result rows never
810
- // reach Redis and the chat-history UI loses every tool
811
- // invocation.
812
- const stepToolCalls = allToolCalls.slice(toolCallsBefore);
813
- const stepToolExecs = toolExecutions.slice(toolExecsBefore);
814
- if (stepToolCalls.length > 0 || stepToolExecs.length > 0) {
815
- const stepThoughtSig = extractThoughtSignature(chunkResult.rawResponseParts);
816
- withTimeout(this.handleToolExecutionStorage(stepToolCalls.map((tc, i) => ({
817
- toolName: tc.toolName,
818
- args: tc.args,
787
+ lastStepText = stepResult.text || lastStepText;
788
+ for (const call of stepResult.toolCalls) {
789
+ allToolCalls.push({
790
+ toolName: call.name,
791
+ args: call.args,
792
+ });
793
+ }
794
+ for (const result of toolResults) {
795
+ toolExecutions.push({
796
+ name: result.name,
797
+ input: result.args,
798
+ output: result.output,
799
+ });
800
+ }
801
+ if (toolResults.length > 0) {
802
+ const stepThoughtSig = extractThoughtSignature(stepResult.raw.rawResponseParts);
803
+ withTimeout(this.handleToolExecutionStorage(stepResult.toolCalls.map((call, i) => ({
804
+ toolName: call.name,
805
+ args: call.args,
819
806
  ...(i === 0 && stepThoughtSig
820
807
  ? { thoughtSignature: stepThoughtSig }
821
808
  : {}),
822
809
  stepIndex: step,
823
- })), stepToolExecs.map((te) => ({
824
- toolName: te.name,
825
- output: te.output,
810
+ })), toolResults.map((result) => ({
811
+ toolName: result.name,
812
+ output: result.output,
826
813
  stepIndex: step,
827
814
  })), options, new Date()), TOOL_STORAGE_TIMEOUT_MS, "tool storage write timed out").catch((error) => {
828
815
  logger.warn("[GoogleAIStudio] Failed to store native tool executions", {
@@ -832,28 +819,57 @@ export class GoogleAIStudioProvider extends BaseProvider {
832
819
  });
833
820
  });
834
821
  }
835
- // Add function responses to history — the @google/genai SDK
836
- // only accepts "user" and "model" as valid roles in contents.
837
- // Function/tool responses must use role: "user" (matching the
838
- // SDK's own automaticFunctionCalling implementation).
839
- currentContents.push({
840
- role: "user",
841
- parts: functionResponses,
842
- });
822
+ const next = baseAdapter.buildToolResultMessages(contents, stepResult, toolResults);
843
823
  // Project this step's growth: the appended tool results ride
844
824
  // the next prompt, which the provider has not reported on yet.
845
825
  try {
846
- contextGuard.noteAppendedChars(JSON.stringify(functionResponses).length);
826
+ const appended = next[next.length - 1];
827
+ contextGuard.noteAppendedChars(JSON.stringify(appended?.parts ?? []).length);
847
828
  }
848
829
  catch {
849
830
  /* estimation is best-effort — never break the loop */
850
831
  }
832
+ return next;
833
+ },
834
+ };
835
+ const engineTools = {};
836
+ for (const [name, tool] of Object.entries(options.tools ?? {})) {
837
+ const execute = tool?.execute;
838
+ if (!execute) {
839
+ continue;
851
840
  }
852
- catch (error) {
853
- logger.error("[GoogleAIStudio] Native SDK error", error);
854
- throw this.handleProviderError(error);
841
+ engineTools[name] = {
842
+ execute: async (args, opts) => execute(args, opts),
843
+ };
844
+ }
845
+ const { stream: engineStream, resultPromise } = runAgenticLoop(adapter, currentContents, {
846
+ tools: engineTools,
847
+ ...(composedSignal ? { abortSignal: composedSignal } : {}),
848
+ });
849
+ const pump = (async () => {
850
+ for await (const chunk of engineStream) {
851
+ channel.push(chunk);
855
852
  }
853
+ })();
854
+ let engineResult;
855
+ try {
856
+ engineResult = await resultPromise;
857
+ }
858
+ catch (error) {
859
+ await pump.catch(() => { });
860
+ logger.error("[GoogleAIStudio] Native SDK error", error);
861
+ throw this.handleProviderError(error);
856
862
  }
863
+ await pump;
864
+ totalInputTokens += engineResult.usage.inputTokens;
865
+ totalOutputTokens += engineResult.usage.outputTokens;
866
+ totalCacheReadTokens += engineResult.usage.cacheReadTokens ?? 0;
867
+ totalReasoningTokens += engineResult.usage.reasoningTokens ?? 0;
868
+ // The turn produced a final answer when the model stopped
869
+ // calling tools of its own accord, rather than being cut off at
870
+ // the cap.
871
+ const completedWithFinalAnswer = engineResult.toolCalls.length === 0 ||
872
+ engineResult.finishReason !== "tool-calls";
857
873
  // Handle max-steps termination: if the model was still calling
858
874
  // tools when we hit the limit, push a synthetic final message.
859
875
  const hitStepLimitWithoutFinalAnswer = step >= maxSteps && !completedWithFinalAnswer;
@@ -6,6 +6,9 @@ import { ATTR, tracers, withClientSpan, withClientStreamSpan, withSpan, } from "
6
6
  import { AuthenticationError, InvalidModelError, NetworkError, ProviderError, RateLimitError, } from "../../types/index.js";
7
7
  import { ERROR_CODES, NeuroLinkError } from "../../utils/errorHandling.js";
8
8
  import { logger } from "../../utils/logger.js";
9
+ import { createGeminiLoopAdapter } from "../../core/geminiLoopAdapter.js";
10
+ import { runAgenticLoop } from "../../core/loopEngine.js";
11
+ import { DEFAULT_TOOL_MAX_RETRIES } from "../../core/constants.js";
9
12
  import { isToolsSchemaExclusionInForce } from "../../core/modules/structuredOutputPolicy.js";
10
13
  import { GEMINI_ELISION_NOTE, planGeminiLoopReclaim, previewGeminiToolResponseText, } from "../../context/geminiLoopGuard.js";
11
14
  import { getAvailableInputTokens, getContextWindowSize, } from "../../constants/contextWindows.js";
@@ -14,7 +17,7 @@ import { withTimeout } from "../../utils/async/index.js";
14
17
  import { estimateTokens } from "../../utils/tokenEstimation.js";
15
18
  import { transformToolExecutions } from "../../utils/transformationUtils.js";
16
19
  import { resolveToolExecutionRecords } from "../../core/toolExecutionRecorder.js";
17
- import { buildGeminiResponseSchema, buildNativeConfig, collectStreamChunks, collectStreamChunksIncremental, computeMaxSteps, createContextGuard, buildUserPartsWithMultimodal, executeNativeToolCalls, extractTextFromParts, extractThoughtSignature, handleMaxStepsTermination, prependConversationMessages, pushModelResponseToHistory, refreshNativeToolDeclarations, DedupExecuteMap, } from "../googleNativeGemini3/index.js";
20
+ import { buildGeminiResponseSchema, buildNativeConfig, collectStreamChunks, computeMaxSteps, createContextGuard, buildUserPartsWithMultimodal, executeNativeToolCalls, extractTextFromParts, extractThoughtSignature, handleMaxStepsTermination, prependConversationMessages, pushModelResponseToHistory, refreshNativeToolDeclarations, DedupExecuteMap, } from "../googleNativeGemini3/index.js";
18
21
  import { createStreamChannel } from "../../core/streamChannel.js";
19
22
  import { toNativeToolDeclarations } from "../../core/nativeToolFormat.js";
20
23
  import { createProxyFetch } from "../../proxy/proxyFetch.js";
@@ -649,8 +652,6 @@ export class GoogleAIStudioProvider extends BaseProvider {
649
652
  });
650
653
  // Convert tools
651
654
  let toolsConfig;
652
- let executeMap = new DedupExecuteMap();
653
- let originalNameMap = new Map();
654
655
  let declarationsResult;
655
656
  if (options.tools &&
656
657
  Object.keys(options.tools).length > 0 &&
@@ -658,8 +659,6 @@ export class GoogleAIStudioProvider extends BaseProvider {
658
659
  const result = toNativeToolDeclarations(options.tools, "functionDeclarations");
659
660
  declarationsResult = result;
660
661
  toolsConfig = result.toolsConfig;
661
- executeMap = result.executeMap;
662
- originalNameMap = result.originalNameMap;
663
662
  logger.debug("[GoogleAIStudio] Converted tools for native SDK", {
664
663
  toolCount: toolsConfig[0].functionDeclarations.length,
665
664
  toolNames: toolsConfig[0].functionDeclarations.map((t) => t.name),
@@ -720,8 +719,6 @@ export class GoogleAIStudioProvider extends BaseProvider {
720
719
  let totalCacheReadTokens = 0;
721
720
  let totalReasoningTokens = 0;
722
721
  let step = 0;
723
- let completedWithFinalAnswer = false;
724
- const failedTools = new Map();
725
722
  // Cheap trigger for the in-turn reclaim, mirroring the Vertex twin.
726
723
  // Planning serializes the WHOLE accumulated history to estimate it,
727
724
  // so running it unconditionally charges that once per step for the
@@ -731,98 +728,88 @@ export class GoogleAIStudioProvider extends BaseProvider {
731
728
  const contextGuard = createContextGuard(getContextWindowSize("googleAiStudio", modelName));
732
729
  try {
733
730
  // Agentic loop for tool calling
734
- while (step < maxSteps) {
735
- // In-turn context guard: this loop appends a model turn plus a
736
- // tool turn every step with nothing bounding growth. No-op
737
- // while the request still fits, so a loop that fits never pays
738
- // a cache invalidation. Step 0 still plans unconditionally —
739
- // the guard has no usage to go on yet, and the incoming history
740
- // can already be oversized before the first call.
741
- if (step === 0 || contextGuard.shouldStop()) {
742
- if (reclaimAiStudioContext(currentContents, modelName, contextGuard.projectedNextPromptTokens)) {
743
- contextGuard.resetAfterReclaim();
731
+ // The turn itself now runs on the shared engine: the step cap,
732
+ // tool dispatch, the failure breaker and usage accumulation are
733
+ // engine-owned. Everything below is the provider half building
734
+ // one request, and the per-step side effects the old loop
735
+ // performed inline.
736
+ const baseAdapter = createGeminiLoopAdapter({
737
+ providerLabel: "GoogleAIStudio",
738
+ maxSteps,
739
+ // Same threshold the hand-rolled dispatcher used, which is
740
+ // what makes an always-failing tool dispatch exactly twice.
741
+ toolFailureBreaker: { maxRetries: DEFAULT_TOOL_MAX_RETRIES },
742
+ liveTools: options.tools ?? {},
743
+ ...(declarationsResult
744
+ ? { declarations: declarationsResult }
745
+ : {}),
746
+ buildRequest: (contents) => ({
747
+ model: modelName,
748
+ contents,
749
+ config,
750
+ ...(composedSignal
751
+ ? { httpOptions: { signal: composedSignal } }
752
+ : {}),
753
+ }),
754
+ sendStep: async (request) => client.models.generateContentStream(request),
755
+ noteUsage: (inputTokens, outputTokens) => {
756
+ contextGuard.noteUsage(inputTokens, outputTokens);
757
+ },
758
+ // Pure: the engine assigns what this returns. The old loop
759
+ // reclaimed in place because it owned `currentContents`.
760
+ planReclaim: (contents, stepIndex) => {
761
+ if (stepIndex !== 0 && !contextGuard.shouldStop()) {
762
+ return undefined;
744
763
  }
745
- }
746
- if (composedSignal?.aborted) {
747
- throw composedSignal.reason instanceof Error
748
- ? composedSignal.reason
749
- : new Error("Request aborted");
750
- }
751
- step++;
752
- // Mid-turn discovery sync: advertise tools hydrated into the
753
- // live record by search_tools during the previous step.
754
- if (declarationsResult) {
755
- refreshNativeToolDeclarations(options.tools, declarationsResult);
756
- }
757
- logger.debug(`[GoogleAIStudio] Native SDK step ${step}/${maxSteps}`);
758
- try {
759
- const rawStream = await client.models.generateContentStream({
760
- model: modelName,
761
- contents: currentContents,
762
- config,
763
- ...(composedSignal
764
- ? { httpOptions: { signal: composedSignal } }
765
- : {}),
766
- });
767
- // For every step, use incremental collection so text parts
768
- // are pushed to the channel as they arrive. For intermediate
769
- // steps (those that produce function calls) we still need the
770
- // complete rawResponseParts for pushModelResponseToHistory,
771
- // which collectStreamChunksIncremental provides at stream end.
772
- const chunkResult = await collectStreamChunksIncremental(rawStream, channel);
773
- totalInputTokens += chunkResult.inputTokens;
774
- totalOutputTokens += chunkResult.outputTokens;
775
- totalCacheReadTokens += chunkResult.cacheReadTokens ?? 0;
776
- totalReasoningTokens += chunkResult.reasoningTokens ?? 0;
777
- // `inputTokens` is this step's promptTokenCount — the FULL
778
- // prompt size for the request just made, which is what the
779
- // guard projects the next request from.
780
- contextGuard.noteUsage(chunkResult.inputTokens, chunkResult.outputTokens);
781
- const stepText = extractTextFromParts(chunkResult.rawResponseParts);
782
- // If no function calls, this was the final step — channel
783
- // already received all text parts incrementally.
784
- if (chunkResult.stepFunctionCalls.length === 0) {
785
- completedWithFinalAnswer = true;
786
- break;
764
+ const working = [...contents];
765
+ if (!reclaimAiStudioContext(working, modelName, contextGuard.projectedNextPromptTokens)) {
766
+ return undefined;
787
767
  }
788
- lastStepText = stepText;
789
- // Record tool call events on the span
790
- for (const fc of chunkResult.stepFunctionCalls) {
768
+ contextGuard.resetAfterReclaim();
769
+ return working;
770
+ },
771
+ });
772
+ // Wrapped because these fire once PER STEP in the loop this
773
+ // replaces, and buildToolResultMessages is the only hook that
774
+ // runs per step with exactly that step's results. Reading them
775
+ // off the turn's final result would batch every step into one
776
+ // late write and lose the per-step thought signature.
777
+ const adapter = {
778
+ ...baseAdapter,
779
+ buildToolResultMessages: (contents, stepResult, toolResults) => {
780
+ step++;
781
+ for (const call of stepResult.toolCalls) {
791
782
  span.addEvent("gen_ai.tool_call", {
792
- "tool.name": fc.name,
783
+ "tool.name": call.name,
793
784
  "tool.step": step,
794
785
  });
795
786
  }
796
- logger.debug(`[GoogleAIStudio] Executing ${chunkResult.stepFunctionCalls.length} function calls`);
797
- // Add model response with ALL parts (including thoughtSignature) to history
798
- pushModelResponseToHistory(currentContents, chunkResult.rawResponseParts, chunkResult.stepFunctionCalls);
799
- const toolCallsBefore = allToolCalls.length;
800
- const toolExecsBefore = toolExecutions.length;
801
- const functionResponses = await executeNativeToolCalls("[GoogleAIStudio]", chunkResult.stepFunctionCalls, executeMap, failedTools, allToolCalls, {
802
- abortSignal: composedSignal,
803
- originalNameMap,
804
- toolExecutions,
805
- liveTools: options.tools,
806
- declarations: declarationsResult,
807
- });
808
- // Persist this step's tool calls/results into conversation
809
- // memory. Without this, tool_call / tool_result rows never
810
- // reach Redis and the chat-history UI loses every tool
811
- // invocation.
812
- const stepToolCalls = allToolCalls.slice(toolCallsBefore);
813
- const stepToolExecs = toolExecutions.slice(toolExecsBefore);
814
- if (stepToolCalls.length > 0 || stepToolExecs.length > 0) {
815
- const stepThoughtSig = extractThoughtSignature(chunkResult.rawResponseParts);
816
- withTimeout(this.handleToolExecutionStorage(stepToolCalls.map((tc, i) => ({
817
- toolName: tc.toolName,
818
- args: tc.args,
787
+ lastStepText = stepResult.text || lastStepText;
788
+ for (const call of stepResult.toolCalls) {
789
+ allToolCalls.push({
790
+ toolName: call.name,
791
+ args: call.args,
792
+ });
793
+ }
794
+ for (const result of toolResults) {
795
+ toolExecutions.push({
796
+ name: result.name,
797
+ input: result.args,
798
+ output: result.output,
799
+ });
800
+ }
801
+ if (toolResults.length > 0) {
802
+ const stepThoughtSig = extractThoughtSignature(stepResult.raw.rawResponseParts);
803
+ withTimeout(this.handleToolExecutionStorage(stepResult.toolCalls.map((call, i) => ({
804
+ toolName: call.name,
805
+ args: call.args,
819
806
  ...(i === 0 && stepThoughtSig
820
807
  ? { thoughtSignature: stepThoughtSig }
821
808
  : {}),
822
809
  stepIndex: step,
823
- })), stepToolExecs.map((te) => ({
824
- toolName: te.name,
825
- output: te.output,
810
+ })), toolResults.map((result) => ({
811
+ toolName: result.name,
812
+ output: result.output,
826
813
  stepIndex: step,
827
814
  })), options, new Date()), TOOL_STORAGE_TIMEOUT_MS, "tool storage write timed out").catch((error) => {
828
815
  logger.warn("[GoogleAIStudio] Failed to store native tool executions", {
@@ -832,28 +819,57 @@ export class GoogleAIStudioProvider extends BaseProvider {
832
819
  });
833
820
  });
834
821
  }
835
- // Add function responses to history — the @google/genai SDK
836
- // only accepts "user" and "model" as valid roles in contents.
837
- // Function/tool responses must use role: "user" (matching the
838
- // SDK's own automaticFunctionCalling implementation).
839
- currentContents.push({
840
- role: "user",
841
- parts: functionResponses,
842
- });
822
+ const next = baseAdapter.buildToolResultMessages(contents, stepResult, toolResults);
843
823
  // Project this step's growth: the appended tool results ride
844
824
  // the next prompt, which the provider has not reported on yet.
845
825
  try {
846
- contextGuard.noteAppendedChars(JSON.stringify(functionResponses).length);
826
+ const appended = next[next.length - 1];
827
+ contextGuard.noteAppendedChars(JSON.stringify(appended?.parts ?? []).length);
847
828
  }
848
829
  catch {
849
830
  /* estimation is best-effort — never break the loop */
850
831
  }
832
+ return next;
833
+ },
834
+ };
835
+ const engineTools = {};
836
+ for (const [name, tool] of Object.entries(options.tools ?? {})) {
837
+ const execute = tool?.execute;
838
+ if (!execute) {
839
+ continue;
851
840
  }
852
- catch (error) {
853
- logger.error("[GoogleAIStudio] Native SDK error", error);
854
- throw this.handleProviderError(error);
841
+ engineTools[name] = {
842
+ execute: async (args, opts) => execute(args, opts),
843
+ };
844
+ }
845
+ const { stream: engineStream, resultPromise } = runAgenticLoop(adapter, currentContents, {
846
+ tools: engineTools,
847
+ ...(composedSignal ? { abortSignal: composedSignal } : {}),
848
+ });
849
+ const pump = (async () => {
850
+ for await (const chunk of engineStream) {
851
+ channel.push(chunk);
855
852
  }
853
+ })();
854
+ let engineResult;
855
+ try {
856
+ engineResult = await resultPromise;
857
+ }
858
+ catch (error) {
859
+ await pump.catch(() => { });
860
+ logger.error("[GoogleAIStudio] Native SDK error", error);
861
+ throw this.handleProviderError(error);
856
862
  }
863
+ await pump;
864
+ totalInputTokens += engineResult.usage.inputTokens;
865
+ totalOutputTokens += engineResult.usage.outputTokens;
866
+ totalCacheReadTokens += engineResult.usage.cacheReadTokens ?? 0;
867
+ totalReasoningTokens += engineResult.usage.reasoningTokens ?? 0;
868
+ // The turn produced a final answer when the model stopped
869
+ // calling tools of its own accord, rather than being cut off at
870
+ // the cap.
871
+ const completedWithFinalAnswer = engineResult.toolCalls.length === 0 ||
872
+ engineResult.finishReason !== "tool-calls";
857
873
  // Handle max-steps termination: if the model was still calling
858
874
  // tools when we hit the limit, push a synthetic final message.
859
875
  const hitStepLimitWithoutFinalAnswer = step >= maxSteps && !completedWithFinalAnswer;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "11.11.2",
3
+ "version": "11.11.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": {