@juspay/neurolink 9.65.0 → 9.65.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.
@@ -21,6 +21,7 @@ export declare const DEFAULT_TEMPERATURE = 0.7;
21
21
  export declare const DEFAULT_TIMEOUT = 60000;
22
22
  export declare const DEFAULT_MAX_STEPS = 200;
23
23
  export declare const DEFAULT_TOOL_MAX_RETRIES = 2;
24
+ export declare const TOOL_STORAGE_TIMEOUT_MS = 5000;
24
25
  export declare const STEP_LIMITS: {
25
26
  min: number;
26
27
  max: number;
@@ -107,6 +107,9 @@ export const DEFAULT_TEMPERATURE = 0.7;
107
107
  export const DEFAULT_TIMEOUT = 60000;
108
108
  export const DEFAULT_MAX_STEPS = 200;
109
109
  export const DEFAULT_TOOL_MAX_RETRIES = 2; // Maximum retries per tool before permanently failing
110
+ // Fire-and-forget tool storage writes (Redis). 5s is generous for a single
111
+ // Redis write; if breached, the .catch logs a warning.
112
+ export const TOOL_STORAGE_TIMEOUT_MS = 5000;
110
113
  // Step execution limits
111
114
  export const STEP_LIMITS = {
112
115
  min: 1,
@@ -951,12 +951,6 @@ User message: "${userMessage}"`;
951
951
  provider: this.config.summarizationProvider || "vertex",
952
952
  model: this.config.summarizationModel || "gemini-2.5-flash",
953
953
  disableTools: true, // Title generation doesn't need tools — saves ~600 tokens of tool descriptions
954
- // A 20-25 letter title is well under 64 tokens; capping prevents
955
- // Vertex 400 INVALID_ARGUMENT — Gemini 2.5 Flash on the native
956
- // @google/genai SDK rejects requests with no maxOutputTokens cap
957
- // when the prompt is short, which silently broke every
958
- // `conversationMemory.enabled + context` test path.
959
- maxTokens: 64,
960
954
  });
961
955
  // Clean up the generated title
962
956
  let title = result.content?.trim() || "New Conversation";
@@ -547,9 +547,16 @@ export const directAgentTools = {
547
547
  // It is only included in directAgentTools when NEUROLINK_ENABLE_BASH_TOOL=true or
548
548
  // toolConfig.enableBashTool is explicitly set to true. See shouldEnableBashTool() in toolUtils.ts.
549
549
  websearchGrounding: tool({
550
- description: "Search the web for current information using Google Search grounding. Returns raw search data for AI processing.",
550
+ description: "Performs a Google Search and returns a summarized answer with source citations. Always check the current date before constructing the query. Use whenever the answer depends on time-sensitive facts or requires verification against real-world sources.",
551
551
  inputSchema: z.object({
552
- query: z.string().describe("Search query to find information about"),
552
+ query: z
553
+ .string()
554
+ .trim()
555
+ .min(1, { message: "must be a non-empty search string" })
556
+ .refine((v) => v.toLowerCase() !== "undefined", {
557
+ message: 'must not be the literal string "undefined" — pass a real search query',
558
+ })
559
+ .describe("The search query string to look up on the web."),
553
560
  maxResults: z
554
561
  .number()
555
562
  .optional()
@@ -581,7 +588,8 @@ export const directAgentTools = {
581
588
  project: hasProjectId,
582
589
  location: projectLocation,
583
590
  });
584
- const websearchModel = "gemini-2.5-flash-lite";
591
+ const websearchModel = process.env.NEUROLINK_WEBSEARCH_MODEL?.trim() ||
592
+ "gemini-2.5-flash-lite";
585
593
  const model = vertex_ai.getGenerativeModel({
586
594
  model: websearchModel,
587
595
  tools: createGoogleSearchTools(),
@@ -21,6 +21,7 @@ export declare const DEFAULT_TEMPERATURE = 0.7;
21
21
  export declare const DEFAULT_TIMEOUT = 60000;
22
22
  export declare const DEFAULT_MAX_STEPS = 200;
23
23
  export declare const DEFAULT_TOOL_MAX_RETRIES = 2;
24
+ export declare const TOOL_STORAGE_TIMEOUT_MS = 5000;
24
25
  export declare const STEP_LIMITS: {
25
26
  min: number;
26
27
  max: number;
@@ -107,6 +107,9 @@ export const DEFAULT_TEMPERATURE = 0.7;
107
107
  export const DEFAULT_TIMEOUT = 60000;
108
108
  export const DEFAULT_MAX_STEPS = 200;
109
109
  export const DEFAULT_TOOL_MAX_RETRIES = 2; // Maximum retries per tool before permanently failing
110
+ // Fire-and-forget tool storage writes (Redis). 5s is generous for a single
111
+ // Redis write; if breached, the .catch logs a warning.
112
+ export const TOOL_STORAGE_TIMEOUT_MS = 5000;
110
113
  // Step execution limits
111
114
  export const STEP_LIMITS = {
112
115
  min: 1,
@@ -951,12 +951,6 @@ User message: "${userMessage}"`;
951
951
  provider: this.config.summarizationProvider || "vertex",
952
952
  model: this.config.summarizationModel || "gemini-2.5-flash",
953
953
  disableTools: true, // Title generation doesn't need tools — saves ~600 tokens of tool descriptions
954
- // A 20-25 letter title is well under 64 tokens; capping prevents
955
- // Vertex 400 INVALID_ARGUMENT — Gemini 2.5 Flash on the native
956
- // @google/genai SDK rejects requests with no maxOutputTokens cap
957
- // when the prompt is short, which silently broke every
958
- // `conversationMemory.enabled + context` test path.
959
- maxTokens: 64,
960
954
  });
