@juspay/neurolink 9.10.0 → 9.11.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.
Files changed (174) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/adapters/video/videoAnalyzer.d.ts +3 -3
  3. package/dist/adapters/video/videoAnalyzer.js +39 -25
  4. package/dist/agent/directTools.d.ts +3 -3
  5. package/dist/cli/commands/config.d.ts +9 -9
  6. package/dist/cli/loop/optionsSchema.d.ts +1 -1
  7. package/dist/constants/contextWindows.d.ts +6 -3
  8. package/dist/constants/contextWindows.js +30 -3
  9. package/dist/constants/index.d.ts +3 -3
  10. package/dist/constants/retry.d.ts +4 -4
  11. package/dist/constants/retry.js +1 -1
  12. package/dist/context/contextCompactor.d.ts +1 -1
  13. package/dist/context/contextCompactor.js +59 -1
  14. package/dist/context/summarizationEngine.d.ts +2 -2
  15. package/dist/context/summarizationEngine.js +44 -18
  16. package/dist/context/toolOutputLimits.d.ts +22 -13
  17. package/dist/context/toolOutputLimits.js +58 -64
  18. package/dist/core/baseProvider.d.ts +11 -2
  19. package/dist/core/baseProvider.js +30 -1
  20. package/dist/core/conversationMemoryManager.d.ts +13 -1
  21. package/dist/core/conversationMemoryManager.js +36 -5
  22. package/dist/core/modules/GenerationHandler.d.ts +6 -0
  23. package/dist/core/modules/GenerationHandler.js +192 -7
  24. package/dist/core/modules/MessageBuilder.js +42 -4
  25. package/dist/core/modules/TelemetryHandler.js +4 -1
  26. package/dist/core/redisConversationMemoryManager.d.ts +19 -3
  27. package/dist/core/redisConversationMemoryManager.js +253 -58
  28. package/dist/index.d.ts +2 -0
  29. package/dist/index.js +3 -0
  30. package/dist/lib/adapters/video/videoAnalyzer.d.ts +3 -3
  31. package/dist/lib/adapters/video/videoAnalyzer.js +39 -25
  32. package/dist/lib/agent/directTools.d.ts +7 -7
  33. package/dist/lib/constants/contextWindows.d.ts +6 -3
  34. package/dist/lib/constants/contextWindows.js +30 -3
  35. package/dist/lib/constants/index.d.ts +3 -3
  36. package/dist/lib/constants/retry.d.ts +4 -4
  37. package/dist/lib/constants/retry.js +1 -1
  38. package/dist/lib/context/contextCompactor.d.ts +1 -1
  39. package/dist/lib/context/contextCompactor.js +59 -1
  40. package/dist/lib/context/summarizationEngine.d.ts +2 -2
  41. package/dist/lib/context/summarizationEngine.js +44 -18
  42. package/dist/lib/context/toolOutputLimits.d.ts +22 -13
  43. package/dist/lib/context/toolOutputLimits.js +58 -64
  44. package/dist/lib/core/baseProvider.d.ts +11 -2
  45. package/dist/lib/core/baseProvider.js +30 -1
  46. package/dist/lib/core/conversationMemoryManager.d.ts +13 -1
  47. package/dist/lib/core/conversationMemoryManager.js +36 -5
  48. package/dist/lib/core/modules/GenerationHandler.d.ts +6 -0
  49. package/dist/lib/core/modules/GenerationHandler.js +192 -7
  50. package/dist/lib/core/modules/MessageBuilder.js +42 -4
  51. package/dist/lib/core/modules/TelemetryHandler.js +4 -1
  52. package/dist/lib/core/redisConversationMemoryManager.d.ts +19 -3
  53. package/dist/lib/core/redisConversationMemoryManager.js +253 -58
  54. package/dist/lib/files/fileTools.d.ts +3 -3
  55. package/dist/lib/index.d.ts +2 -0
  56. package/dist/lib/index.js +3 -0
  57. package/dist/lib/mcp/externalServerManager.js +36 -1
  58. package/dist/lib/memory/memoryRetrievalTools.d.ts +166 -0
  59. package/dist/lib/memory/memoryRetrievalTools.js +145 -0
  60. package/dist/lib/neurolink.d.ts +35 -1
  61. package/dist/lib/neurolink.js +471 -16
  62. package/dist/lib/providers/amazonBedrock.d.ts +1 -1
  63. package/dist/lib/providers/amazonBedrock.js +78 -45
  64. package/dist/lib/providers/amazonSagemaker.d.ts +1 -1
  65. package/dist/lib/providers/amazonSagemaker.js +1 -1
  66. package/dist/lib/providers/anthropic.d.ts +1 -1
  67. package/dist/lib/providers/anthropic.js +7 -7
  68. package/dist/lib/providers/anthropicBaseProvider.d.ts +1 -1
  69. package/dist/lib/providers/anthropicBaseProvider.js +7 -6
  70. package/dist/lib/providers/azureOpenai.d.ts +1 -1
  71. package/dist/lib/providers/azureOpenai.js +1 -1
  72. package/dist/lib/providers/googleAiStudio.d.ts +1 -1
  73. package/dist/lib/providers/googleAiStudio.js +5 -5
  74. package/dist/lib/providers/googleVertex.d.ts +1 -1
  75. package/dist/lib/providers/googleVertex.js +74 -17
  76. package/dist/lib/providers/huggingFace.d.ts +1 -1
  77. package/dist/lib/providers/huggingFace.js +1 -1
  78. package/dist/lib/providers/litellm.d.ts +1 -1
  79. package/dist/lib/providers/litellm.js +18 -16
  80. package/dist/lib/providers/mistral.d.ts +1 -1
  81. package/dist/lib/providers/mistral.js +1 -1
  82. package/dist/lib/providers/ollama.d.ts +1 -1
  83. package/dist/lib/providers/ollama.js +8 -7
  84. package/dist/lib/providers/openAI.d.ts +1 -1
  85. package/dist/lib/providers/openAI.js +6 -6
  86. package/dist/lib/providers/openRouter.d.ts +1 -1
  87. package/dist/lib/providers/openRouter.js +6 -2
  88. package/dist/lib/providers/openaiCompatible.d.ts +1 -1
  89. package/dist/lib/providers/openaiCompatible.js +1 -1
  90. package/dist/lib/proxy/proxyFetch.js +291 -65
  91. package/dist/lib/server/utils/validation.d.ts +4 -4
  92. package/dist/lib/services/server/ai/observability/instrumentation.js +12 -3
  93. package/dist/lib/telemetry/telemetryService.d.ts +2 -1
  94. package/dist/lib/telemetry/telemetryService.js +8 -1
  95. package/dist/lib/types/contextTypes.d.ts +26 -2
  96. package/dist/lib/types/conversation.d.ts +72 -40
  97. package/dist/lib/types/conversationMemoryInterface.d.ts +5 -1
  98. package/dist/lib/types/generateTypes.d.ts +26 -0
  99. package/dist/lib/types/modelTypes.d.ts +2 -2
  100. package/dist/lib/types/multimodal.d.ts +2 -0
  101. package/dist/lib/types/observability.d.ts +10 -0
  102. package/dist/lib/types/sdkTypes.d.ts +1 -1
  103. package/dist/lib/utils/conversationMemory.d.ts +4 -3
  104. package/dist/lib/utils/conversationMemory.js +44 -6
  105. package/dist/lib/utils/errorHandling.d.ts +5 -0
  106. package/dist/lib/utils/errorHandling.js +7 -2
  107. package/dist/lib/utils/logger.d.ts +8 -0
  108. package/dist/lib/utils/logger.js +56 -1
  109. package/dist/lib/utils/messageBuilder.js +74 -4
  110. package/dist/lib/utils/redis.js +6 -1
  111. package/dist/lib/utils/tokenEstimation.d.ts +2 -2
  112. package/dist/lib/utils/tokenEstimation.js +16 -1
  113. package/dist/lib/utils/videoAnalysisProcessor.d.ts +2 -1
  114. package/dist/lib/utils/videoAnalysisProcessor.js +7 -2
  115. package/dist/lib/workflow/config.d.ts +110 -110
  116. package/dist/mcp/externalServerManager.js +36 -1
  117. package/dist/memory/memoryRetrievalTools.d.ts +166 -0
  118. package/dist/memory/memoryRetrievalTools.js +144 -0
  119. package/dist/neurolink.d.ts +35 -1
  120. package/dist/neurolink.js +471 -16
  121. package/dist/providers/amazonBedrock.d.ts +1 -1
  122. package/dist/providers/amazonBedrock.js +78 -45
  123. package/dist/providers/amazonSagemaker.d.ts +1 -1
  124. package/dist/providers/amazonSagemaker.js +1 -1
  125. package/dist/providers/anthropic.d.ts +1 -1
  126. package/dist/providers/anthropic.js +7 -7
  127. package/dist/providers/anthropicBaseProvider.d.ts +1 -1
  128. package/dist/providers/anthropicBaseProvider.js +7 -6
  129. package/dist/providers/azureOpenai.d.ts +1 -1
  130. package/dist/providers/azureOpenai.js +1 -1
  131. package/dist/providers/googleAiStudio.d.ts +1 -1
  132. package/dist/providers/googleAiStudio.js +5 -5
  133. package/dist/providers/googleVertex.d.ts +1 -1
  134. package/dist/providers/googleVertex.js +74 -17
  135. package/dist/providers/huggingFace.d.ts +1 -1
  136. package/dist/providers/huggingFace.js +1 -1
  137. package/dist/providers/litellm.d.ts +1 -1
  138. package/dist/providers/litellm.js +18 -16
  139. package/dist/providers/mistral.d.ts +1 -1
  140. package/dist/providers/mistral.js +1 -1
  141. package/dist/providers/ollama.d.ts +1 -1
  142. package/dist/providers/ollama.js +8 -7
  143. package/dist/providers/openAI.d.ts +1 -1
  144. package/dist/providers/openAI.js +6 -6
  145. package/dist/providers/openRouter.d.ts +1 -1
  146. package/dist/providers/openRouter.js +6 -2
  147. package/dist/providers/openaiCompatible.d.ts +1 -1
  148. package/dist/providers/openaiCompatible.js +1 -1
  149. package/dist/proxy/proxyFetch.js +291 -65
  150. package/dist/services/server/ai/observability/instrumentation.js +12 -3
  151. package/dist/telemetry/telemetryService.d.ts +2 -1
  152. package/dist/telemetry/telemetryService.js +8 -1
  153. package/dist/types/contextTypes.d.ts +26 -2
  154. package/dist/types/conversation.d.ts +72 -40
  155. package/dist/types/conversationMemoryInterface.d.ts +5 -1
  156. package/dist/types/generateTypes.d.ts +26 -0
  157. package/dist/types/modelTypes.d.ts +10 -10
  158. package/dist/types/multimodal.d.ts +2 -0
  159. package/dist/types/observability.d.ts +10 -0
  160. package/dist/types/sdkTypes.d.ts +1 -1
  161. package/dist/utils/conversationMemory.d.ts +4 -3
  162. package/dist/utils/conversationMemory.js +44 -6
  163. package/dist/utils/errorHandling.d.ts +5 -0
  164. package/dist/utils/errorHandling.js +7 -2
  165. package/dist/utils/logger.d.ts +8 -0
  166. package/dist/utils/logger.js +56 -1
  167. package/dist/utils/messageBuilder.js +74 -4
  168. package/dist/utils/redis.js +6 -1
  169. package/dist/utils/tokenEstimation.d.ts +2 -2
  170. package/dist/utils/tokenEstimation.js +16 -1
  171. package/dist/utils/videoAnalysisProcessor.d.ts +2 -1
  172. package/dist/utils/videoAnalysisProcessor.js +7 -2
  173. package/dist/workflow/config.d.ts +12 -12
  174. package/package.json +1 -1
