@juspay/neurolink 9.19.1 → 9.20.0

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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## [9.20.0](https://github.com/juspay/neurolink/compare/v9.19.1...v9.20.0) (2026-03-09)
2
+
3
+ ### Features
4
+
5
+ - **(landing):** redesign nervous system visualization with performance and mobile fixes ([a4c7e91](https://github.com/juspay/neurolink/commit/a4c7e91a22622b41821db09e17885a54b26c66aa))
6
+
1
7
  ## [9.19.1](https://github.com/juspay/neurolink/compare/v9.19.0...v9.19.1) (2026-03-07)
2
8
 
3
9
  ### Bug Fixes
@@ -11,10 +11,19 @@
11
11
  * - Small conversation handling (BUG-005): for <= 4 messages, truncates
12
12
  * message content proportionally instead of returning no-op.
13
13
  */
14
- import { randomUUID } from "crypto";
15
14
  import { estimateTokens, estimateMessagesTokens, truncateToTokenBudget, } from "../../utils/tokenEstimation.js";
16
15
  import { logger } from "../../utils/logger.js";
16
+ import { randomUUID } from "crypto";
17
17
  const TRUNCATION_MARKER_CONTENT = "[Earlier conversation history was truncated to fit within context limits]";
18
+ function validateRoleAlternation(messages) {
19
+ for (let i = 1; i < messages.length; i++) {
20
+ if (messages[i].role === messages[i - 1].role &&
21
+ messages[i].role !== "system") {
22
+ logger.warn(`[SlidingWindowTruncator] Role alternation broken at index ${i}: consecutive "${messages[i].role}" messages`);
23
+ break;
24
+ }
25
+ }
26
+ }
18
27
  /**
19
28
  * For conversations with <= 4 messages that exceed token budget,
20
29
  * truncate the CONTENT of the longest messages rather than removing messages.
@@ -122,18 +131,19 @@ export function truncateWithSlidingWindow(messages, config) {
122
131
  break;
123
132
  }
124
133
  const keptAfterTruncation = remainingMessages.slice(evenRemoveCount);
125
- const truncationMarker = {
126
- id: `truncation-${randomUUID()}`,
127
- role: "user",
134
+ // Insert a dedicated system-role truncation marker with machine-readable
135
+ // metadata so effectiveHistory.ts can detect it via isTruncationMarker /
136
+ // truncationId and removeTruncationTags can rewind it.
137
+ const truncId = randomUUID();
138
+ const marker = {
139
+ id: `truncation-marker-${truncId}`,
140
+ role: "system",
128
141
  content: TRUNCATION_MARKER_CONTENT,
129
- timestamp: new Date().toISOString(),
130
- metadata: { isSummary: false, truncated: true },
142
+ isTruncationMarker: true,
143
+ truncationId: truncId,
131
144
  };
132
- const candidateMessages = [
133
- ...firstPair,
134
- truncationMarker,
135
- ...keptAfterTruncation,
136
- ];
145
+ const candidateMessages = [...firstPair, marker, ...keptAfterTruncation];
146
+ validateRoleAlternation(candidateMessages);
137
147
  // If we have token targets, verify the result fits
138
148
  if (config?.targetTokens) {
139
149
  const candidateTokens = estimateMessagesTokens(candidateMessages, config.provider);
@@ -160,16 +170,20 @@ export function truncateWithSlidingWindow(messages, config) {
160
170
  const evenMaxRemove = maxRemove - (maxRemove % 2);
161
171
  if (evenMaxRemove > 0) {
162
172
  const keptMessages = remainingMessages.slice(evenMaxRemove);
163
- const truncationMarker = {
164
- id: `truncation-${randomUUID()}`,
165
- role: "user",
173
+ // Insert a dedicated system-role truncation marker (see iterative block above)
174
+ const fallbackTruncId = randomUUID();
175
+ const fallbackMarker = {
176
+ id: `truncation-marker-${fallbackTruncId}`,
177
+ role: "system",
166
178
  content: TRUNCATION_MARKER_CONTENT,
167
- timestamp: new Date().toISOString(),
168
- metadata: { isSummary: false, truncated: true },
179
+ isTruncationMarker: true,
180
+ truncationId: fallbackTruncId,
169
181
  };
182
+ const fallbackMessages = [...firstPair, fallbackMarker, ...keptMessages];
183
+ validateRoleAlternation(fallbackMessages);
170
184
  return {
171
185
  truncated: true,
172
- messages: [...firstPair, truncationMarker, ...keptMessages],
186
+ messages: fallbackMessages,
173
187
  messagesRemoved: evenMaxRemove,
174
188
  };
175
189
  }
@@ -11,10 +11,19 @@
11
11
  * - Small conversation handling (BUG-005): for <= 4 messages, truncates
12
12
  * message content proportionally instead of returning no-op.
13
13
  */
14
- import { randomUUID } from "crypto";
15
14
  import { estimateTokens, estimateMessagesTokens, truncateToTokenBudget, } from "../../utils/tokenEstimation.js";
16
15
  import { logger } from "../../utils/logger.js";
16
+ import { randomUUID } from "crypto";
17
17
  const TRUNCATION_MARKER_CONTENT = "[Earlier conversation history was truncated to fit within context limits]";
18
+ function validateRoleAlternation(messages) {
19
+ for (let i = 1; i < messages.length; i++) {
20
+ if (messages[i].role === messages[i - 1].role &&
21
+ messages[i].role !== "system") {
22
+ logger.warn(`[SlidingWindowTruncator] Role alternation broken at index ${i}: consecutive "${messages[i].role}" messages`);
23
+ break;
24
+ }
25
+ }
26
+ }
18
27
  /**
19
28
  * For conversations with <= 4 messages that exceed token budget,
20
29
  * truncate the CONTENT of the longest messages rather than removing messages.
@@ -122,18 +131,19 @@ export function truncateWithSlidingWindow(messages, config) {
122
131
  break;
123
132
  }
124
133
  const keptAfterTruncation = remainingMessages.slice(evenRemoveCount);
125
- const truncationMarker = {
126
- id: `truncation-${randomUUID()}`,
127
- role: "user",
134
+ // Insert a dedicated system-role truncation marker with machine-readable
135
+ // metadata so effectiveHistory.ts can detect it via isTruncationMarker /
136
+ // truncationId and removeTruncationTags can rewind it.
137
+ const truncId = randomUUID();
138
+ const marker = {
139
+ id: `truncation-marker-${truncId}`,
140
+ role: "system",
128
141
  content: TRUNCATION_MARKER_CONTENT,
129
- timestamp: new Date().toISOString(),
130
- metadata: { isSummary: false, truncated: true },
142
+ isTruncationMarker: true,
143
+ truncationId: truncId,
131
144
  };
132
- const candidateMessages = [
133
- ...firstPair,
134
- truncationMarker,
135
- ...keptAfterTruncation,
136
- ];
145
+ const candidateMessages = [...firstPair, marker, ...keptAfterTruncation];
146
+ validateRoleAlternation(candidateMessages);
137
147
  // If we have token targets, verify the result fits
138
148
  if (config?.targetTokens) {
139
149
  const candidateTokens = estimateMessagesTokens(candidateMessages, config.provider);
@@ -160,16 +170,20 @@ export function truncateWithSlidingWindow(messages, config) {
160
170
  const evenMaxRemove = maxRemove - (maxRemove % 2);
161
171
  if (evenMaxRemove > 0) {
162
172
  const keptMessages = remainingMessages.slice(evenMaxRemove);
163
- const truncationMarker = {
164
- id: `truncation-${randomUUID()}`,
165
- role: "user",
173
+ // Insert a dedicated system-role truncation marker (see iterative block above)
174
+ const fallbackTruncId = randomUUID();
175
+ const fallbackMarker = {
176
+ id: `truncation-marker-${fallbackTruncId}`,
177
+ role: "system",
166
178
  content: TRUNCATION_MARKER_CONTENT,
167
- timestamp: new Date().toISOString(),
168
- metadata: { isSummary: false, truncated: true },
179
+ isTruncationMarker: true,
180
+ truncationId: fallbackTruncId,
169
181
  };
182
+ const fallbackMessages = [...firstPair, fallbackMarker, ...keptMessages];
183
+ validateRoleAlternation(fallbackMessages);
170
184
  return {
171
185
  truncated: true,
172
- messages: [...firstPair, truncationMarker, ...keptMessages],
186
+ messages: fallbackMessages,
173
187
  messagesRemoved: evenMaxRemove,
174
188
  };
175
189
  }
@@ -59,7 +59,7 @@ export declare class GoogleAIStudioProvider extends BaseProvider {
59
59
  * Estimate token count from text using centralized estimation with provider multipliers
60
60
  */
61
61
  private estimateTokenCount;
62
- protected executeStream(options: StreamOptions, _analysisSchema?: ZodUnknownSchema | Schema<unknown>): Promise<StreamResult>;
62
+ protected executeStream(options: StreamOptions, analysisSchema?: ZodUnknownSchema | Schema<unknown>): Promise<StreamResult>;
63
63
  /**
64
64
  * Execute stream using native @google/genai SDK for Gemini 3 models
65
65
  * This bypasses @ai-sdk/google to properly handle thought_signature
@@ -385,12 +385,15 @@ export class GoogleAIStudioProvider extends BaseProvider {
385
385
  return estimateTokens(text, "google-ai");
386
386
  }
387
387
  // executeGenerate removed - BaseProvider handles all generation with tools
388
- async executeStream(options, _analysisSchema) {
388
+ async executeStream(options, analysisSchema) {
389
389
  // Check if this is a Gemini 3 model with tools - use native SDK for thought_signature
390
390
  const gemini3CheckModelName = options.model || this.modelName;
391
+ // Structured output (analysisSchema, JSON format, or schema) is incompatible with tools on Gemini.
392
+ // Compute once and reuse in both the native Gemini 3 gate and the streamText fallback path.
393
+ const wantsStructuredOutput = analysisSchema || options.output?.format === "json" || options.schema;
391
394
  // Check for tools from options AND from SDK (MCP tools)
392
395
  // Need to check early if we should route to native SDK
393
- const gemini3CheckShouldUseTools = !options.disableTools && this.supportsTools();
396
+ const gemini3CheckShouldUseTools = !options.disableTools && this.supportsTools() && !wantsStructuredOutput;
394
397
  const optionTools = options.tools || {};
395
398
  const sdkTools = gemini3CheckShouldUseTools ? await this.getAllTools() : {};
396
399
  const combinedToolCount = Object.keys(optionTools).length + Object.keys(sdkTools).length;
@@ -442,7 +445,13 @@ export class GoogleAIStudioProvider extends BaseProvider {
442
445
  const timeoutController = createTimeoutController(timeout, this.providerName, "stream");
443
446
  try {
444
447
  // Get tools consistently with generate method (include user-provided RAG tools)
445
- const shouldUseTools = !options.disableTools && this.supportsTools();
448
+ // wantsStructuredOutput already computed before the Gemini 3 native-routing gate
449
+ if (wantsStructuredOutput &&
450
+ !options.disableTools &&
451
+ this.supportsTools()) {
452
+ logger.warn("[GoogleAIStudio] Structured output active — disabling tools (Gemini limitation).");
453
+ }
454
+ const shouldUseTools = !options.disableTools && this.supportsTools() && !wantsStructuredOutput;
446
455
  const baseTools = shouldUseTools ? await this.getAllTools() : {};
447
456
  const rawTools = shouldUseTools
448
457
  ? { ...baseTools, ...(options.tools || {}) }
@@ -88,6 +88,18 @@ export function sanitizeSchemaForGemini(schema) {
88
88
  result[key] = value;
89
89
  }
90
90
  }
91
+ // Recurse through composed schema branches
92
+ if (Array.isArray(result.allOf)) {
93
+ result.allOf = result.allOf.map((s) => sanitizeSchemaForGemini(s));
94
+ }
95
+ if (result.not && typeof result.not === "object") {
96
+ result.not = sanitizeSchemaForGemini(result.not);
97
+ }
98
+ for (const branch of ["if", "then", "else"]) {
99
+ if (result[branch] && typeof result[branch] === "object") {
100
+ result[branch] = sanitizeSchemaForGemini(result[branch]);
101
+ }
102
+ }
91
103
  return result;
92
104
  }
93
105
  /**
@@ -111,6 +123,10 @@ export function sanitizeToolsForGemini(tools) {
111
123
  typeof params.parse === "function") {
112
124
  const rawJsonSchema = convertZodToJsonSchema(params);
113
125
  const inlined = inlineJsonSchema(rawJsonSchema);
126
+ // Gemini sanitization strips Zod-only features not supported by the Gemini API:
127
+ // union types (anyOf/oneOf) are collapsed to string, default values and
128
+ // additionalProperties are removed. The resulting schema is Gemini-compatible
129
+ // but loses some type constraints from the original Zod schema.
114
130
  const sanitizedSchema = sanitizeSchemaForGemini(inlined);
115
131
  sanitized[name] = createAISDKTool({
116
132
  description: tool.description || `Tool: ${name}`,
@@ -119,13 +135,28 @@ export function sanitizeToolsForGemini(tools) {
119
135
  execute: tool.execute,
120
136
  });
121
137
  }
138
+ else if (params &&
139
+ typeof params === "object" &&
140
+ "jsonSchema" in params) {
141
+ // Non-Zod JSON schema (e.g., from ai SDK jsonSchema() helper) — still needs sanitization
142
+ const rawSchema = params
143
+ .jsonSchema;
144
+ const sanitizedSchema = sanitizeSchemaForGemini(inlineJsonSchema(rawSchema));
145
+ sanitized[name] = createAISDKTool({
146
+ description: tool.description || `Tool: ${name}`,
147
+ parameters: aiJsonSchema(sanitizedSchema),
148
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
149
+ execute: tool.execute,
150
+ });
151
+ }
122
152
  else {
123
153
  sanitized[name] = tool;
124
154
  }
125
155
  }
126
156
  catch (error) {
127
- logger.warn(`[Gemini] Failed to sanitize tool "${name}", using original`, { error: error instanceof Error ? error.message : String(error) });
128
- sanitized[name] = tool;
157
+ logger.warn(`[Gemini] Failed to sanitize tool "${name}", skipping: ${error instanceof Error ? error.message : String(error)}`);
158
+ // Don't fall back to the original tool — an incompatible schema would fail the Gemini request
159
+ continue;
129
160
  }
130
161
  }
131
162
  return sanitized;
@@ -784,7 +784,7 @@ export class GoogleVertexProvider extends BaseProvider {
784
784
  const sdkTools = gemini3CheckShouldUseTools ? await this.getAllTools() : {};
785
785
  const combinedToolCount = Object.keys(optionTools).length + Object.keys(sdkTools).length;
786
786
  const hasTools = gemini3CheckShouldUseTools && combinedToolCount > 0;
787
- if (isGemini3Model(gemini3CheckModelName) && hasTools) {
787
+ if (isGemini3Model(gemini3CheckModelName) && hasTools && !analysisSchema) {
788
788
  // Process CSV files before routing to native SDK (bypasses normal message builder)
789
789
  const processedOptions = await this.processCSVFilesForNativeSDK(options);
790
790
  // Merge SDK tools into options for native SDK path
@@ -792,6 +792,16 @@ export class GoogleVertexProvider extends BaseProvider {
792
792
  ...processedOptions,
793
793
  tools: { ...sdkTools, ...optionTools },
794
794
  };
795
+ // Gemini cannot use tools and JSON schema simultaneously
796
+ const wantsStructuredOutput = analysisSchema ||
797
+ processedOptions.output?.format === "json" ||
798
+ processedOptions.schema;
799
+ if (wantsStructuredOutput) {
800
+ mergedOptions.tools = {};
801
+ mergedOptions.toolChoice = undefined;
802
+ mergedOptions.maxSteps = undefined;
803
+ logger.warn("[GoogleVertex] Structured output active — disabling tools for Gemini 3 (Gemini limitation).");
804
+ }
795
805
  logger.info("[GoogleVertex] Routing Gemini 3 to native SDK for tool calling", {
796
806
  model: gemini3CheckModelName,
797
807
  optionToolCount: Object.keys(optionTools).length,
@@ -59,7 +59,7 @@ export declare class GoogleAIStudioProvider extends BaseProvider {
59
59
  * Estimate token count from text using centralized estimation with provider multipliers
60
60
  */
61
61
  private estimateTokenCount;
62
- protected executeStream(options: StreamOptions, _analysisSchema?: ZodUnknownSchema | Schema<unknown>): Promise<StreamResult>;
62
+ protected executeStream(options: StreamOptions, analysisSchema?: ZodUnknownSchema | Schema<unknown>): Promise<StreamResult>;
63
63
  /**
64
64
  * Execute stream using native @google/genai SDK for Gemini 3 models
65
65
  * This bypasses @ai-sdk/google to properly handle thought_signature
@@ -385,12 +385,15 @@ export class GoogleAIStudioProvider extends BaseProvider {
385
385
  return estimateTokens(text, "google-ai");
386
386
  }
387
387
  // executeGenerate removed - BaseProvider handles all generation with tools
388
- async executeStream(options, _analysisSchema) {
388
+ async executeStream(options, analysisSchema) {
389
389
  // Check if this is a Gemini 3 model with tools - use native SDK for thought_signature
390
390
  const gemini3CheckModelName = options.model || this.modelName;
391
+ // Structured output (analysisSchema, JSON format, or schema) is incompatible with tools on Gemini.
392
+ // Compute once and reuse in both the native Gemini 3 gate and the streamText fallback path.
393
+ const wantsStructuredOutput = analysisSchema || options.output?.format === "json" || options.schema;
391
394
  // Check for tools from options AND from SDK (MCP tools)
392
395
  // Need to check early if we should route to native SDK
393
- const gemini3CheckShouldUseTools = !options.disableTools && this.supportsTools();
396
+ const gemini3CheckShouldUseTools = !options.disableTools && this.supportsTools() && !wantsStructuredOutput;
394
397
  const optionTools = options.tools || {};
395
398
  const sdkTools = gemini3CheckShouldUseTools ? await this.getAllTools() : {};
396
399
  const combinedToolCount = Object.keys(optionTools).length + Object.keys(sdkTools).length;
@@ -442,7 +445,13 @@ export class GoogleAIStudioProvider extends BaseProvider {
442
445
  const timeoutController = createTimeoutController(timeout, this.providerName, "stream");
443
446
  try {
444
447
  // Get tools consistently with generate method (include user-provided RAG tools)
445
- const shouldUseTools = !options.disableTools && this.supportsTools();
448
+ // wantsStructuredOutput already computed before the Gemini 3 native-routing gate
449
+ if (wantsStructuredOutput &&
450
+ !options.disableTools &&
451
+ this.supportsTools()) {
452
+ logger.warn("[GoogleAIStudio] Structured output active — disabling tools (Gemini limitation).");
453
+ }
454
+ const shouldUseTools = !options.disableTools && this.supportsTools() && !wantsStructuredOutput;
446
455
  const baseTools = shouldUseTools ? await this.getAllTools() : {};
447
456
  const rawTools = shouldUseTools
448
457
  ? { ...baseTools, ...(options.tools || {}) }
@@ -88,6 +88,18 @@ export function sanitizeSchemaForGemini(schema) {
88
88
  result[key] = value;
89
89
  }
90
90
  }
91
+ // Recurse through composed schema branches
92
+ if (Array.isArray(result.allOf)) {
93
+ result.allOf = result.allOf.map((s) => sanitizeSchemaForGemini(s));
94
+ }
95
+ if (result.not && typeof result.not === "object") {
96
+ result.not = sanitizeSchemaForGemini(result.not);
97
+ }
98
+ for (const branch of ["if", "then", "else"]) {
99
+ if (result[branch] && typeof result[branch] === "object") {
100
+ result[branch] = sanitizeSchemaForGemini(result[branch]);
101
+ }
102
+ }
91
103
  return result;
92
104
  }
93
105
  /**
@@ -111,6 +123,10 @@ export function sanitizeToolsForGemini(tools) {
111
123
  typeof params.parse === "function") {
112
124
  const rawJsonSchema = convertZodToJsonSchema(params);
113
125
  const inlined = inlineJsonSchema(rawJsonSchema);
126
+ // Gemini sanitization strips Zod-only features not supported by the Gemini API:
127
+ // union types (anyOf/oneOf) are collapsed to string, default values and
128
+ // additionalProperties are removed. The resulting schema is Gemini-compatible
129
+ // but loses some type constraints from the original Zod schema.
114
130
  const sanitizedSchema = sanitizeSchemaForGemini(inlined);
115
131
  sanitized[name] = createAISDKTool({
116
132
  description: tool.description || `Tool: ${name}`,
@@ -119,13 +135,28 @@ export function sanitizeToolsForGemini(tools) {
119
135
  execute: tool.execute,
120
136
  });
121
137
  }
138
+ else if (params &&
139
+ typeof params === "object" &&
140
+ "jsonSchema" in params) {
141
+ // Non-Zod JSON schema (e.g., from ai SDK jsonSchema() helper) — still needs sanitization
142
+ const rawSchema = params
143
+ .jsonSchema;
144
+ const sanitizedSchema = sanitizeSchemaForGemini(inlineJsonSchema(rawSchema));
145
+ sanitized[name] = createAISDKTool({
146
+ description: tool.description || `Tool: ${name}`,
147
+ parameters: aiJsonSchema(sanitizedSchema),
148
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
149
+ execute: tool.execute,
150
+ });
151
+ }
122
152
  else {
123
153
  sanitized[name] = tool;
124
154
  }
125
155
  }
126
156
  catch (error) {
127
- logger.warn(`[Gemini] Failed to sanitize tool "${name}", using original`, { error: error instanceof Error ? error.message : String(error) });
128
- sanitized[name] = tool;
157
+ logger.warn(`[Gemini] Failed to sanitize tool "${name}", skipping: ${error instanceof Error ? error.message : String(error)}`);
158
+ // Don't fall back to the original tool — an incompatible schema would fail the Gemini request
159
+ continue;
129
160
  }
130
161
  }
131
162
  return sanitized;
@@ -784,7 +784,7 @@ export class GoogleVertexProvider extends BaseProvider {
784
784
  const sdkTools = gemini3CheckShouldUseTools ? await this.getAllTools() : {};
785
785
  const combinedToolCount = Object.keys(optionTools).length + Object.keys(sdkTools).length;
786
786
  const hasTools = gemini3CheckShouldUseTools && combinedToolCount > 0;
787
- if (isGemini3Model(gemini3CheckModelName) && hasTools) {
787
+ if (isGemini3Model(gemini3CheckModelName) && hasTools && !analysisSchema) {
788
788
  // Process CSV files before routing to native SDK (bypasses normal message builder)
789
789
  const processedOptions = await this.processCSVFilesForNativeSDK(options);
790
790
  // Merge SDK tools into options for native SDK path
@@ -792,6 +792,16 @@ export class GoogleVertexProvider extends BaseProvider {
792
792
  ...processedOptions,
793
793
  tools: { ...sdkTools, ...optionTools },
794
794
  };
795
+ // Gemini cannot use tools and JSON schema simultaneously
796
+ const wantsStructuredOutput = analysisSchema ||
797
+ processedOptions.output?.format === "json" ||
798
+ processedOptions.schema;
799
+ if (wantsStructuredOutput) {
800
+ mergedOptions.tools = {};
801
+ mergedOptions.toolChoice = undefined;
802
+ mergedOptions.maxSteps = undefined;
803
+ logger.warn("[GoogleVertex] Structured output active — disabling tools for Gemini 3 (Gemini limitation).");
804
+ }
795
805
  logger.info("[GoogleVertex] Routing Gemini 3 to native SDK for tool calling", {
796
806
  model: gemini3CheckModelName,
797
807
  optionToolCount: Object.keys(optionTools).length,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "9.19.1",
3
+ "version": "9.20.0",
4
4
  "description": "Universal AI Development Platform with working MCP integration, multi-provider support, and professional CLI. Built-in tools operational, 58+ external MCP servers discoverable. Connect to filesystem, GitHub, database operations, and more. Build, test, and deploy AI applications with 13 providers: OpenAI, Anthropic, Google AI, AWS Bedrock, Azure, Hugging Face, Ollama, and Mistral AI.",
5
5
  "author": {
6
6
  "name": "Juspay Technologies",