961
955
  // Clean up the generated title
962
956
  let title = result.content?.trim() || "New Conversation";
@@ -1,15 +1,17 @@
1
1
  import {} from "ai";
2
2
  import { ErrorCategory, ErrorSeverity, GoogleAIModels, } from "../constants/enums.js";
3
3
  import { BaseProvider } from "../core/baseProvider.js";
4
- import { IMAGE_GENERATION_MODELS } from "../core/constants.js";
4
+ import { IMAGE_GENERATION_MODELS, TOOL_STORAGE_TIMEOUT_MS, } from "../core/constants.js";
5
5
  import { processUnifiedFilesArray } from "../utils/messageBuilder.js";
6
6
  import { ATTR, tracers, withClientSpan, withClientStreamSpan, withSpan, } from "../telemetry/index.js";
7
7
  import { AuthenticationError, InvalidModelError, NetworkError, ProviderError, RateLimitError, } from "../types/index.js";
8
8
  import { ERROR_CODES, NeuroLinkError } from "../utils/errorHandling.js";
9
9
  import { logger } from "../utils/logger.js";
10
10
  import { composeAbortSignals, createTimeoutController, TimeoutError, } from "../utils/timeout.js";
11
+ import { withTimeout } from "../utils/async/index.js";
11
12
  import { estimateTokens } from "../utils/tokenEstimation.js";
12
- import { buildGeminiResponseSchema, buildNativeConfig, buildNativeToolDeclarations, collectStreamChunks, collectStreamChunksIncremental, computeMaxSteps, createTextChannel, buildUserPartsWithMultimodal, executeNativeToolCalls, extractTextFromParts, handleMaxStepsTermination, prependConversationMessages, pushModelResponseToHistory, } from "./googleNativeGemini3.js";
13
+ import { transformToolExecutions } from "../utils/transformationUtils.js";
14
+ import { buildGeminiResponseSchema, buildNativeConfig, buildNativeToolDeclarations, collectStreamChunks, collectStreamChunksIncremental, computeMaxSteps, createTextChannel, buildUserPartsWithMultimodal, executeNativeToolCalls, extractTextFromParts, extractThoughtSignature, handleMaxStepsTermination, prependConversationMessages, pushModelResponseToHistory, } from "./googleNativeGemini3.js";
13
15
  import { createProxyFetch } from "../proxy/proxyFetch.js";