@@ -15,12 +15,14 @@ catch {
15
15
  }
16
16
  import { EventEmitter } from "events";
17
17
  import pLimit from "p-limit";
18
+ import { ErrorCategory, ErrorSeverity } from "./constants/enums.js";
18
19
  import { CIRCUIT_BREAKER, CIRCUIT_BREAKER_RESET_MS, MEMORY_THRESHOLDS, NANOSECOND_TO_MS_DIVISOR, PERFORMANCE_THRESHOLDS, PROVIDER_TIMEOUTS, RETRY_ATTEMPTS, RETRY_DELAYS, TOOL_TIMEOUTS, } from "./constants/index.js";
19
20
  import { checkContextBudget } from "./context/budgetChecker.js";
20
21
  import { ContextCompactor, } from "./context/contextCompactor.js";
21
22
  import { isContextOverflowError } from "./context/errorDetection.js";
22
23
  import { repairToolPairs } from "./context/toolPairRepair.js";
23
24
  import { SYSTEM_LIMITS } from "./core/constants.js";
25
+ import { ConversationMemoryManager } from "./core/conversationMemoryManager.js";
24
26
  import { AIProviderFactory } from "./core/factory.js";
25
27
  import { ProviderRegistry } from "./factories/providerRegistry.js";
26
28
  import { FileReferenceRegistry } from "./files/fileReferenceRegistry.js";
