@juspay/neurolink 9.20.0 → 9.22.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,15 @@
1
+ ## [9.22.0](https://github.com/juspay/neurolink/compare/v9.21.0...v9.22.0) (2026-03-12)
2
+
3
+ ### Features
4
+
5
+ - **(landing):** comprehensive mobile redesign across all 13 landing page components ([405e3e5](https://github.com/juspay/neurolink/commit/405e3e5eb8672b50ee1fc319088fb8c2b4fb78a0))
6
+
7
+ ## [9.21.0](https://github.com/juspay/neurolink/compare/v9.20.0...v9.21.0) (2026-03-09)
8
+
9
+ ### Features
10
+
11
+ - **(reports-conversation):** Add support for report metaData in getUserAllSessionsHistory ([2273af0](https://github.com/juspay/neurolink/commit/2273af00f2089dba4f691f771b8e16a6a71274b5))
12
+
1
13
  ## [9.20.0](https://github.com/juspay/neurolink/compare/v9.19.1...v9.20.0) (2026-03-09)
2
14
 
3
15
  ### Features
@@ -20,7 +20,6 @@ function validateRoleAlternation(messages) {
20
20
  if (messages[i].role === messages[i - 1].role &&
21
21
  messages[i].role !== "system") {
22
22
  logger.warn(`[SlidingWindowTruncator] Role alternation broken at index ${i}: consecutive "${messages[i].role}" messages`);
23
- break;
24
23
  }
25
24
  }
26
25
  }
@@ -131,13 +130,13 @@ export function truncateWithSlidingWindow(messages, config) {
131
130
  break;
132
131
  }
133
132
  const keptAfterTruncation = remainingMessages.slice(evenRemoveCount);
134
- // Insert a dedicated system-role truncation marker with machine-readable
135
- // metadata so effectiveHistory.ts can detect it via isTruncationMarker /
133
+ // Insert a truncation marker with machine-readable metadata so
134
+ // effectiveHistory.ts can detect it via isTruncationMarker /
136
135
  // truncationId and removeTruncationTags can rewind it.
137
136
  const truncId = randomUUID();
138
137
  const marker = {
139
138
  id: `truncation-marker-${truncId}`,
140
- role: "system",
139
+ role: "user",
141
140
  content: TRUNCATION_MARKER_CONTENT,
142
141
  isTruncationMarker: true,
143
142
  truncationId: truncId,
@@ -170,11 +169,11 @@ export function truncateWithSlidingWindow(messages, config) {
170
169
  const evenMaxRemove = maxRemove - (maxRemove % 2);
171
170
  if (evenMaxRemove > 0) {
172
171
  const keptMessages = remainingMessages.slice(evenMaxRemove);
173
- // Insert a dedicated system-role truncation marker (see iterative block above)
172
+ // Insert a truncation marker (see iterative block above)
174
173
  const fallbackTruncId = randomUUID();
175
174
  const fallbackMarker = {
176
175
  id: `truncation-marker-${fallbackTruncId}`,
177
- role: "system",
176
+ role: "user",
178
177
  content: TRUNCATION_MARKER_CONTENT,
179
178
  isTruncationMarker: true,
180
179
  truncationId: fallbackTruncId,
@@ -2,7 +2,7 @@
2
2
  * Redis Conversation Memory Manager for NeuroLink
3
3
  * Redis-based implementation of conversation storage with same interface as ConversationMemoryManager
4
4
  */
5
- import type { ChatMessage, ConversationMemoryConfig, ConversationMemoryStats, RedisConversationObject, RedisStorageConfig, SessionMemory, SessionMetadata, StoreConversationTurnOptions } from "../types/conversation.js";
5
+ import type { ChatMessage, ConversationMemoryConfig, ConversationMemoryStats, RedisConversationObject, RedisStorageConfig, SessionMemory, SessionMetadata, StoreConversationTurnOptions, AgenticLoopReportMetadata } from "../types/conversation.js";
6
6
  import type { IConversationMemoryManager } from "../types/conversationMemoryInterface.js";
7
7
  /**
8
8
  * Redis-based implementation of the ConversationMemoryManager
@@ -184,4 +184,14 @@ export declare class RedisConversationMemoryManager implements IConversationMemo
184
184
  * Flush pending tool execution data for a session and merge into conversation
185
185
  */
186
186
  private flushPendingToolData;
187
+ /**
188
+ * Update agentic loop report metadata for a conversation session.
189
+ * Upserts a report entry by reportId — updates existing or adds new.
190
+ * Follows the read → patch → write pattern (same as title generation).
191
+ *
192
+ * @param sessionId The session identifier
193
+ * @param userId The user identifier (optional)
194
+ * @param report The report metadata to upsert
195
+ */
196
+ updateAgenticLoopReport(sessionId: string, userId: string | undefined, report: AgenticLoopReportMetadata): Promise<void>;
187
197
  }
@@ -10,6 +10,7 @@ import { generateToolOutputPreview } from "../context/toolOutputLimits.js";
10
10
  import { SummarizationEngine } from "../context/summarizationEngine.js";
11
11
  import { NeuroLink } from "../neurolink.js";
12
12
  import { ConversationMemoryError } from "../types/conversation.js";
13
+ import { withTimeout } from "../utils/errorHandling.js";
13
14
  import { buildContextFromPointer, getEffectiveTokenThreshold, } from "../utils/conversationMemory.js";
14
15
  import { runWithCurrentLangfuseContext } from "../services/server/ai/observability/instrumentation.js";
15
16
  import { logger } from "../utils/logger.js";
@@ -749,6 +750,11 @@ export class RedisConversationMemoryManager {
749
750
  title: conversation.title,
750
751
  createdAt: conversation.createdAt,
751
752
  updatedAt: conversation.updatedAt,
753
+ metadata: conversation.additionalMetadata?.agenticLoopReports
754
+ ? {
755
+ agenticLoopReports: conversation.additionalMetadata.agenticLoopReports,
756
+ }
757
+ : undefined,
752
758
  };
753
759
  }
754
760
  logger.debug("[RedisConversationMemoryManager] No valid conversation data found", {
@@ -1345,4 +1351,85 @@ User message: "${userMessage}"`;
1345
1351
  this.pendingToolExecutions.delete(pendingKey);
1346
1352
  }
1347
1353
  }
1354
+ /**
1355
+ * Update agentic loop report metadata for a conversation session.
1356
+ * Upserts a report entry by reportId — updates existing or adds new.
1357
+ * Follows the read → patch → write pattern (same as title generation).
1358
+ *
1359
+ * @param sessionId The session identifier
1360
+ * @param userId The user identifier (optional)
1361
+ * @param report The report metadata to upsert
1362
+ */
1363
+ async updateAgenticLoopReport(sessionId, userId, report) {
1364
+ logger.debug("[RedisConversationMemoryManager] Updating agentic loop report", {
1365
+ sessionId,
1366
+ userId,
1367
+ reportId: report.reportId,
1368
+ reportType: report.reportType,
1369
+ reportStatus: report.reportStatus,
1370
+ });
1371
+ await this.ensureInitialized();
1372
+ if (!this.redisClient) {
1373
+ logger.warn("[RedisConversationMemoryManager] Redis client not available for report update", { sessionId, userId });
1374
+ return;
1375
+ }
1376
+ try {
1377
+ const redisKey = getSessionKey(this.redisConfig, sessionId, userId || undefined);
1378
+ const conversationData = await withTimeout(this.redisClient.get(redisKey), 5000);
1379
+ if (!conversationData) {
1380
+ logger.warn("[RedisConversationMemoryManager] No conversation found for report update", { sessionId, userId });
1381
+ return;
1382
+ }
1383
+ const conversation = deserializeConversation(conversationData);
1384
+ if (!conversation) {
1385
+ logger.warn("[RedisConversationMemoryManager] Failed to deserialize conversation for report update", { sessionId, userId });
1386
+ return;
1387
+ }
1388
+ // Initialize additionalMetadata and agenticLoopReports if needed
1389
+ if (!conversation.additionalMetadata) {
1390
+ conversation.additionalMetadata = {};
1391
+ }
1392
+ if (!conversation.additionalMetadata.agenticLoopReports) {
1393
+ conversation.additionalMetadata.agenticLoopReports = [];
1394
+ }
1395
+ // Upsert: find existing report by reportId and update, or push new entry
1396
+ const existingIndex = conversation.additionalMetadata.agenticLoopReports.findIndex((r) => r.reportId === report.reportId);
1397
+ if (existingIndex >= 0) {
1398
+ conversation.additionalMetadata.agenticLoopReports[existingIndex] =
1399
+ report;
1400
+ logger.debug("[RedisConversationMemoryManager] Updated existing agentic loop report", { sessionId, reportId: report.reportId });
1401
+ }
1402
+ else {
1403
+ conversation.additionalMetadata.agenticLoopReports.push(report);
1404
+ logger.debug("[RedisConversationMemoryManager] Added new agentic loop report", { sessionId, reportId: report.reportId });
1405
+ }
1406
+ conversation.updatedAt = new Date().toISOString();
1407
+ // Write back to Redis
1408
+ const serializedData = serializeConversation(conversation);
1409
+ await withTimeout(this.redisClient.set(redisKey, serializedData), 5000);
1410
+ if (this.redisConfig.ttl > 0) {
1411
+ await withTimeout(this.redisClient.expire(redisKey, this.redisConfig.ttl), 5000);
1412
+ }
1413
+ logger.info("[RedisConversationMemoryManager] Successfully updated agentic loop report", {
1414
+ sessionId,
1415
+ userId,
1416
+ reportId: report.reportId,
1417
+ reportStatus: report.reportStatus,
1418
+ });
1419
+ }
1420
+ catch (error) {
1421
+ logger.error("[RedisConversationMemoryManager] Failed to update agentic loop report", {
1422
+ sessionId,
1423
+ userId,
1424
+ reportId: report.reportId,
1425
+ error: error instanceof Error ? error.message : String(error),
1426
+ });
1427
+ throw new ConversationMemoryError("Failed to update agentic loop report", "STORAGE_ERROR", {
1428
+ sessionId,
1429
+ userId,
1430
+ reportId: report.reportId,
1431
+ error: error instanceof Error ? error.message : String(error),
1432
+ });
1433
+ }
1434
+ }
1348
1435
  }
@@ -20,7 +20,6 @@ function validateRoleAlternation(messages) {
20
20
  if (messages[i].role === messages[i - 1].role &&
21
21
  messages[i].role !== "system") {
22
22
  logger.warn(`[SlidingWindowTruncator] Role alternation broken at index ${i}: consecutive "${messages[i].role}" messages`);
23
- break;
24
23
  }
25
24
  }
26
25
  }
@@ -131,13 +130,13 @@ export function truncateWithSlidingWindow(messages, config) {
131
130
  break;
132
131
  }
133
132
  const keptAfterTruncation = remainingMessages.slice(evenRemoveCount);
134
- // Insert a dedicated system-role truncation marker with machine-readable
135
- // metadata so effectiveHistory.ts can detect it via isTruncationMarker /
133
+ // Insert a truncation marker with machine-readable metadata so
134
+ // effectiveHistory.ts can detect it via isTruncationMarker /
136
135
  // truncationId and removeTruncationTags can rewind it.
137
136
  const truncId = randomUUID();
138
137
  const marker = {
139
138
  id: `truncation-marker-${truncId}`,
140
- role: "system",
139
+ role: "user",
141
140
  content: TRUNCATION_MARKER_CONTENT,
142
141
  isTruncationMarker: true,
143
142
  truncationId: truncId,
@@ -170,11 +169,11 @@ export function truncateWithSlidingWindow(messages, config) {
170
169
  const evenMaxRemove = maxRemove - (maxRemove % 2);
171
170
  if (evenMaxRemove > 0) {
172
171
  const keptMessages = remainingMessages.slice(evenMaxRemove);
173
- // Insert a dedicated system-role truncation marker (see iterative block above)
172
+ // Insert a truncation marker (see iterative block above)
174
173
  const fallbackTruncId = randomUUID();
175
174
  const fallbackMarker = {
176
175
  id: `truncation-marker-${fallbackTruncId}`,
177
- role: "system",
176
+ role: "user",
178
177
  content: TRUNCATION_MARKER_CONTENT,
179
178
  isTruncationMarker: true,
180
179
  truncationId: fallbackTruncId,
@@ -2,7 +2,7 @@
2
2
  * Redis Conversation Memory Manager for NeuroLink
3
3
  * Redis-based implementation of conversation storage with same interface as ConversationMemoryManager
4
4
  */
5
- import type { ChatMessage, ConversationMemoryConfig, ConversationMemoryStats, RedisConversationObject, RedisStorageConfig, SessionMemory, SessionMetadata, StoreConversationTurnOptions } from "../types/conversation.js";
5
+ import type { ChatMessage, ConversationMemoryConfig, ConversationMemoryStats, RedisConversationObject, RedisStorageConfig, SessionMemory, SessionMetadata, StoreConversationTurnOptions, AgenticLoopReportMetadata } from "../types/conversation.js";
6
6
  import type { IConversationMemoryManager } from "../types/conversationMemoryInterface.js";
7
7
  /**
8
8
  * Redis-based implementation of the ConversationMemoryManager
@@ -184,4 +184,14 @@ export declare class RedisConversationMemoryManager implements IConversationMemo
184
184
  * Flush pending tool execution data for a session and merge into conversation
185
185
  */
186
186
  private flushPendingToolData;
187
+ /**
188
+ * Update agentic loop report metadata for a conversation session.
189
+ * Upserts a report entry by reportId — updates existing or adds new.
190
+ * Follows the read → patch → write pattern (same as title generation).
191
+ *
192
+ * @param sessionId The session identifier
193
+ * @param userId The user identifier (optional)
194
+ * @param report The report metadata to upsert
195
+ */
196
+ updateAgenticLoopReport(sessionId: string, userId: string | undefined, report: AgenticLoopReportMetadata): Promise<void>;
187
197
  }
@@ -10,6 +10,7 @@ import { generateToolOutputPreview } from "../context/toolOutputLimits.js";
10
10
  import { SummarizationEngine } from "../context/summarizationEngine.js";
11
11
  import { NeuroLink } from "../neurolink.js";
12
12
  import { ConversationMemoryError } from "../types/conversation.js";
13
+ import { withTimeout } from "../utils/errorHandling.js";
13
14
  import { buildContextFromPointer, getEffectiveTokenThreshold, } from "../utils/conversationMemory.js";
14
15
  import { runWithCurrentLangfuseContext } from "../services/server/ai/observability/instrumentation.js";
15
16
  import { logger } from "../utils/logger.js";
@@ -749,6 +750,11 @@ export class RedisConversationMemoryManager {
749
750
  title: conversation.title,
750
751
  createdAt: conversation.createdAt,
751
752
  updatedAt: conversation.updatedAt,
753
+ metadata: conversation.additionalMetadata?.agenticLoopReports
754
+ ? {
755
+ agenticLoopReports: conversation.additionalMetadata.agenticLoopReports,
756
+ }
757
+ : undefined,
752
758
  };
753
759
  }
754
760
  logger.debug("[RedisConversationMemoryManager] No valid conversation data found", {
@@ -1345,5 +1351,86 @@ User message: "${userMessage}"`;
1345
1351
  this.pendingToolExecutions.delete(pendingKey);
1346
1352
  }
1347
1353
  }
1354
+ /**
1355
+ * Update agentic loop report metadata for a conversation session.
1356
+ * Upserts a report entry by reportId — updates existing or adds new.
1357
+ * Follows the read → patch → write pattern (same as title generation).
1358
+ *
1359
+ * @param sessionId The session identifier
1360
+ * @param userId The user identifier (optional)
1361
+ * @param report The report metadata to upsert
1362
+ */
1363
+ async updateAgenticLoopReport(sessionId, userId, report) {
1364
+ logger.debug("[RedisConversationMemoryManager] Updating agentic loop report", {
1365
+ sessionId,
1366
+ userId,
1367
+ reportId: report.reportId,
1368
+ reportType: report.reportType,
1369
+ reportStatus: report.reportStatus,
1370
+ });
1371
+ await this.ensureInitialized();
1372
+ if (!this.redisClient) {
1373
+ logger.warn("[RedisConversationMemoryManager] Redis client not available for report update", { sessionId, userId });
1374
+ return;
1375
+ }
1376
+ try {
1377
+ const redisKey = getSessionKey(this.redisConfig, sessionId, userId || undefined);
1378
+ const conversationData = await withTimeout(this.redisClient.get(redisKey), 5000);
1379
+ if (!conversationData) {
1380
+ logger.warn("[RedisConversationMemoryManager] No conversation found for report update", { sessionId, userId });
1381
+ return;
1382
+ }
1383
+ const conversation = deserializeConversation(conversationData);
1384
+ if (!conversation) {
1385
+ logger.warn("[RedisConversationMemoryManager] Failed to deserialize conversation for report update", { sessionId, userId });
1386
+ return;
1387
+ }
1388
+ // Initialize additionalMetadata and agenticLoopReports if needed
1389
+ if (!conversation.additionalMetadata) {
1390
+ conversation.additionalMetadata = {};
1391
+ }
1392
+ if (!conversation.additionalMetadata.agenticLoopReports) {
1393
+ conversation.additionalMetadata.agenticLoopReports = [];
1394
+ }
1395
+ // Upsert: find existing report by reportId and update, or push new entry
1396
+ const existingIndex = conversation.additionalMetadata.agenticLoopReports.findIndex((r) => r.reportId === report.reportId);
1397
+ if (existingIndex >= 0) {
1398
+ conversation.additionalMetadata.agenticLoopReports[existingIndex] =
1399
+ report;
1400
+ logger.debug("[RedisConversationMemoryManager] Updated existing agentic loop report", { sessionId, reportId: report.reportId });
1401
+ }
1402
+ else {
1403
+ conversation.additionalMetadata.agenticLoopReports.push(report);
1404
+ logger.debug("[RedisConversationMemoryManager] Added new agentic loop report", { sessionId, reportId: report.reportId });
1405
+ }
1406
+ conversation.updatedAt = new Date().toISOString();
1407
+ // Write back to Redis
1408
+ const serializedData = serializeConversation(conversation);
1409
+ await withTimeout(this.redisClient.set(redisKey, serializedData), 5000);
1410
+ if (this.redisConfig.ttl > 0) {
1411
+ await withTimeout(this.redisClient.expire(redisKey, this.redisConfig.ttl), 5000);
1412
+ }
1413
+ logger.info("[RedisConversationMemoryManager] Successfully updated agentic loop report", {
1414
+ sessionId,
1415
+ userId,
1416
+ reportId: report.reportId,
1417
+ reportStatus: report.reportStatus,
1418
+ });
1419
+ }
1420
+ catch (error) {
1421
+ logger.error("[RedisConversationMemoryManager] Failed to update agentic loop report", {
1422
+ sessionId,
1423
+ userId,
1424
+ reportId: report.reportId,
1425
+ error: error instanceof Error ? error.message : String(error),
1426
+ });
1427
+ throw new ConversationMemoryError("Failed to update agentic loop report", "STORAGE_ERROR", {
1428
+ sessionId,
1429
+ userId,
1430
+ reportId: report.reportId,
1431
+ error: error instanceof Error ? error.message : String(error),
1432
+ });
1433
+ }
1434
+ }
1348
1435
  }
1349
1436
  //# sourceMappingURL=redisConversationMemoryManager.js.map
@@ -925,6 +925,26 @@ export declare class NeuroLink {
925
925
  * @returns true if the tool was removed, false if it didn't exist
926
926
  */
927
927
  unregisterTool(name: string): boolean;
928
+ /**
929
+ * Update agentic loop report metadata for a conversation session.
930
+ * Upserts a report entry by reportId — updates existing or adds new.
931
+ * Only supported when using Redis conversation memory.
932
+ *
933
+ * @param sessionId The session identifier
934
+ * @param report The agentic loop report metadata to upsert
935
+ * @param userId Optional user identifier
936
+ * @throws Error if conversation memory is not initialized or is not Redis-backed
937
+ *
938
+ * @example
939
+ * ```typescript
940
+ * await neurolink.updateAgenticLoopReport("session-123", {
941
+ * reportId: "report-abc",
942
+ * reportType: "META",
943
+ * reportStatus: "INPROGRESS",
944
+ * });
945
+ * ```
946
+ */
947
+ updateAgenticLoopReport(sessionId: string, report: import("./types/conversation.js").AgenticLoopReportMetadata, userId?: string): Promise<void>;
928
948
  /**
929
949
  * Get all registered custom tools
930
950
  * @returns Map of tool names to MCPExecutableTool format
@@ -38,6 +38,7 @@ import { initializeMem0 } from "./memory/mem0Initializer.js";
38
38
  import { createMemoryRetrievalTools } from "./memory/memoryRetrievalTools.js";
39
39
  import { initializeHippocampus, } from "./memory/hippocampusInitializer.js";
40
40
  import { flushOpenTelemetry, getLangfuseHealthStatus, initializeOpenTelemetry, isOpenTelemetryInitialized, setLangfuseContext, shutdownOpenTelemetry, } from "./services/server/ai/observability/instrumentation.js";
41
+ import { ConversationMemoryError } from "./types/conversation.js";
41
42
  import { getConversationMessages, storeConversationTurn, } from "./utils/conversationMemory.js";
42
43
  // Enhanced error handling imports
43
44
  import { CircuitBreaker, ERROR_CODES, ErrorFactory, isAbortError, isRetriableError, logStructuredError, NeuroLinkError, withRetry, withTimeout, } from "./utils/errorHandling.js";
@@ -4641,6 +4642,38 @@ Current user's request: ${currentInput}`;
4641
4642
  }
4642
4643
  return removed;
4643
4644
  }
4645
+ /**
4646
+ * Update agentic loop report metadata for a conversation session.
4647
+ * Upserts a report entry by reportId — updates existing or adds new.
4648
+ * Only supported when using Redis conversation memory.
4649
+ *
4650
+ * @param sessionId The session identifier
4651
+ * @param report The agentic loop report metadata to upsert
4652
+ * @param userId Optional user identifier
4653
+ * @throws Error if conversation memory is not initialized or is not Redis-backed
4654
+ *
4655
+ * @example
4656
+ * ```typescript
4657
+ * await neurolink.updateAgenticLoopReport("session-123", {
4658
+ * reportId: "report-abc",
4659
+ * reportType: "META",
4660
+ * reportStatus: "INPROGRESS",
4661
+ * });
4662
+ * ```
4663
+ */
4664
+ async updateAgenticLoopReport(sessionId, report, userId) {
4665
+ if (!this.conversationMemory) {
4666
+ throw new ConversationMemoryError("Conversation memory is not initialized. Enable conversationMemory in NeuroLink options.", "CONFIG_ERROR");
4667
+ }
4668
+ // Check if the memory manager is Redis-backed (has updateAgenticLoopReport method)
4669
+ if (!("updateAgenticLoopReport" in this.conversationMemory) ||
4670
+ typeof this.conversationMemory
4671
+ .updateAgenticLoopReport !== "function") {
4672
+ throw new ConversationMemoryError("updateAgenticLoopReport is only supported with Redis conversation memory.", "CONFIG_ERROR");
4673
+ }
4674
+ await withTimeout(this
4675
+ .conversationMemory.updateAgenticLoopReport(sessionId, userId, report), 5000);
4676
+ }
4644
4677
  /**
4645
4678
  * Get all registered custom tools
4646
4679
  * @returns Map of tool names to MCPExecutableTool format
@@ -457,9 +457,20 @@ export class GoogleAIStudioProvider extends BaseProvider {
457
457
  ? { ...baseTools, ...(options.tools || {}) }
458
458
  : {};
459
459
  // Sanitize tool schemas for Gemini proto compatibility (converts anyOf/oneOf unions to string)
460
- const tools = Object.keys(rawTools).length > 0
461
- ? sanitizeToolsForGemini(rawTools)
462
- : rawTools;
460
+ let tools;
461
+ if (Object.keys(rawTools).length > 0) {
462
+ const sanitized = sanitizeToolsForGemini(rawTools);
463
+ if (sanitized.dropped.length > 0) {
464
+ logger.warn(`[GoogleAIStudio] Dropped ${sanitized.dropped.length} incompatible tool(s): ${sanitized.dropped.join(", ")}`);
465
+ }
466
+ tools =
467
+ Object.keys(sanitized.tools).length > 0
468
+ ? sanitized.tools
469
+ : undefined;
470
+ }
471
+ else {
472
+ tools = undefined;
473
+ }
463
474
  // Build message array from options with multimodal support
464
475
  // Using protected helper from BaseProvider to eliminate code duplication
465
476
  const messages = await this.buildMessagesForStream(options);
@@ -470,7 +481,7 @@ export class GoogleAIStudioProvider extends BaseProvider {
470
481
  maxTokens: options.maxTokens, // No default limit - unlimited unless specified
471
482
  tools,
472
483
  maxSteps: options.maxSteps || DEFAULT_MAX_STEPS,
473
- toolChoice: shouldUseTools ? "auto" : "none",
484
+ toolChoice: shouldUseTools && tools ? "auto" : "none",
474
485
  abortSignal: composeAbortSignals(options.abortSignal, timeoutController?.controller.signal),
475
486
  experimental_telemetry: this.telemetryHandler.getTelemetryConfig(options),
476
487
  // Gemini 3: use thinkingLevel via providerOptions
@@ -66,7 +66,10 @@ export declare function sanitizeSchemaForGemini(schema: Record<string, unknown>)
66
66
  * This function pre-converts each tool's Zod parameters to sanitized JSON Schema
67
67
  * and re-wraps with the Vercel AI SDK's jsonSchema() helper.
68
68
  */
69
- export declare function sanitizeToolsForGemini(tools: Record<string, Tool>): Record<string, Tool>;
69
+ export declare function sanitizeToolsForGemini(tools: Record<string, Tool>): {
70
+ tools: Record<string, Tool>;
71
+ dropped: string[];
72
+ };
70
73
  /**
71
74
  * Convert Vercel AI SDK tools to @google/genai FunctionDeclarations and an execute map.
72
75
  *
@@ -114,6 +114,7 @@ export function sanitizeSchemaForGemini(schema) {
114
114
  */
115
115
  export function sanitizeToolsForGemini(tools) {
116
116
  const sanitized = {};
117
+ const dropped = [];
117
118
  for (const [name, tool] of Object.entries(tools)) {
118
119
  try {
119
120
  const params = tool.parameters;
@@ -156,10 +157,11 @@ export function sanitizeToolsForGemini(tools) {
156
157
  catch (error) {
157
158
  logger.warn(`[Gemini] Failed to sanitize tool "${name}", skipping: ${error instanceof Error ? error.message : String(error)}`);
158
159
  // Don't fall back to the original tool — an incompatible schema would fail the Gemini request
160
+ dropped.push(name);
159
161
  continue;
160
162
  }
161
163
  }
162
- return sanitized;
164
+ return { tools: sanitized, dropped };
163
165
  }
164
166
  /**
165
167
  * Convert Vercel AI SDK tools to @google/genai FunctionDeclarations and an execute map.
@@ -777,14 +777,17 @@ export class GoogleVertexProvider extends BaseProvider {
777
777
  async executeStream(options, analysisSchema) {
778
778
  // Check if this is a Gemini 3 model with tools - use native SDK for thought_signature
779
779
  const gemini3CheckModelName = this.resolveAlias(options.model || this.modelName || getDefaultVertexModel());
780
+ // Structured output (analysisSchema, JSON format, or schema) is incompatible with tools on Gemini.
781
+ // Compute once and reuse in both the native Gemini 3 gate and the streamText fallback path.
782
+ const wantsStructuredOutput = analysisSchema || options.output?.format === "json" || options.schema;
780
783
  // Check for tools from options AND from SDK (MCP tools)
781
784
  // Need to check early if we should route to native SDK
782
- const gemini3CheckShouldUseTools = !options.disableTools && this.supportsTools();
785
+ const gemini3CheckShouldUseTools = !options.disableTools && this.supportsTools() && !wantsStructuredOutput;
783
786
  const optionTools = options.tools || {};
784
787
  const sdkTools = gemini3CheckShouldUseTools ? await this.getAllTools() : {};
785
788
  const combinedToolCount = Object.keys(optionTools).length + Object.keys(sdkTools).length;
786
789
  const hasTools = gemini3CheckShouldUseTools && combinedToolCount > 0;
787
- if (isGemini3Model(gemini3CheckModelName) && hasTools && !analysisSchema) {
790
+ if (isGemini3Model(gemini3CheckModelName) && hasTools) {
788
791
  // Process CSV files before routing to native SDK (bypasses normal message builder)
789
792
  const processedOptions = await this.processCSVFilesForNativeSDK(options);
790
793
  // Merge SDK tools into options for native SDK path
@@ -792,16 +795,6 @@ export class GoogleVertexProvider extends BaseProvider {
792
795
  ...processedOptions,
793
796
  tools: { ...sdkTools, ...optionTools },
794
797
  };
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
- }
805
798
  logger.info("[GoogleVertex] Routing Gemini 3 to native SDK for tool calling", {
806
799
  model: gemini3CheckModelName,
807
800
  optionToolCount: Object.keys(optionTools).length,
@@ -831,15 +824,30 @@ export class GoogleVertexProvider extends BaseProvider {
831
824
  : {};
832
825
  // Only sanitize for Gemini models (not Anthropic/Claude models routed through Vertex)
833
826
  const isAnthropic = isAnthropicModel(gemini3CheckModelName);
834
- const tools = Object.keys(rawTools).length > 0 && !isAnthropic
835
- ? sanitizeToolsForGemini(rawTools)
836
- : rawTools;
827
+ let tools;
828
+ if (Object.keys(rawTools).length > 0 && !isAnthropic) {
829
+ const sanitized = sanitizeToolsForGemini(rawTools);
830
+ if (sanitized.dropped.length > 0) {
831
+ logger.warn(`[GoogleVertex] Dropped ${sanitized.dropped.length} incompatible tool(s): ${sanitized.dropped.join(", ")}`);
832
+ }
833
+ tools =
834
+ Object.keys(sanitized.tools).length > 0
835
+ ? sanitized.tools
836
+ : undefined;
837
+ }
838
+ else if (isAnthropic && Object.keys(rawTools).length > 0) {
839
+ // Anthropic models don't need Gemini sanitization — pass tools through
840
+ tools = rawTools;
841
+ }
842
+ else {
843
+ tools = undefined;
844
+ }
837
845
  logger.debug(`${functionTag}: Tools for streaming`, {
838
846
  shouldUseTools,
839
847
  baseToolCount: Object.keys(baseStreamTools).length,
840
848
  externalToolCount: Object.keys(options.tools || {}).length,
841
- toolCount: Object.keys(tools).length,
842
- toolNames: Object.keys(tools),
849
+ toolCount: Object.keys(tools ?? {}).length,
850
+ toolNames: Object.keys(tools ?? {}),
843
851
  });
844
852
  // Model-specific maxTokens handling
845
853
  const modelName = this.resolveAlias(options.model || this.modelName || getDefaultVertexModel());
@@ -857,6 +865,7 @@ export class GoogleVertexProvider extends BaseProvider {
857
865
  ...(maxTokens && { maxTokens }),
858
866
  maxRetries: 0, // NL11: Disable AI SDK's invisible internal retries; we handle retries with OTel instrumentation
859
867
  ...(shouldUseTools &&
868
+ tools &&
860
869
  Object.keys(tools).length > 0 && {
861
870
  tools,
862
871
  toolChoice: "auto",
@@ -377,6 +377,36 @@ export type SessionMetadata = {
377
377
  title: string;
378
378
  createdAt: string;
379
379
  updatedAt: string;
380
+ /** Additional metadata including agentic loop reports */
381
+ metadata?: {
382
+ agenticLoopReports?: AgenticLoopReportMetadata[];
383
+ };
384
+ };
385
+ /**
386
+ * Report type for agentic loop reports
387
+ * Identifies the platform or category of the report
388
+ */
389
+ export type AgenticLoopReportType = "META" | "GOOGLEADS" | "GOOGLEGA4" | "OTHER";
390
+ /**
391
+ * Status of an agentic loop report
392
+ */
393
+ export type AgenticLoopReportStatus = "INPROGRESS" | "COMPLETED";
394
+ /**
395
+ * Metadata for an individual agentic loop report
396
+ * A conversation session can have multiple reports tracked via this type
397
+ */
398
+ export type AgenticLoopReportMetadata = {
399
+ /** Unique identifier for this report */
400
+ reportId: string;
401
+ /** Platform/category of the report */
402
+ reportType: AgenticLoopReportType;
403
+ /** Current status of the report */
404
+ reportStatus: AgenticLoopReportStatus;
405
+ /** Optional audit period date range for the report */
406
+ auditPeriod?: {
407
+ startDate: string;
408
+ endDate: string;
409
+ };
380
410
  };
381
411
  /**
382
412
  * Base conversation metadata (shared fields across all conversation types)
@@ -413,6 +443,13 @@ export type ConversationBase = {
413
443
  cacheReadTokens?: number;
414
444
  cacheWriteTokens?: number;
415
445
  };
446
+ /** Additional metadata for extensible conversation-level data */
447
+ additionalMetadata?: {
448
+ /** Agentic loop reports associated with this conversation */
449
+ agenticLoopReports?: AgenticLoopReportMetadata[];
450
+ /** Allow future extensibility */
451
+ [key: string]: unknown;
452
+ };
416
453
  };
417
454
  /**
418
455
  * Redis conversation storage object format
@@ -925,6 +925,26 @@ export declare class NeuroLink {
925
925
  * @returns true if the tool was removed, false if it didn't exist
926
926
  */
927
927
  unregisterTool(name: string): boolean;
928
+ /**
929
+ * Update agentic loop report metadata for a conversation session.
930
+ * Upserts a report entry by reportId — updates existing or adds new.
931
+ * Only supported when using Redis conversation memory.
932
+ *
933
+ * @param sessionId The session identifier
934
+ * @param report The agentic loop report metadata to upsert
935
+ * @param userId Optional user identifier
936
+ * @throws Error if conversation memory is not initialized or is not Redis-backed
937
+ *
938
+ * @example
939
+ * ```typescript
940
+ * await neurolink.updateAgenticLoopReport("session-123", {
941
+ * reportId: "report-abc",
942
+ * reportType: "META",
943
+ * reportStatus: "INPROGRESS",
944
+ * });
945
+ * ```
946
+ */
947
+ updateAgenticLoopReport(sessionId: string, report: import("./types/conversation.js").AgenticLoopReportMetadata, userId?: string): Promise<void>;
928
948
  /**
929
949
  * Get all registered custom tools
930
950
  * @returns Map of tool names to MCPExecutableTool format
package/dist/neurolink.js CHANGED
@@ -38,6 +38,7 @@ import { initializeMem0 } from "./memory/mem0Initializer.js";
38
38
  import { createMemoryRetrievalTools } from "./memory/memoryRetrievalTools.js";
39
39
  import { initializeHippocampus, } from "./memory/hippocampusInitializer.js";
40
40
  import { flushOpenTelemetry, getLangfuseHealthStatus, initializeOpenTelemetry, isOpenTelemetryInitialized, setLangfuseContext, shutdownOpenTelemetry, } from "./services/server/ai/observability/instrumentation.js";
41
+ import { ConversationMemoryError } from "./types/conversation.js";
41
42
  import { getConversationMessages, storeConversationTurn, } from "./utils/conversationMemory.js";
42
43
  // Enhanced error handling imports
43
44
  import { CircuitBreaker, ERROR_CODES, ErrorFactory, isAbortError, isRetriableError, logStructuredError, NeuroLinkError, withRetry, withTimeout, } from "./utils/errorHandling.js";
@@ -4641,6 +4642,38 @@ Current user's request: ${currentInput}`;
4641
4642
  }
4642
4643
  return removed;
4643
4644
  }
4645
+ /**
4646
+ * Update agentic loop report metadata for a conversation session.
4647
+ * Upserts a report entry by reportId — updates existing or adds new.
4648
+ * Only supported when using Redis conversation memory.
4649
+ *
4650
+ * @param sessionId The session identifier
4651
+ * @param report The agentic loop report metadata to upsert
4652
+ * @param userId Optional user identifier
4653
+ * @throws Error if conversation memory is not initialized or is not Redis-backed
4654
+ *
4655
+ * @example
4656
+ * ```typescript
4657
+ * await neurolink.updateAgenticLoopReport("session-123", {
4658
+ * reportId: "report-abc",
4659
+ * reportType: "META",
4660
+ * reportStatus: "INPROGRESS",
4661
+ * });
4662
+ * ```
4663
+ */
4664
+ async updateAgenticLoopReport(sessionId, report, userId) {
4665
+ if (!this.conversationMemory) {
4666
+ throw new ConversationMemoryError("Conversation memory is not initialized. Enable conversationMemory in NeuroLink options.", "CONFIG_ERROR");
4667
+ }
4668
+ // Check if the memory manager is Redis-backed (has updateAgenticLoopReport method)
4669
+ if (!("updateAgenticLoopReport" in this.conversationMemory) ||
4670
+ typeof this.conversationMemory
4671
+ .updateAgenticLoopReport !== "function") {
4672
+ throw new ConversationMemoryError("updateAgenticLoopReport is only supported with Redis conversation memory.", "CONFIG_ERROR");
4673
+ }
4674
+ await withTimeout(this
4675
+ .conversationMemory.updateAgenticLoopReport(sessionId, userId, report), 5000);
4676
+ }
4644
4677
  /**
4645
4678
  * Get all registered custom tools
4646
4679
  * @returns Map of tool names to MCPExecutableTool format
@@ -457,9 +457,20 @@ export class GoogleAIStudioProvider extends BaseProvider {
457
457
  ? { ...baseTools, ...(options.tools || {}) }
458
458
  : {};
459
459
  // Sanitize tool schemas for Gemini proto compatibility (converts anyOf/oneOf unions to string)
460
- const tools = Object.keys(rawTools).length > 0
461
- ? sanitizeToolsForGemini(rawTools)
462
- : rawTools;
460
+ let tools;
461
+ if (Object.keys(rawTools).length > 0) {
462
+ const sanitized = sanitizeToolsForGemini(rawTools);
463
+ if (sanitized.dropped.length > 0) {
464
+ logger.warn(`[GoogleAIStudio] Dropped ${sanitized.dropped.length} incompatible tool(s): ${sanitized.dropped.join(", ")}`);
465
+ }
466
+ tools =
467
+ Object.keys(sanitized.tools).length > 0
468
+ ? sanitized.tools
469
+ : undefined;
470
+ }
471
+ else {
472
+ tools = undefined;
473
+ }
463
474
  // Build message array from options with multimodal support
464
475
  // Using protected helper from BaseProvider to eliminate code duplication
465
476
  const messages = await this.buildMessagesForStream(options);
@@ -470,7 +481,7 @@ export class GoogleAIStudioProvider extends BaseProvider {
470
481
  maxTokens: options.maxTokens, // No default limit - unlimited unless specified
471
482
  tools,
472
483
  maxSteps: options.maxSteps || DEFAULT_MAX_STEPS,
473
- toolChoice: shouldUseTools ? "auto" : "none",
484
+ toolChoice: shouldUseTools && tools ? "auto" : "none",
474
485
  abortSignal: composeAbortSignals(options.abortSignal, timeoutController?.controller.signal),
475
486
  experimental_telemetry: this.telemetryHandler.getTelemetryConfig(options),
476
487
  // Gemini 3: use thinkingLevel via providerOptions
@@ -66,7 +66,10 @@ export declare function sanitizeSchemaForGemini(schema: Record<string, unknown>)
66
66
  * This function pre-converts each tool's Zod parameters to sanitized JSON Schema
67
67
  * and re-wraps with the Vercel AI SDK's jsonSchema() helper.
68
68
  */
69
- export declare function sanitizeToolsForGemini(tools: Record<string, Tool>): Record<string, Tool>;
69
+ export declare function sanitizeToolsForGemini(tools: Record<string, Tool>): {
70
+ tools: Record<string, Tool>;
71
+ dropped: string[];
72
+ };
70
73
  /**
71
74
  * Convert Vercel AI SDK tools to @google/genai FunctionDeclarations and an execute map.
72
75
  *
@@ -114,6 +114,7 @@ export function sanitizeSchemaForGemini(schema) {
114
114
  */
115
115
  export function sanitizeToolsForGemini(tools) {
116
116
  const sanitized = {};
117
+ const dropped = [];
117
118
  for (const [name, tool] of Object.entries(tools)) {
118
119
  try {
119
120
  const params = tool.parameters;
@@ -156,10 +157,11 @@ export function sanitizeToolsForGemini(tools) {
156
157
  catch (error) {
157
158
  logger.warn(`[Gemini] Failed to sanitize tool "${name}", skipping: ${error instanceof Error ? error.message : String(error)}`);
158
159
  // Don't fall back to the original tool — an incompatible schema would fail the Gemini request
160
+ dropped.push(name);
159
161
  continue;
160
162
  }
161
163
  }
162
- return sanitized;
164
+ return { tools: sanitized, dropped };
163
165
  }
164
166
  /**
165
167
  * Convert Vercel AI SDK tools to @google/genai FunctionDeclarations and an execute map.
@@ -777,14 +777,17 @@ export class GoogleVertexProvider extends BaseProvider {
777
777
  async executeStream(options, analysisSchema) {
778
778
  // Check if this is a Gemini 3 model with tools - use native SDK for thought_signature
779
779
  const gemini3CheckModelName = this.resolveAlias(options.model || this.modelName || getDefaultVertexModel());
780
+ // Structured output (analysisSchema, JSON format, or schema) is incompatible with tools on Gemini.
781
+ // Compute once and reuse in both the native Gemini 3 gate and the streamText fallback path.
782
+ const wantsStructuredOutput = analysisSchema || options.output?.format === "json" || options.schema;
780
783
  // Check for tools from options AND from SDK (MCP tools)
781
784
  // Need to check early if we should route to native SDK
782
- const gemini3CheckShouldUseTools = !options.disableTools && this.supportsTools();
785
+ const gemini3CheckShouldUseTools = !options.disableTools && this.supportsTools() && !wantsStructuredOutput;
783
786
  const optionTools = options.tools || {};
784
787
  const sdkTools = gemini3CheckShouldUseTools ? await this.getAllTools() : {};
785
788
  const combinedToolCount = Object.keys(optionTools).length + Object.keys(sdkTools).length;
786
789
  const hasTools = gemini3CheckShouldUseTools && combinedToolCount > 0;
787
- if (isGemini3Model(gemini3CheckModelName) && hasTools && !analysisSchema) {
790
+ if (isGemini3Model(gemini3CheckModelName) && hasTools) {
788
791
  // Process CSV files before routing to native SDK (bypasses normal message builder)
789
792
  const processedOptions = await this.processCSVFilesForNativeSDK(options);
790
793
  // Merge SDK tools into options for native SDK path
@@ -792,16 +795,6 @@ export class GoogleVertexProvider extends BaseProvider {
792
795
  ...processedOptions,
793
796
  tools: { ...sdkTools, ...optionTools },
794
797
  };
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
- }
805
798
  logger.info("[GoogleVertex] Routing Gemini 3 to native SDK for tool calling", {
806
799
  model: gemini3CheckModelName,
807
800
  optionToolCount: Object.keys(optionTools).length,
@@ -831,15 +824,30 @@ export class GoogleVertexProvider extends BaseProvider {
831
824
  : {};
832
825
  // Only sanitize for Gemini models (not Anthropic/Claude models routed through Vertex)
833
826
  const isAnthropic = isAnthropicModel(gemini3CheckModelName);
834
- const tools = Object.keys(rawTools).length > 0 && !isAnthropic
835
- ? sanitizeToolsForGemini(rawTools)
836
- : rawTools;
827
+ let tools;
828
+ if (Object.keys(rawTools).length > 0 && !isAnthropic) {
829
+ const sanitized = sanitizeToolsForGemini(rawTools);
830
+ if (sanitized.dropped.length > 0) {
831
+ logger.warn(`[GoogleVertex] Dropped ${sanitized.dropped.length} incompatible tool(s): ${sanitized.dropped.join(", ")}`);
832
+ }
833
+ tools =
834
+ Object.keys(sanitized.tools).length > 0
835
+ ? sanitized.tools
836
+ : undefined;
837
+ }
838
+ else if (isAnthropic && Object.keys(rawTools).length > 0) {
839
+ // Anthropic models don't need Gemini sanitization — pass tools through
840
+ tools = rawTools;
841
+ }
842
+ else {
843
+ tools = undefined;
844
+ }
837
845
  logger.debug(`${functionTag}: Tools for streaming`, {
838
846
  shouldUseTools,
839
847
  baseToolCount: Object.keys(baseStreamTools).length,
840
848
  externalToolCount: Object.keys(options.tools || {}).length,
841
- toolCount: Object.keys(tools).length,
842
- toolNames: Object.keys(tools),
849
+ toolCount: Object.keys(tools ?? {}).length,
850
+ toolNames: Object.keys(tools ?? {}),
843
851
  });
844
852
  // Model-specific maxTokens handling
845
853
  const modelName = this.resolveAlias(options.model || this.modelName || getDefaultVertexModel());
@@ -857,6 +865,7 @@ export class GoogleVertexProvider extends BaseProvider {
857
865
  ...(maxTokens && { maxTokens }),
858
866
  maxRetries: 0, // NL11: Disable AI SDK's invisible internal retries; we handle retries with OTel instrumentation
859
867
  ...(shouldUseTools &&
868
+ tools &&
860
869
  Object.keys(tools).length > 0 && {
861
870
  tools,
862
871
  toolChoice: "auto",
@@ -377,6 +377,36 @@ export type SessionMetadata = {
377
377
  title: string;
378
378
  createdAt: string;
379
379
  updatedAt: string;
380
+ /** Additional metadata including agentic loop reports */
381
+ metadata?: {
382
+ agenticLoopReports?: AgenticLoopReportMetadata[];
383
+ };
384
+ };
385
+ /**
386
+ * Report type for agentic loop reports
387
+ * Identifies the platform or category of the report
388
+ */
389
+ export type AgenticLoopReportType = "META" | "GOOGLEADS" | "GOOGLEGA4" | "OTHER";
390
+ /**
391
+ * Status of an agentic loop report
392
+ */
393
+ export type AgenticLoopReportStatus = "INPROGRESS" | "COMPLETED";
394
+ /**
395
+ * Metadata for an individual agentic loop report
396
+ * A conversation session can have multiple reports tracked via this type
397
+ */
398
+ export type AgenticLoopReportMetadata = {
399
+ /** Unique identifier for this report */
400
+ reportId: string;
401
+ /** Platform/category of the report */
402
+ reportType: AgenticLoopReportType;
403
+ /** Current status of the report */
404
+ reportStatus: AgenticLoopReportStatus;
405
+ /** Optional audit period date range for the report */
406
+ auditPeriod?: {
407
+ startDate: string;
408
+ endDate: string;
409
+ };
380
410
  };
381
411
  /**
382
412
  * Base conversation metadata (shared fields across all conversation types)
@@ -413,6 +443,13 @@ export type ConversationBase = {
413
443
  cacheReadTokens?: number;
414
444
  cacheWriteTokens?: number;
415
445
  };
446
+ /** Additional metadata for extensible conversation-level data */
447
+ additionalMetadata?: {
448
+ /** Agentic loop reports associated with this conversation */
449
+ agenticLoopReports?: AgenticLoopReportMetadata[];
450
+ /** Allow future extensibility */
451
+ [key: string]: unknown;
452
+ };
416
453
  };
417
454
  /**
418
455
  * Redis conversation storage object format
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "9.20.0",
3
+ "version": "9.22.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",