14
16
  // Google AI Live API types now imported from ../types/providerSpecific.js
15
17
  // Import proper types for multimodal message handling
@@ -557,6 +559,10 @@ export class GoogleAIStudioProvider extends BaseProvider {
557
559
  const channel = createTextChannel();
558
560
  // Shared mutable state updated by the background agentic loop.
559
561
  const allToolCalls = [];
562
+ // Mirror the Vertex Gemini stream path: track tool executions so
563
+ // the storage hook can persist real outputs and StreamResult can
564
+ // surface toolsUsed/toolExecutions for tool-bearing turns.
565
+ const toolExecutions = [];
560
566
  // analyticsResolvers lets the background loop settle the analytics
561
567
  // promise once token counts are known (after the loop completes).
562
568
  let analyticsResolve;
@@ -627,7 +633,40 @@ export class GoogleAIStudioProvider extends BaseProvider {
627
633
  logger.debug(`[GoogleAIStudio] Executing ${chunkResult.stepFunctionCalls.length} function calls`);
628
634
  // Add model response with ALL parts (including thoughtSignature) to history
629
635
  pushModelResponseToHistory(currentContents, chunkResult.rawResponseParts, chunkResult.stepFunctionCalls);
630
- const functionResponses = await executeNativeToolCalls("[GoogleAIStudio]", chunkResult.stepFunctionCalls, executeMap, failedTools, allToolCalls, { abortSignal: composedSignal, originalNameMap });
636
+ const toolCallsBefore = allToolCalls.length;
637
+ const toolExecsBefore = toolExecutions.length;
638
+ const functionResponses = await executeNativeToolCalls("[GoogleAIStudio]", chunkResult.stepFunctionCalls, executeMap, failedTools, allToolCalls, {
639
+ abortSignal: composedSignal,
640
+ originalNameMap,
641
+ toolExecutions,
642
+ });
643
+ // Persist this step's tool calls/results into conversation
644
+ // memory. Without this, tool_call / tool_result rows never
645
+ // reach Redis and the chat-history UI loses every tool
646
+ // invocation.
647
+ const stepToolCalls = allToolCalls.slice(toolCallsBefore);
648
+ const stepToolExecs = toolExecutions.slice(toolExecsBefore);
649
+ if (stepToolCalls.length > 0 || stepToolExecs.length > 0) {
650
+ const stepThoughtSig = extractThoughtSignature(chunkResult.rawResponseParts);
651
+ withTimeout(this.handleToolExecutionStorage(stepToolCalls.map((tc, i) => ({
652
+ toolName: tc.toolName,
653
+ args: tc.args,
654
+ ...(i === 0 && stepThoughtSig
655
+ ? { thoughtSignature: stepThoughtSig }
656
+ : {}),
657
+ stepIndex: step,
658
+ })), stepToolExecs.map((te) => ({
659
+ toolName: te.name,
660
+ output: te.output,
661
+ stepIndex: step,
662
+ })), options, new Date()), TOOL_STORAGE_TIMEOUT_MS, "tool storage write timed out").catch((error) => {
663
+ logger.warn("[GoogleAIStudio] Failed to store native tool executions", {
664
+ error: error instanceof Error
665
+ ? error.message
666
+ : String(error),
667
+ });
668
+ });
669
+ }
631
670
  // Add function responses to history — the @google/genai SDK
632
671
  // only accepts "user" and "model" as valid roles in contents.
633
672
  // Function/tool responses must use role: "user" (matching the
@@ -684,7 +723,7 @@ export class GoogleAIStudioProvider extends BaseProvider {
684
723
  // Suppress unhandled-rejection warnings on loopPromise — errors are
685
724
  // forwarded to the channel and will surface when the caller iterates.
686
725
  loopPromise.catch(() => undefined);
687
- return {
726
+ const result = {
688
727
  stream: channel.iterable,
689
728
  provider: this.providerName,
690
729
  model: modelName,
@@ -692,6 +731,20 @@ export class GoogleAIStudioProvider extends BaseProvider {
692
731
  analytics: analyticsPromise,
693
732
  metadata,
694
733
  };
734
+ // Surface tools-used + executions via getters so they resolve at
735
+ // access time, after the background loop has populated the live
736
+ // arrays. Same lazy pattern used for `structuredOutput` elsewhere.
737
+ Object.defineProperty(result, "toolsUsed", {
738
+ enumerable: true,
739
+ configurable: true,
740
+ get: () => allToolCalls.map((tc) => tc.toolName),
741
+ });
742
+ Object.defineProperty(result, "toolExecutions", {
743
+ enumerable: true,
744
+ configurable: true,
745
+ get: () => transformToolExecutions(toolExecutions),
746
+ });
747
+ return result;
695
748
  }
696
749
  finally {
697
750
  // Timeout controller cleanup is managed inside the background loop
@@ -822,11 +875,35 @@ export class GoogleAIStudioProvider extends BaseProvider {
822
875
  // Add model response with ALL parts (including thoughtSignature) to history
823
876
  // This is critical for Gemini 3 - it requires thought signatures in subsequent turns
824
877
  pushModelResponseToHistory(currentContents, chunkResult.rawResponseParts, chunkResult.stepFunctionCalls);
878
+ const toolCallsBefore = allToolCalls.length;
879
+ const toolExecsBefore = toolExecutions.length;
825
880
  const functionResponses = await executeNativeToolCalls("[GoogleAIStudio]", chunkResult.stepFunctionCalls, executeMap, failedTools, allToolCalls, {
826
881
  toolExecutions,
827
882
  abortSignal: composedSignal,
828
883
  originalNameMap,
829
884
  });
885
+ // Persist this step's tool calls/results into conversation memory.
886
+ const stepToolCalls = allToolCalls.slice(toolCallsBefore);
887
+ const stepToolExecs = toolExecutions.slice(toolExecsBefore);
888
+ if (stepToolCalls.length > 0 || stepToolExecs.length > 0) {
889
+ const stepThoughtSig = extractThoughtSignature(chunkResult.rawResponseParts);
890
+ withTimeout(this.handleToolExecutionStorage(stepToolCalls.map((tc, i) => ({
891
+ toolName: tc.toolName,
892
+ args: tc.args,
893
+ ...(i === 0 && stepThoughtSig
894
+ ? { thoughtSignature: stepThoughtSig }
895
+ : {}),
896
+ stepIndex: step,
897
+ })), stepToolExecs.map((te) => ({
898
+ toolName: te.name,
899
+ output: te.output,
900
+ stepIndex: step,
901
+ })), options, new Date()), TOOL_STORAGE_TIMEOUT_MS, "tool storage write timed out").catch((error) => {
902
+ logger.warn("[GoogleAIStudio] Failed to store native generate tool executions", {
903
+ error: error instanceof Error ? error.message : String(error),
904
+ });
905
+ });
906
+ }
830
907
  // Add function responses to history — the @google/genai SDK
831
908
  // only accepts "user" and "model" as valid roles in contents.
832
909
  // Function/tool responses must use role: "user" (matching the
@@ -862,7 +939,7 @@ export class GoogleAIStudioProvider extends BaseProvider {
862
939
  },
863
940
  responseTime,
864
941
  toolsUsed: allToolCalls.map((tc) => tc.toolName),
865
- toolExecutions: toolExecutions,
942
+ toolExecutions: transformToolExecutions(toolExecutions),
866
943
  enhancedWithTools: allToolCalls.length > 0,
867
944
  };
868
945
  return this.enhanceResult(baseResult, options, startTime);
@@ -9,7 +9,7 @@
9
9
  * providers so they can share a single implementation.
10
10
  */
11
11
  import { type Tool } from "ai";
12
- import type { ThinkingConfig, CollectedChunkResult, NativeFunctionCall, NativeFunctionResponse, NativeToolDeclarationsResult, NativeToolsConfig, TextChannel, VertexNativePart, GeminiMultimodalInput } from "../types/index.js";
12
+ import type { ThinkingConfig, ChatMessage, CollectedChunkResult, MinimalChatMessage, NativeFunctionCall, NativeFunctionResponse, NativeToolDeclarationsResult, NativeToolsConfig, TextChannel, VertexNativePart, GeminiMultimodalInput } from "../types/index.js";
13
13
  export declare function sanitizeForGoogleFunctionName(name: string): string;
14
14
  /**
15
15
  * Resolve a sanitized Gemini tool name to one that is both unique within
@@ -218,10 +218,7 @@ export declare function buildGeminiResponseSchema(schema: unknown): Record<strin
218
218
  export declare function prependConversationMessages(contents: Array<{
219
219
  role: string;
220
220
  parts: unknown[];
221
- }>, conversationMessages?: Array<{
222
- role: string;
223
- content: string;
224
- }>): void;
221
+ }>, conversationMessages?: Array<ChatMessage | MinimalChatMessage>): void;
225
222
  /**
226
223
  * Build the `parts` array for the current user turn of a Gemini native
227
224
  * `generateContent` request, including inline image + PDF blobs.
@@ -646,6 +646,10 @@ export async function executeNativeToolCalls(logLabel, stepFunctionCalls, execut
646
646
  // original name. Falls back to the safe name if the map is missing or
647
647
  // doesn't contain the call (e.g. tool added mid-conversation).
648
648
  const externalName = (safeName) => options?.originalNameMap?.get(safeName) ?? safeName;
649
+ // Note: tool:start / tool:end events are emitted by ToolsManager's
650
+ // `execute` wrapper (see src/lib/core/modules/ToolsManager.ts:355 and :790)
651
+ // around every tool's execute function. The native paths invoke that same
652
+ // wrapped execute via the executeMap, so emitting here would duplicate.
649
653
  for (const call of stepFunctionCalls) {
650
654
  const exposedName = externalName(call.name);
651
655
  allToolCalls.push({ toolName: exposedName, args: call.args });
@@ -808,22 +812,113 @@ export function buildGeminiResponseSchema(schema) {
808
812
  * - The current user input should be appended AFTER calling this helper
809
813
  * so the prior turns appear first in chronological order.
810
814
  */
811
- export function prependConversationMessages(contents, conversationMessages) {
815
+ export function prependConversationMessages(contents,
816
+ // Accept either the full ChatMessage shape (when callers pass real Redis-
817
+ // backed history) or the reduced MinimalChatMessage shape (tests / synthetic
818
+ // callers). Only role, content, tool, args, and metadata.* are read here.
819
+ conversationMessages) {
812
820
  if (!conversationMessages || conversationMessages.length === 0) {
813
821
  return;
814
822
  }
823
+ // Walk prior turns building ordered segments. Tool_call / tool_result rows
824
+ // get grouped by (turnCounter, stepIndex) so parallel calls within a step
825
+ // stay together and don't bleed across turn boundaries. Regular user/
826
+ // assistant messages act as those boundaries.
827
+ //
828
+ // Without this reconstruction, a text-only mapper would strip tool rows
829
+ // from history — leaving the model unaware of any tools it called in
830
+ // prior turns. The grouped emit (model with functionCall parts → user
831
+ // with functionResponse parts) is what @google/genai's own
832
+ // automaticFunctionCalling produces, so the SDK validates it as a
833
+ // well-formed multi-turn conversation.
834
+ const stepMap = new Map();
835
+ const segments = [];
836
+ let turnCounter = 0;
837
+ const makeKey = (stepIndex) => `${turnCounter}:${stepIndex ?? "undefined"}`;
838
+ const getOrCreateStep = (stepIndex) => {
839
+ const key = makeKey(stepIndex);
840
+ const existing = stepMap.get(key);
841
+ if (existing) {
842
+ return existing;
843
+ }
844
+ const step = {
845
+ type: "tool_step",
846
+ callParts: [],
847
+ resultParts: [],
848
+ };
849
+ stepMap.set(key, step);
850
+ segments.push(step);
851
+ return step;
852
+ };
815
853
  for (const msg of conversationMessages) {
816
- if (msg.role !== "user" && msg.role !== "assistant") {
854
+ if (msg.role === "tool_call") {
855
+ const step = getOrCreateStep(msg.metadata?.stepIndex);
856
+ const fcPart = {
857
+ functionCall: {
858
+ name: msg.tool || "unknown",
859
+ args: msg.args || {},
860
+ },
861
+ };
862
+ if (msg.metadata?.thoughtSignature) {
863
+ fcPart.thoughtSignature = msg.metadata.thoughtSignature;
864
+ }
865
+ step.callParts.push(fcPart);
817
866
  continue;
818
867
  }
819
- const text = typeof msg.content === "string" ? msg.content : "";
820
- if (text.length === 0) {
868
+ if (msg.role === "tool_result") {
869
+ const step = getOrCreateStep(msg.metadata?.stepIndex);
870
+ let responsePayload;
871
+ try {
872
+ responsePayload =
873
+ msg.content !== undefined && msg.content !== null
874
+ ? { result: JSON.parse(msg.content) }
875
+ : { result: "success" };
876
+ }
877
+ catch {
878
+ responsePayload = { result: msg.content ?? "success" };
879
+ }
880
+ step.resultParts.push({
881
+ functionResponse: {
882
+ name: msg.tool || "unknown",
883
+ response: responsePayload,
884
+ },
885
+ });
821
886
  continue;
822
887
  }
823
- contents.push({
824
- role: msg.role === "assistant" ? "model" : "user",
825
- parts: [{ text }],
826
- });
888
+ // Regular (user / assistant) message — acts as a turn boundary.
889
+ const role = msg.role === "assistant" ? "model" : msg.role;
890
+ if (role !== "user" && role !== "model") {
891
+ continue;
892
+ }
893
+ if (!msg.content || msg.content.trim().length === 0) {
894
+ continue;
895
+ }
896
+ // Increment turn counter BEFORE pushing the segment so any tool_calls
897
+ // that follow this message get a fresh (turnCounter, stepIndex) namespace.
898
+ turnCounter++;
899
+ const textPart = { text: msg.content };
900
+ if (msg.metadata?.thoughtSignature) {
901
+ textPart.thoughtSignature = msg.metadata.thoughtSignature;
902
+ }
903
+ segments.push({ type: "regular", role, parts: [textPart] });
904
+ }
905
+ // Emit in order: each ToolStep → model turn (calls) + user turn (results)
906
+ // — same ordering @google/genai's automaticFunctionCalling produces.
907
+ for (const seg of segments) {
908
+ if (seg.type === "regular") {
909
+ contents.push({ role: seg.role, parts: seg.parts });
910
+ continue;
911
+ }
912
+ if (seg.callParts.length === 0) {
913
+ if (seg.resultParts.length > 0) {
914
+ logger.debug("[GoogleNativeGemini3] Dropping orphan tool_result segment with no matching tool_call rows", { resultCount: seg.resultParts.length });
915
+ }
916
+ continue;
917
+ }
918
+ contents.push({ role: "model", parts: seg.callParts });
919
+ if (seg.resultParts.length > 0) {
920
+ contents.push({ role: "user", parts: seg.resultParts });
921
+ }
827
922
  }
828
923
  }
829
924
  /**