@@ -31,10 +33,11 @@ import { ExternalServerManager } from "./mcp/externalServerManager.js";
31
33
  import { directToolsServer } from "./mcp/servers/agent/directToolsServer.js";
32
34
  import { MCPToolRegistry } from "./mcp/toolRegistry.js";
33
35
  import { initializeMem0 } from "./memory/mem0Initializer.js";
36
+ import { createMemoryRetrievalTools } from "./memory/memoryRetrievalTools.js";
34
37
  import { flushOpenTelemetry, getLangfuseHealthStatus, initializeOpenTelemetry, isOpenTelemetryInitialized, setLangfuseContext, shutdownOpenTelemetry, } from "./services/server/ai/observability/instrumentation.js";
35
38
  import { getConversationMessages, storeConversationTurn, } from "./utils/conversationMemory.js";
36
39
  // Enhanced error handling imports
37
- import { CircuitBreaker, ErrorFactory, isAbortError, isRetriableError, logStructuredError, NeuroLinkError, withRetry, withTimeout, } from "./utils/errorHandling.js";
40
+ import { CircuitBreaker, ERROR_CODES, ErrorFactory, isAbortError, isRetriableError, logStructuredError, NeuroLinkError, withRetry, withTimeout, } from "./utils/errorHandling.js";
38
41
  // Factory processing imports
39
42
  import { createCleanStreamOptions, enhanceTextGenerationOptions, processFactoryOptions, processStreamingFactoryOptions, validateFactoryConfig, } from "./utils/factoryProcessing.js";
40
43
  import { logger, mcpLogger } from "./utils/logger.js";
@@ -171,6 +174,8 @@ export class NeuroLink {
171
174
  // Mem0 memory instance and config for conversation context
172
175
  mem0Instance;
173
176
  mem0Config;
177
+ // Accumulated cost in USD across all generate() calls on this instance
178
+ _sessionCostUsd = 0;
174
179
  // File Reference Registry for lazy on-demand file processing
175
180
  fileRegistry;
176
181
  // Cached file tools to avoid redundant createFileTools() calls per generate/stream
@@ -184,11 +189,42 @@ export class NeuroLink {
184
189
  options.context !== null) {
185
190
  try {
186
191
  const ctx = options.context;
187
- if (ctx.userId || ctx.sessionId) {
192
+ // Trigger context scoping if any meaningful Langfuse field is present
193
+ if (ctx.userId ||
194
+ ctx.sessionId ||
195
+ ctx.conversationId ||
196
+ ctx.requestId ||
197
+ ctx.traceName ||
198
+ ctx.metadata) {
199
+ // Build customAttributes from top-level metadata string/number/boolean fields
200
+ let customAttributes;
201
+ if (ctx.metadata && typeof ctx.metadata === "object") {
202
+ const metaObj = ctx.metadata;
203
+ const attrs = {};
204
+ for (const [k, v] of Object.entries(metaObj)) {
205
+ if (typeof v === "string" ||
206
+ typeof v === "number" ||
207
+ typeof v === "boolean") {
208
+ attrs[k] = v;
209
+ }
210
+ }
211
+ if (Object.keys(attrs).length > 0) {
212
+ customAttributes = attrs;
213
+ }
214
+ }
188
215
  return await new Promise((resolve, reject) => {
189
216
  setLangfuseContext({
190
217
  userId: typeof ctx.userId === "string" ? ctx.userId : null,
191
218
  sessionId: typeof ctx.sessionId === "string" ? ctx.sessionId : null,
219
+ conversationId: typeof ctx.conversationId === "string"
220
+ ? ctx.conversationId
221
+ : null,
222
+ requestId: typeof ctx.requestId === "string" ? ctx.requestId : null,
223
+ traceName: typeof ctx.traceName === "string" ? ctx.traceName : null,
224
+ metadata: ctx.metadata && typeof ctx.metadata === "object"
225
+ ? ctx.metadata
226
+ : null,
227
+ ...(customAttributes !== undefined && { customAttributes }),
192
228
  }, async () => {
193
229
  try {
194
230
  const result = await callback();
@@ -315,6 +351,7 @@ export class NeuroLink {
315
351
  this.initializeExternalServerManager(constructorId, constructorStartTime, constructorHrTimeStart);
316
352
  this.initializeHITL(config, constructorId, constructorStartTime, constructorHrTimeStart);
317
353
  this.registerFileTools();
354
+ this.registerMemoryRetrievalTools();
318
355
  this.initializeLangfuse(constructorId, constructorStartTime, constructorHrTimeStart);
319
356
  this.logConstructorComplete(constructorId, constructorStartTime, constructorHrTimeStart);
320
357
  }
@@ -518,6 +555,104 @@ export class NeuroLink {
518
555
  logger.debug(`[NeuroLink] Registered ${Object.keys(fileTools).length} file reference tools`);
519
556
  });
520
557
  }
558
+ /**
559
+ * Register memory retrieval tools that allow the AI to access
560
+ * conversation history, including full tool outputs.
561
+ * Only registered when Redis conversation memory is active.
562
+ */
563
+ registerMemoryRetrievalTools() {
564
+ // Check if conversation memory is configured
565
+ // Memory retrieval tool requires Redis (getSessionRaw) but registration
566
+ // is deferred — the execute handler checks at runtime whether the actual
567
+ // memory manager supports getSessionRaw.
568
+ const memConfig = this.conversationMemoryConfig?.conversationMemory;
569
+ const hasRedisConfig = !!memConfig?.redisConfig ||
570
+ (memConfig &&
571
+ "redis" in memConfig &&
572
+ !!memConfig.redis) ||
573
+ process.env.STORAGE_TYPE === "redis";
574
+ if (!memConfig?.enabled || !hasRedisConfig) {
575
+ logger.debug("[NeuroLink] Skipping memory retrieval tools — requires Redis conversation memory");
576
+ return;
577
+ }
578
+ // Defer registration until conversation memory is actually initialized
579
+ // We register a placeholder that will use the lazy-initialized memory manager
580
+ const self = this;
581
+ const tools = {
582
+ retrieve_context: {
583
+ description: "Retrieve messages from conversation memory. Use this to access full tool " +
584
+ "outputs when a result was truncated, review previous assistant responses, " +
585
+ "or search through conversation history.",
586
+ execute: async (params) => {
587
+ // Lazy access: conversationMemory is initialized on first generate() call
588
+ const memoryManager = self.conversationMemory;
589
+ if (!memoryManager || !("getSessionRaw" in memoryManager)) {
590
+ return {
591
+ success: false,
592
+ error: "Memory retrieval not available — Redis memory manager not initialized",
593
+ metadata: {
594
+ toolName: "retrieve_context",
595
+ serverId: "direct",
596
+ executionTime: 0,
597
+ },
598
+ };
599
+ }
600
+ const actualTools = createMemoryRetrievalTools(memoryManager);
601
+ const result = await actualTools.retrieve_context.execute(params, {
602
+ toolCallId: "memory-retrieval",
603
+ messages: [],
604
+ });
605
+ // Check if the tool itself reported an error
606
+ const hasError = result &&
607
+ typeof result === "object" &&
608
+ "error" in result &&
609
+ !("messages" in result);
610
+ const errorMsg = hasError
611
+ ? result.error
612
+ : undefined;
613
+ return {
614
+ success: !hasError,
615
+ data: result,
616
+ ...(errorMsg ? { error: errorMsg } : {}),
617
+ metadata: {
618
+ toolName: "retrieve_context",
619
+ serverId: "direct",
620
+ executionTime: 0,
621
+ },
622
+ };
623
+ },
624
+ },
625
+ };
626
+ const registrations = Object.entries(tools).map(async ([toolName, toolDef]) => {
627
+ const toolId = `direct.${toolName}`;
628
+ const toolInfo = {
629
+ name: toolName,
630
+ description: toolDef.description,
631
+ inputSchema: {},
632
+ serverId: "direct",
633
+ category: "built-in",
634
+ };
635
+ await this.toolRegistry.registerTool(toolId, toolInfo, {
636
+ execute: async (params) => {
637
+ try {
638
+ return await toolDef.execute(params);
639
+ }
640
+ catch (error) {
641
+ return {
642
+ success: false,
643
+ error: error instanceof Error ? error.message : String(error),
644
+ metadata: { toolName, serverId: "direct", executionTime: 0 },
645
+ };
646
+ }
647
+ },
648
+ description: toolDef.description,
649
+ inputSchema: {},
650
+ });
651
+ });
652
+ void Promise.all(registrations).then(() => {
653
+ logger.info("[NeuroLink] Memory retrieval tools registered");
654
+ });
655
+ }
521
656
  /** Format memory context for prompt inclusion */
522
657
  formatMemoryContext(memoryContext, currentInput) {
523
658
  return `Context from previous conversations:
@@ -1451,10 +1586,30 @@ Current user's request: ${currentInput}`;
1451
1586
  if (!options.input?.text || typeof options.input.text !== "string") {
1452
1587
  throw new Error("Input text is required and must be a non-empty string");
1453
1588
  }
1589
+ // Check budget limit before making API call
1590
+ if (options.maxBudgetUsd !== undefined &&
1591
+ options.maxBudgetUsd > 0 &&
1592
+ this._sessionCostUsd >= options.maxBudgetUsd) {
1593
+ throw new NeuroLinkError({
1594
+ code: "SESSION_BUDGET_EXCEEDED",
1595
+ message: `Session budget exceeded: spent $${this._sessionCostUsd.toFixed(4)} of $${options.maxBudgetUsd.toFixed(4)} limit`,
1596
+ category: ErrorCategory.VALIDATION,
1597
+ severity: ErrorSeverity.HIGH,
1598
+ retriable: false,
1599
+ context: {
1600
+ spent: this._sessionCostUsd,
1601
+ limit: options.maxBudgetUsd,
1602
+ },
1603
+ });
1604
+ }
1454
1605
  // Check if workflow is requested
1455
1606
  if (options.workflow || options.workflowConfig) {
1456
1607
  return await this.generateWithWorkflow(options);
1457
1608
  }
1609
+ // Check if PPT output mode is requested
1610
+ if (options.output?.mode === "ppt") {
1611
+ return await this.generateWithPPT(options);
1612
+ }
1458
1613
  // Set session and user IDs from context for Langfuse spans and execute with proper async scoping
1459
1614
  return await this.setLangfuseContextFromOptions(options, async () => {
1460
1615
  if (this.conversationMemoryConfig?.conversationMemory?.mem0Enabled &&
@@ -1585,6 +1740,20 @@ Current user's request: ${currentInput}`;
1585
1740
  abortSignal: options.abortSignal,
1586
1741
  skipToolPromptInjection: options.skipToolPromptInjection,
1587
1742
  };
1743
+ // Auto-map top-level sessionId/userId to context for convenience
1744
+ // Tests and users may pass sessionId/userId as top-level options
1745
+ const extraContext = options;
1746
+ if (extraContext.sessionId || extraContext.userId) {
1747
+ baseOptions.context = {
1748
+ ...baseOptions.context,
1749
+ ...(extraContext.sessionId && !baseOptions.context?.sessionId
1750
+ ? { sessionId: extraContext.sessionId }
1751
+ : {}),
1752
+ ...(extraContext.userId && !baseOptions.context?.userId
1753
+ ? { userId: extraContext.userId }
1754
+ : {}),
1755
+ };
1756
+ }
1588
1757
  // Apply factory enhancement using centralized utilities
1589
1758
  const textOptions = enhanceTextGenerationOptions(baseOptions, factoryResult);
1590
1759
  // Pass conversation memory config if available
@@ -1664,6 +1833,10 @@ Current user's request: ${currentInput}`;
1664
1833
  video: textResult.video,
1665
1834
  ppt: textResult.ppt,
1666
1835
  };
1836
+ // Accumulate session cost for budget tracking
1837
+ if (generateResult.analytics?.cost && generateResult.analytics.cost > 0) {
1838
+ this._sessionCostUsd += generateResult.analytics.cost;
1839
+ }
1667
1840
  this.scheduleGenerateMem0Storage(options, originalPrompt, generateResult);
1668
1841
  return generateResult;
1669
1842
  });
@@ -1691,6 +1864,40 @@ Current user's request: ${currentInput}`;
1691
1864
  });
1692
1865
  }
1693
1866
  }
1867
+ /**
1868
+ * Handle PPT generation mode
1869
+ */
1870
+ async generateWithPPT(options) {
1871
+ const startTime = Date.now();
1872
+ // Dynamic import to avoid circular deps (same pattern as RAG)
1873
+ const { generatePresentation } = await import("./features/ppt/presentationOrchestrator.js");
1874
+ const { extractPPTContext, getEffectivePPTProvider } = await import("./features/ppt/utils.js");
1875
+ // Get provider instance for content planning
1876
+ const requestedProvider = (options.provider || "vertex");
1877
+ const provider = await AIProviderFactory.createProvider(requestedProvider, options.model, true, this);
1878
+ // Resolve effective PPT provider (may auto-select if current is not PPT-compatible)
1879
+ const effectiveProvider = await getEffectivePPTProvider(provider, requestedProvider, options.model || "default", this);
1880
+ // Extract PPT context from options
1881
+ const pptContext = extractPPTContext(options);
1882
+ // Generate the presentation
1883
+ const pptResult = await generatePresentation({
1884
+ context: pptContext,
1885
+ provider: effectiveProvider.provider,
1886
+ providerName: effectiveProvider.providerName,
1887
+ modelName: effectiveProvider.modelName,
1888
+ neurolink: this,
1889
+ });
1890
+ // Map PPT result to GenerateResult
1891
+ return {
1892
+ content: `Presentation generated: ${pptResult.filePath} (${pptResult.totalSlides} slides)`,
1893
+ finishReason: "stop",
1894
+ provider: pptResult.provider || requestedProvider,
1895
+ model: pptResult.model || options.model || "default",
1896
+ usage: undefined,
1897
+ responseTime: Date.now() - startTime,
1898
+ ppt: pptResult,
1899
+ };
1900
+ }
1694
1901
  /**
1695
1902
  * Generate with workflow engine integration
1696
1903
  * Returns both original and processed responses for AB testing
@@ -1957,6 +2164,11 @@ Current user's request: ${currentInput}`;
1957
2164
  */
1958
2165
  async generateTextInternal(options) {
1959
2166
  const generateInternalId = `generate-internal-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
2167
+ const existingRequestId = options.context?.requestId;
2168
+ const requestId = typeof existingRequestId === "string" && existingRequestId
2169
+ ? existingRequestId
2170
+ : `req-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
2171
+ options.context = { ...options.context, requestId };
1960
2172
  const generateInternalStartTime = Date.now();
1961
2173
  const generateInternalHrTimeStart = process.hrtime.bigint();
1962
2174
  const functionTag = "NeuroLink.generateTextInternal";
@@ -1972,11 +2184,23 @@ Current user's request: ${currentInput}`;
1972
2184
  responseTimeMs: Date.now() - generateInternalStartTime,
1973
2185
  tokensUsed: mcpResult.usage?.total || 0,
1974
2186
  toolsUsed: mcpResult.toolsUsed?.length || 0,
2187
+ ...(mcpResult.usage?.cacheCreationTokens !== undefined && {
2188
+ cacheCreationTokens: mcpResult.usage.cacheCreationTokens,
2189
+ }),
2190
+ ...(mcpResult.usage?.cacheReadTokens !== undefined && {
2191
+ cacheReadTokens: mcpResult.usage.cacheReadTokens,
2192
+ }),
2193
+ ...(mcpResult.usage?.cacheSavingsPercent !== undefined && {
2194
+ cacheSavingsPercent: mcpResult.usage.cacheSavingsPercent,
2195
+ }),
1975
2196
  });
1976
- await storeConversationTurn(this.conversationMemory, options, mcpResult, new Date(generateInternalStartTime));
2197
+ await storeConversationTurn(this.conversationMemory, options, mcpResult, new Date(generateInternalStartTime), requestId);
1977
2198
  this.emitter.emit("response:end", mcpResult.content || "");
1978
2199
  return mcpResult;
1979
2200
  }
2201
+ if (options.abortSignal?.aborted) {
2202
+ throw new DOMException("The operation was aborted", "AbortError");
2203
+ }
1980
2204
  const directResult = await this.directProviderGeneration(options);
1981
2205
  logger.debug(`[${functionTag}] Direct generation successful`);
1982
2206
  logger.info(`[NeuroLink.generateTextInternal] generate() - COMPLETE SUCCESS`, {
@@ -1985,8 +2209,17 @@ Current user's request: ${currentInput}`;
1985
2209
  responseTimeMs: Date.now() - generateInternalStartTime,
1986
2210
  tokensUsed: directResult.usage?.total || 0,
1987
2211
  toolsUsed: directResult.toolsUsed?.length || 0,
2212
+ ...(directResult.usage?.cacheCreationTokens !== undefined && {
2213
+ cacheCreationTokens: directResult.usage.cacheCreationTokens,
2214
+ }),
2215
+ ...(directResult.usage?.cacheReadTokens !== undefined && {
2216
+ cacheReadTokens: directResult.usage.cacheReadTokens,
2217
+ }),
2218
+ ...(directResult.usage?.cacheSavingsPercent !== undefined && {
2219
+ cacheSavingsPercent: directResult.usage.cacheSavingsPercent,
2220
+ }),
1988
2221
  });
1989
- await storeConversationTurn(this.conversationMemory, options, directResult, new Date(generateInternalStartTime));
2222
+ await storeConversationTurn(this.conversationMemory, options, directResult, new Date(generateInternalStartTime), requestId);
1990
2223
  this.emitter.emit("response:end", directResult.content || "");
1991
2224
  this.emitter.emit("message", `Text generation completed successfully`);
1992
2225
  return directResult;
@@ -2012,7 +2245,7 @@ Current user's request: ${currentInput}`;
2012
2245
  enableSummarize: false, // Skip LLM call for recovery
2013
2246
  truncationFraction: 0.75, // Aggressive truncation
2014
2247
  });
2015
- const compactionResult = await compactor.compact(conversationMessages, compactionTarget);
2248
+ const compactionResult = await compactor.compact(conversationMessages, compactionTarget, undefined, options.context?.requestId);
2016
2249
  if (compactionResult.compacted) {
2017
2250
  const repairedResult = repairToolPairs(compactionResult.messages);
2018
2251
  logger.info(`[${functionTag}] Aggressive compaction complete, retrying`, {
@@ -2053,7 +2286,7 @@ Current user's request: ${currentInput}`;
2053
2286
  model: options.model || "unknown",
2054
2287
  responseTime: Date.now() - generateInternalStartTime,
2055
2288
  };
2056
- await withTimeout(storeConversationTurn(this.conversationMemory, options, abortedResult, new Date(generateInternalStartTime)), 5000);
2289
+ await withTimeout(storeConversationTurn(this.conversationMemory, options, abortedResult, new Date(generateInternalStartTime), requestId), 5000);
2057
2290
  }
2058
2291
  catch (storeError) {
2059
2292
  logger.warn(`[${functionTag}] Failed to store conversation turn after abort`, {
@@ -2110,7 +2343,22 @@ Current user's request: ${currentInput}`;
2110
2343
  elapsedNs: (process.hrtime.bigint() - generateInternalHrTimeStart).toString(),
2111
2344
  message: "Starting conversation memory initialization",
2112
2345
  });
2113
- await this.conversationMemory.initialize();
2346
+ try {
2347
+ await this.conversationMemory.initialize();
2348
+ }
2349
+ catch (err) {
2350
+ logger.warn("[NEUROLINK] Redis memory init failed, falling back to in-memory", {
2351
+ error: err instanceof Error ? err.message : String(err),
2352
+ generateInternalId,
2353
+ });
2354
+ const memCfg = this.conversationMemoryConfig?.conversationMemory;
2355
+ this.conversationMemory = new ConversationMemoryManager({
2356
+ enabled: true,
2357
+ maxSessions: memCfg?.maxSessions ?? 100,
2358
+ maxTurnsPerSession: memCfg?.maxTurnsPerSession ?? 50,
2359
+ });
2360
+ await this.conversationMemory.initialize();
2361
+ }
2114
2362
  const conversationMemoryEndTime = process.hrtime.bigint();
2115
2363
  const conversationMemoryDurationNs = conversationMemoryEndTime - conversationMemoryStartTime;
2116
2364
  logger.debug(`[NeuroLink] ✅ LOG_POINT_G004_CONVERSATION_MEMORY_INIT_SUCCESS`, {
@@ -2142,6 +2390,10 @@ Current user's request: ${currentInput}`;
2142
2390
  const maxMcpRetries = RETRY_ATTEMPTS.QUICK;
2143
2391
  const maxAttempts = maxMcpRetries + 1;
2144
2392
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
2393
+ if (options.abortSignal?.aborted) {
2394
+ logger.debug(`[${functionTag}] Abort signal already fired before attempt ${attempt}, stopping retries`);
2395
+ throw new DOMException("The operation was aborted", "AbortError");
2396
+ }
2145
2397
  try {
2146
2398
  logger.debug(`[${functionTag}] Attempting MCP generation (attempt ${attempt}/${maxAttempts})...`);
2147
2399
  const mcpResult = await this.tryMCPGeneration(options);
@@ -2198,7 +2450,21 @@ Current user's request: ${currentInput}`;
2198
2450
  logger.debug(`[${functionTag}] All MCP attempts exhausted, falling back to direct generation`);
2199
2451
  break;
2200
2452
  }
2201
- await new Promise((resolve) => setTimeout(resolve, 500));
2453
+ await new Promise((resolve) => {
2454
+ const timer = setTimeout(resolve, 500);
2455
+ if (options.abortSignal) {
2456
+ const onAbort = () => {
2457
+ clearTimeout(timer);
2458
+ resolve();
2459
+ };
2460
+ options.abortSignal.addEventListener("abort", onAbort, {
2461
+ once: true,
2462
+ });
2463
+ }
2464
+ });
2465
+ if (options.abortSignal?.aborted) {
2466
+ throw new DOMException("The operation was aborted", "AbortError");
2467
+ }
2202
2468
  }
2203
2469
  }
2204
2470
  return null;
@@ -2207,7 +2473,12 @@ Current user's request: ${currentInput}`;
2207
2473
  * Try MCP-enhanced generation (no fallback recursion)
2208
2474
  */
2209
2475
  async tryMCPGeneration(options) {
2476
+ if (options.abortSignal?.aborted) {
2477
+ throw new DOMException("The operation was aborted", "AbortError");
2478
+ }
2210
2479
  // 🚀 EXHAUSTIVE LOGGING POINT T001: TRY MCP GENERATION ENTRY
2480
+ const requestId = options.context?.requestId ||
2481
+ "unknown";
2211
2482
  const tryMCPId = `try-mcp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
2212
2483
  const tryMCPStartTime = Date.now();
2213
2484
  const tryMCPHrTimeStart = process.hrtime.bigint();
@@ -2261,6 +2532,7 @@ Current user's request: ${currentInput}`;
2261
2532
  ? options.systemPrompt || ""
2262
2533
  : this.createToolAwareSystemPrompt(options.systemPrompt, availableTools);
2263
2534
  logger.debug("Tool-aware system prompt created", {
2535
+ requestId,
2264
2536
  originalPromptLength: options.systemPrompt?.length || 0,
2265
2537
  enhancedPromptLength: enhancedSystemPrompt.length,
2266
2538
  skippedToolInjection: !!options.skipToolPromptInjection,
@@ -2268,6 +2540,44 @@ Current user's request: ${currentInput}`;
2268
2540
  });
2269
2541
  // Get conversation messages for context
2270
2542
  let conversationMessages = await getConversationMessages(this.conversationMemory, options);
2543
+ if (logger.shouldLog("debug")) {
2544
+ try {
2545
+ logger.debug("[Observability] Conversation history summary", {
2546
+ requestId,
2547
+ messageCount: conversationMessages?.length || 0,
2548
+ messages: conversationMessages?.map((msg, i) => {
2549
+ let contentLength;
2550
+ if (typeof msg.content === "string") {
2551
+ contentLength = msg.content.length;
2552
+ }
2553
+ else {
2554
+ try {
2555
+ contentLength = JSON.stringify(msg.content).length;
2556
+ }
2557
+ catch {
2558
+ contentLength = 0;
2559
+ }
2560
+ }
2561
+ return {
2562
+ index: i,
2563
+ role: msg.role,
2564
+ contentLength,
2565
+ contentPreview: typeof msg.content === "string"
2566
+ ? msg.content.substring(0, 200)
2567
+ : "[multimodal]",
2568
+ };
2569
+ }),
2570
+ });
2571
+ }
2572
+ catch {
2573
+ // Ignore serialization errors in debug logging
2574
+ }
2575
+ }
2576
+ logger.debug("[Observability] Available tools for LLM", {
2577
+ requestId,
2578
+ toolCount: availableTools?.length || 0,
2579
+ toolNames: availableTools?.map((t) => t.name) || [],
2580
+ });
2271
2581
  // Pre-generation budget check
2272
2582
  const budgetResult = checkContextBudget({
2273
2583
  provider: providerName,
@@ -2278,6 +2588,19 @@ Current user's request: ${currentInput}`;
2278
2588
  currentPrompt: options.prompt,
2279
2589
  toolDefinitions: availableTools,
2280
2590
  });
2591
+ logger.info("[TokenBudget] Token breakdown", {
2592
+ requestId,
2593
+ system: budgetResult.breakdown?.systemPrompt || 0,
2594
+ history: budgetResult.breakdown?.conversationHistory || 0,
2595
+ tools: budgetResult.breakdown?.toolDefinitions || 0,
2596
+ currentPrompt: budgetResult.breakdown?.currentPrompt || 0,
2597
+ files: budgetResult.breakdown?.fileAttachments || 0,
2598
+ total: budgetResult.estimatedInputTokens,
2599
+ budget: budgetResult.availableInputTokens,
2600
+ usagePercent: Math.round(budgetResult.usageRatio * 1000) / 10,
2601
+ conversationMessageCount: conversationMessages?.length || 0,
2602
+ shouldCompact: budgetResult.shouldCompact,
2603
+ });
2281
2604
  if (budgetResult.shouldCompact && this.conversationMemory) {
2282
2605
  logger.info("[NeuroLink] Context budget exceeded, triggering auto-compaction", {
2283
2606
  usageRatio: budgetResult.usageRatio,
@@ -2291,7 +2614,7 @@ Current user's request: ${currentInput}`;
2291
2614
  summarizationModel: this.conversationMemoryConfig?.conversationMemory
2292
2615
  ?.summarizationModel,
2293
2616
  });
2294
- const compactionResult = await compactor.compact(conversationMessages, budgetResult.availableInputTokens, this.conversationMemoryConfig?.conversationMemory);
2617
+ const compactionResult = await compactor.compact(conversationMessages, budgetResult.availableInputTokens, this.conversationMemoryConfig?.conversationMemory, requestId);
2295
2618
  if (compactionResult.compacted) {
2296
2619
  const repairedResult = repairToolPairs(compactionResult.messages);
2297
2620
  conversationMessages = repairedResult.messages;
@@ -2313,6 +2636,16 @@ Current user's request: ${currentInput}`;
2313
2636
  customTools: this.getCustomTools(),
2314
2637
  executeTool: this.executeTool.bind(this),
2315
2638
  }, functionTag);
2639
+ logger.debug("[Observability] User input to LLM", {
2640
+ requestId,
2641
+ promptPreview: options.prompt?.substring(0, 200),
2642
+ promptLength: options.prompt?.length || 0,
2643
+ model: options.model,
2644
+ maxTokens: options.maxTokens,
2645
+ temperature: options.temperature,
2646
+ maxSteps: options.maxSteps,
2647
+ skipToolPromptInjection: options.skipToolPromptInjection,
2648
+ });
2316
2649
  const result = await provider.generate({
2317
2650
  ...options,
2318
2651
  systemPrompt: enhancedSystemPrompt,
@@ -2434,6 +2767,9 @@ Current user's request: ${currentInput}`;
2434
2767
  let lastError = null;
2435
2768
  // Try each provider in order
2436
2769
  for (const providerName of tryProviders) {
2770
+ if (options.abortSignal?.aborted) {
2771
+ throw new DOMException("The operation was aborted", "AbortError");
2772
+ }
2437
2773
  try {
2438
2774
  logger.debug(`[${functionTag}] Attempting provider: ${providerName}`);
2439
2775
  // Get conversation messages for context (use pre-compacted if provided)
@@ -2456,7 +2792,7 @@ Current user's request: ${currentInput}`;
2456
2792
  });
2457
2793
  if (budgetCheck.shouldCompact && this.conversationMemory) {
2458
2794
  const compactor = new ContextCompactor({ provider: providerName });
2459
- const compactionResult = await compactor.compact(conversationMessages, budgetCheck.availableInputTokens);
2795
+ const compactionResult = await compactor.compact(conversationMessages, budgetCheck.availableInputTokens, undefined, options.context?.requestId);
2460
2796
  if (compactionResult.compacted) {
2461
2797
  const repairedResult = repairToolPairs(compactionResult.messages);
2462
2798
  conversationMessages = repairedResult.messages;
@@ -3031,6 +3367,8 @@ Current user's request: ${currentInput}`;
3031
3367
  providerDetails,
3032
3368
  enableSummarization: enhancedOptions.enableSummarization,
3033
3369
  events: eventSequence.length > 0 ? eventSequence : undefined,
3370
+ requestId: enhancedOptions.context
3371
+ ?.requestId,
3034
3372
  });
3035
3373
  logger.debug("[NeuroLink.stream] Stored conversation turn with events", {
3036
3374
  sessionId,
@@ -3135,7 +3473,7 @@ Current user's request: ${currentInput}`;
3135
3473
  });
3136
3474
  if (streamBudget.shouldCompact && this.conversationMemory) {
3137
3475
  const compactor = new ContextCompactor({ provider: providerName });
3138
- const compactionResult = await compactor.compact(conversationMessages, streamBudget.availableInputTokens);
3476
+ const compactionResult = await compactor.compact(conversationMessages, streamBudget.availableInputTokens, undefined, options.context?.requestId);
3139
3477
  if (compactionResult.compacted) {
3140
3478
  const repairedResult = repairToolPairs(compactionResult.messages);
3141
3479
  conversationMessages = repairedResult.messages;
@@ -3269,6 +3607,9 @@ Current user's request: ${currentInput}`;
3269
3607
  startTimeStamp: new Date(startTime),
3270
3608
  providerDetails,
3271
3609
  enableSummarization: enhancedOptions?.enableSummarization,
3610
+ requestId: enhancedOptions?.context?.requestId ||
3611
+ options.context
3612
+ ?.requestId,
3272
3613
  });
3273
3614
  }
3274
3615
  catch (error) {
@@ -4864,7 +5205,13 @@ Current user's request: ${currentInput}`;
4864
5205
  const initId = `stats-init-${Date.now()}`;
4865
5206
  await this.initializeConversationMemoryForGeneration(initId, Date.now(), process.hrtime.bigint());
4866
5207
  if (!this.conversationMemory) {
4867
- throw new Error("Conversation memory is not enabled");
5208
+ throw new NeuroLinkError({
5209
+ code: ERROR_CODES.MISSING_CONFIGURATION,
5210
+ message: "Conversation memory is not enabled",
5211
+ category: ErrorCategory.VALIDATION,
5212
+ severity: ErrorSeverity.HIGH,
5213
+ retriable: false,
5214
+ });
4868
5215
  }
4869
5216
  return await this.conversationMemory.getStats();
4870
5217
  }
@@ -4878,10 +5225,23 @@ Current user's request: ${currentInput}`;
4878
5225
  const initId = `history-init-${Date.now()}`;
4879
5226
  await this.initializeConversationMemoryForGeneration(initId, Date.now(), process.hrtime.bigint());
4880
5227
  if (!this.conversationMemory) {
4881
- throw new Error("Conversation memory is not enabled");
5228
+ throw new NeuroLinkError({
5229
+ code: ERROR_CODES.MISSING_CONFIGURATION,
5230
+ message: "Conversation memory is not enabled",
5231
+ category: ErrorCategory.VALIDATION,
5232
+ severity: ErrorSeverity.HIGH,
5233
+ retriable: false,
5234
+ });
4882
5235
  }
4883
5236
  if (!sessionId || typeof sessionId !== "string") {
4884
- throw new Error("Session ID must be a non-empty string");
5237
+ throw new NeuroLinkError({
5238
+ code: ERROR_CODES.INVALID_PARAMETERS,
5239
+ message: "Session ID must be a non-empty string",
5240
+ category: ErrorCategory.VALIDATION,
5241
+ severity: ErrorSeverity.MEDIUM,
5242
+ retriable: false,
5243
+ context: { sessionId },
5244
+ });
4885
5245
  }
4886
5246
  try {
4887
5247
  // Use the existing buildContextMessages method to get the complete history
@@ -4910,7 +5270,13 @@ Current user's request: ${currentInput}`;
4910
5270
  const initId = `clear-session-init-${Date.now()}`;
4911
5271
  await this.initializeConversationMemoryForGeneration(initId, Date.now(), process.hrtime.bigint());
4912
5272
  if (!this.conversationMemory) {
4913
- throw new Error("Conversation memory is not enabled");
5273
+ throw new NeuroLinkError({
5274
+ code: ERROR_CODES.MISSING_CONFIGURATION,
5275
+ message: "Conversation memory is not enabled",
5276
+ category: ErrorCategory.VALIDATION,
5277
+ severity: ErrorSeverity.HIGH,
5278
+ retriable: false,
5279
+ });
4914
5280
  }
4915
5281
  return await this.conversationMemory.clearSession(sessionId);
4916
5282
  }
@@ -4922,7 +5288,13 @@ Current user's request: ${currentInput}`;
4922
5288
  const initId = `clear-all-init-${Date.now()}`;
4923
5289
  await this.initializeConversationMemoryForGeneration(initId, Date.now(), process.hrtime.bigint());
4924
5290
  if (!this.conversationMemory) {
4925
- throw new Error("Conversation memory is not enabled");
5291
+ throw new NeuroLinkError({
5292
+ code: ERROR_CODES.MISSING_CONFIGURATION,
5293
+ message: "Conversation memory is not enabled",
5294
+ category: ErrorCategory.VALIDATION,
5295
+ severity: ErrorSeverity.HIGH,
5296
+ retriable: false,
5297
+ });
4926
5298
  }
4927
5299
  await this.conversationMemory.clearAllSessions();
4928
5300
  }
@@ -4973,6 +5345,89 @@ Current user's request: ${currentInput}`;
4973
5345
  "RedisConversationMemoryManager";
4974
5346
  return !!(isRedisStorage && hasRedisConversationMemory);
4975
5347
  }
5348
+ /**
5349
+ * Get the raw messages array for a session.
5350
+ * Returns the full messages list without context filtering or summarization.
5351
+ * @param sessionId - The session ID to retrieve messages for
5352
+ * @returns Array of ChatMessage objects, or empty array if session doesn't exist
5353
+ */
5354
+ async getSessionMessages(sessionId, userId) {
5355
+ const initId = `get-msgs-init-${Date.now()}`;
5356
+ await this.initializeConversationMemoryForGeneration(initId, Date.now(), process.hrtime.bigint());
5357
+ if (!this.conversationMemory) {
5358
+ throw new NeuroLinkError({
5359
+ code: ERROR_CODES.MISSING_CONFIGURATION,
5360
+ message: "Conversation memory is not enabled",
5361
+ category: ErrorCategory.VALIDATION,
5362
+ severity: ErrorSeverity.HIGH,
5363
+ retriable: false,
5364
+ });
5365
+ }
5366
+ if (!sessionId || typeof sessionId !== "string") {
5367
+ throw new NeuroLinkError({
5368
+ code: ERROR_CODES.INVALID_PARAMETERS,
5369
+ message: "Session ID must be a non-empty string",
5370
+ category: ErrorCategory.VALIDATION,
5371
+ severity: ErrorSeverity.MEDIUM,
5372
+ retriable: false,
5373
+ context: { sessionId },
5374
+ });
5375
+ }
5376
+ return await this.conversationMemory.getSessionMessages(sessionId, userId);
5377
+ }
5378
+ /**
5379
+ * Replace the entire messages array for a session.
5380
+ * @param sessionId - The session ID to update
5381
+ * @param messages - The new messages array
5382
+ * @param userId - Optional user ID for scoped Redis key lookup
5383
+ */
5384
+ async setSessionMessages(sessionId, messages, userId) {
5385
+ const initId = `set-msgs-init-${Date.now()}`;
5386
+ await this.initializeConversationMemoryForGeneration(initId, Date.now(), process.hrtime.bigint());
5387
+ if (!this.conversationMemory) {
5388
+ throw new NeuroLinkError({
5389
+ code: ERROR_CODES.MISSING_CONFIGURATION,
5390
+ message: "Conversation memory is not enabled",
5391
+ category: ErrorCategory.VALIDATION,
5392
+ severity: ErrorSeverity.HIGH,
5393
+ retriable: false,
5394
+ });
5395
+ }
5396
+ if (!sessionId || typeof sessionId !== "string") {
5397
+ throw new NeuroLinkError({
5398
+ code: ERROR_CODES.INVALID_PARAMETERS,
5399
+ message: "Session ID must be a non-empty string",
5400
+ category: ErrorCategory.VALIDATION,
5401
+ severity: ErrorSeverity.MEDIUM,
5402
+ retriable: false,
5403
+ context: { sessionId },
5404
+ });
5405
+ }
5406
+ await this.conversationMemory.setSessionMessages(sessionId, messages, userId);
5407
+ }
5408
+ /**
5409
+ * Modify the last assistant message in a session using a transformer function.
5410
+ * Convenience wrapper around getSessionMessages/setSessionMessages.
5411
+ * @param sessionId - The session ID to modify
5412
+ * @param transformer - Function that receives the last assistant message content and returns the modified content
5413
+ * @param userId - Optional user ID for scoped Redis key lookup
5414
+ * @returns true if a message was modified, false if no assistant message was found
5415
+ */
5416
+ async modifyLastAssistantMessage(sessionId, transformer, userId) {
5417
+ const messages = await this.getSessionMessages(sessionId, userId);
5418
+ // Find the last assistant message (searching from the end)
5419
+ for (let i = messages.length - 1; i >= 0; i--) {
5420
+ if (messages[i].role === "assistant") {
5421
+ messages[i] = {
5422
+ ...messages[i],
5423
+ content: transformer(messages[i].content),
5424
+ };
5425
+ await this.setSessionMessages(sessionId, messages, userId);
5426
+ return true;
5427
+ }
5428
+ }
5429
+ return false;
5430
+ }
4976
5431
  // ===== EXTERNAL MCP SERVER METHODS =====
4977
5432
  /**
4978
5433
  * Add an external MCP server