@juspay/neurolink 8.19.0 → 8.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.
Files changed (57) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/adapters/providerImageAdapter.d.ts +12 -0
  3. package/dist/adapters/providerImageAdapter.js +30 -3
  4. package/dist/cli/loop/optionsSchema.js +4 -0
  5. package/dist/config/conversationMemory.d.ts +17 -1
  6. package/dist/config/conversationMemory.js +37 -10
  7. package/dist/core/baseProvider.js +23 -13
  8. package/dist/core/conversationMemoryFactory.js +0 -3
  9. package/dist/core/conversationMemoryInitializer.js +1 -9
  10. package/dist/core/conversationMemoryManager.d.ts +31 -8
  11. package/dist/core/conversationMemoryManager.js +174 -80
  12. package/dist/core/modules/GenerationHandler.d.ts +5 -0
  13. package/dist/core/modules/GenerationHandler.js +56 -9
  14. package/dist/core/redisConversationMemoryManager.d.ts +28 -13
  15. package/dist/core/redisConversationMemoryManager.js +211 -121
  16. package/dist/lib/adapters/providerImageAdapter.d.ts +12 -0
  17. package/dist/lib/adapters/providerImageAdapter.js +30 -3
  18. package/dist/lib/config/conversationMemory.d.ts +17 -1
  19. package/dist/lib/config/conversationMemory.js +37 -10
  20. package/dist/lib/core/baseProvider.js +23 -13
  21. package/dist/lib/core/conversationMemoryFactory.js +0 -3
  22. package/dist/lib/core/conversationMemoryInitializer.js +1 -9
  23. package/dist/lib/core/conversationMemoryManager.d.ts +31 -8
  24. package/dist/lib/core/conversationMemoryManager.js +174 -80
  25. package/dist/lib/core/modules/GenerationHandler.d.ts +5 -0
  26. package/dist/lib/core/modules/GenerationHandler.js +56 -9
  27. package/dist/lib/core/redisConversationMemoryManager.d.ts +28 -13
  28. package/dist/lib/core/redisConversationMemoryManager.js +211 -121
  29. package/dist/lib/mcp/servers/agent/directToolsServer.js +5 -0
  30. package/dist/lib/mcp/toolRegistry.js +5 -0
  31. package/dist/lib/neurolink.js +29 -22
  32. package/dist/lib/types/conversation.d.ts +58 -9
  33. package/dist/lib/types/generateTypes.d.ts +1 -0
  34. package/dist/lib/types/sdkTypes.d.ts +1 -1
  35. package/dist/lib/types/streamTypes.d.ts +1 -0
  36. package/dist/lib/utils/conversationMemory.d.ts +43 -1
  37. package/dist/lib/utils/conversationMemory.js +181 -5
  38. package/dist/lib/utils/conversationMemoryUtils.js +16 -1
  39. package/dist/lib/utils/fileDetector.d.ts +25 -0
  40. package/dist/lib/utils/fileDetector.js +433 -10
  41. package/dist/lib/utils/messageBuilder.js +6 -2
  42. package/dist/lib/utils/redis.js +0 -5
  43. package/dist/mcp/servers/agent/directToolsServer.js +5 -0
  44. package/dist/mcp/toolRegistry.js +5 -0
  45. package/dist/neurolink.js +29 -22
  46. package/dist/types/conversation.d.ts +58 -9
  47. package/dist/types/generateTypes.d.ts +1 -0
  48. package/dist/types/sdkTypes.d.ts +1 -1
  49. package/dist/types/streamTypes.d.ts +1 -0
  50. package/dist/utils/conversationMemory.d.ts +43 -1
  51. package/dist/utils/conversationMemory.js +181 -5
  52. package/dist/utils/conversationMemoryUtils.js +16 -1
  53. package/dist/utils/fileDetector.d.ts +25 -0
  54. package/dist/utils/fileDetector.js +433 -10
  55. package/dist/utils/messageBuilder.js +6 -2
  56. package/dist/utils/redis.js +0 -5
  57. package/package.json +1 -1
@@ -4,10 +4,12 @@
4
4
  */
5
5
  import { randomUUID } from "crypto";
6
6
  import { ConversationMemoryError } from "../types/conversation.js";
7
- import { MESSAGES_PER_TURN } from "../config/conversationMemory.js";
7
+ import { MESSAGES_PER_TURN, RECENT_MESSAGES_RATIO, } from "../config/conversationMemory.js";
8
8
  import { logger } from "../utils/logger.js";
9
9
  import { NeuroLink } from "../neurolink.js";
10
10
  import { createRedisClient, getSessionKey, getUserSessionsKey, getNormalizedConfig, serializeConversation, deserializeConversation, scanKeys, } from "../utils/redis.js";
11
+ import { TokenUtils } from "../constants/tokens.js";
12
+ import { buildContextFromPointer, getEffectiveTokenThreshold, generateSummary, } from "../utils/conversationMemory.js";
11
13
  /**
12
14
  * Redis-based implementation of the ConversationMemoryManager
13
15
  * Uses the same interface but stores data in Redis
@@ -27,6 +29,11 @@ export class RedisConversationMemoryManager {
27
29
  * Key format: "${sessionId}:${userId}"
28
30
  */
29
31
  titleGenerationInProgress = new Set();
32
+ /**
33
+ * Track sessions currently being summarized to prevent race conditions
34
+ * Key format: "${sessionId}:${userId}"
35
+ */
36
+ summarizationInProgress = new Set();
30
37
  constructor(config, redisConfig = {}) {
31
38
  this.config = config;
32
39
  this.redisConfig = getNormalizedConfig(redisConfig);
@@ -138,25 +145,12 @@ export class RedisConversationMemoryManager {
138
145
  return false;
139
146
  }
140
147
  }
141
- /**
142
- * Generate next message ID for a conversation
143
- */
144
- generateMessageId(conversation) {
145
- const currentCount = conversation?.messages?.length || 0;
146
- return `msg_${currentCount + 1}`;
147
- }
148
148
  /**
149
149
  * Generate current timestamp in ISO format
150
150
  */
151
151
  generateTimestamp() {
152
152
  return new Date().toISOString();
153
153
  }
154
- /**
155
- * Generate a unique conversation ID using UUID v4
156
- */
157
- generateUniqueId() {
158
- return randomUUID();
159
- }
160
154
  /**
161
155
  * Store tool execution data for a session (temporarily to avoid race conditions)
162
156
  */
@@ -224,49 +218,31 @@ export class RedisConversationMemoryManager {
224
218
  /**
225
219
  * Store a conversation turn for a session
226
220
  */
227
- async storeConversationTurn(sessionId, userId, userMessage, aiResponse, startTimeStamp) {
221
+ async storeConversationTurn(options) {
228
222
  logger.debug("[RedisConversationMemoryManager] Storing conversation turn", {
229
- sessionId,
230
- userId,
231
- userMessageLength: userMessage.length,
232
- aiResponseLength: aiResponse.length,
223
+ sessionId: options.sessionId,
224
+ userId: options.userId,
233
225
  });
234
226
  await this.ensureInitialized();
235
227
  try {
236
228
  if (!this.redisClient) {
237
229
  throw new Error("Redis client not initialized");
238
230
  }
239
- // Generate Redis key
240
- const redisKey = getSessionKey(this.redisConfig, sessionId, userId);
241
- // Get existing conversation object
231
+ const redisKey = getSessionKey(this.redisConfig, options.sessionId, options.userId);
242
232
  const conversationData = await this.redisClient.get(redisKey);
243
233
  let conversation = deserializeConversation(conversationData);
244
234
  const currentTime = new Date().toISOString();
245
- const normalizedUserId = userId || "randomUser";
246
- // If no existing conversation, create a new one
235
+ const normalizedUserId = options.userId || "randomUser";
247
236
  if (!conversation) {
248
- // Generate title asynchronously in the background (non-blocking)
249
- const titleGenerationKey = `${sessionId}:${normalizedUserId}`;
237
+ const titleGenerationKey = `${options.sessionId}:${normalizedUserId}`;
250
238
  setImmediate(async () => {
251
- // Check if title generation is already in progress for this session
252
239
  if (this.titleGenerationInProgress.has(titleGenerationKey)) {
253
- logger.debug("[RedisConversationMemoryManager] Title generation already in progress, skipping", {
254
- sessionId,
255
- userId: normalizedUserId,
256
- titleGenerationKey,
257
- });
258
240
  return;
259
241
  }
260
- // Mark title generation as in progress
261
242
  this.titleGenerationInProgress.add(titleGenerationKey);
262
243
  try {
263
- const title = await this.generateConversationTitle(userMessage);
264
- logger.info("[RedisConversationMemoryManager] Successfully generated conversation title", {
265
- sessionId,
266
- userId: normalizedUserId,
267
- title,
268
- });
269
- const updatedRedisKey = getSessionKey(this.redisConfig, sessionId, userId || undefined);
244
+ const title = await this.generateConversationTitle(options.userMessage);
245
+ const updatedRedisKey = getSessionKey(this.redisConfig, options.sessionId, options.userId || undefined);
270
246
  const updatedConversationData = await this.redisClient?.get(updatedRedisKey);
271
247
  const updatedConversation = deserializeConversation(updatedConversationData || null);
272
248
  if (updatedConversation) {
@@ -281,7 +257,7 @@ export class RedisConversationMemoryManager {
281
257
  }
282
258
  catch (titleError) {
283
259
  logger.warn("[RedisConversationMemoryManager] Failed to generate conversation title in background", {
284
- sessionId,
260
+ sessionId: options.sessionId,
285
261
  userId: normalizedUserId,
286
262
  error: titleError instanceof Error
287
263
  ? titleError.message
@@ -289,136 +265,243 @@ export class RedisConversationMemoryManager {
289
265
  });
290
266
  }
291
267
  finally {
292
- // Always remove from tracking set when done (success or failure)
293
268
  this.titleGenerationInProgress.delete(titleGenerationKey);
294
- logger.debug("[RedisConversationMemoryManager] Title generation completed, removed from tracking", {
295
- sessionId,
296
- userId: normalizedUserId,
297
- titleGenerationKey,
298
- remainingInProgress: this.titleGenerationInProgress.size,
299
- });
300
269
  }
301
270
  });
302
271
  conversation = {
303
- id: this.generateUniqueId(), // Generate unique UUID v4 for conversation
272
+ id: randomUUID(),
304
273
  title: "New Conversation", // Temporary title until generated
305
- sessionId,
274
+ sessionId: options.sessionId,
306
275
  userId: normalizedUserId,
307
- createdAt: startTimeStamp?.toISOString() || currentTime,
308
- updatedAt: startTimeStamp?.toISOString() || currentTime,
276
+ createdAt: options.startTimeStamp?.toISOString() || currentTime,
277
+ updatedAt: options.startTimeStamp?.toISOString() || currentTime,
309
278
  messages: [],
310
279
  };
311
280
  }
312
281
  else {
313
- // Update existing conversation timestamp
314
282
  conversation.updatedAt = currentTime;
315
283
  }
316
- logger.info("[RedisConversationMemoryManager] Processing conversation", {
317
- isNewConversation: !conversationData,
318
- messageCount: conversation.messages.length,
319
- sessionId: conversation.sessionId,
320
- userId: conversation.userId,
321
- });
322
- // Add new messages to conversation history with new format
284
+ const tokenThreshold = options.providerDetails
285
+ ? getEffectiveTokenThreshold(options.providerDetails.provider, options.providerDetails.model, this.config.tokenThreshold, conversation.tokenThreshold)
286
+ : this.config.tokenThreshold || 50000;
323
287
  const userMsg = {
324
- id: this.generateMessageId(conversation),
325
- timestamp: startTimeStamp?.toISOString() || this.generateTimestamp(),
288
+ id: randomUUID(),
289
+ timestamp: options.startTimeStamp?.toISOString() || this.generateTimestamp(),
326
290
  role: "user",
327
- content: userMessage,
291
+ content: options.userMessage,
328
292
  };
329
293
  conversation.messages.push(userMsg);
330
- await this.flushPendingToolData(conversation, sessionId, normalizedUserId);
294
+ await this.flushPendingToolData(conversation, options.sessionId, normalizedUserId);
331
295
  const assistantMsg = {
332
- id: this.generateMessageId(conversation),
296
+ id: randomUUID(),
333
297
  timestamp: this.generateTimestamp(),
334
298
  role: "assistant",
335
- content: aiResponse,
299
+ content: options.aiResponse,
336
300
  };
337
301
  conversation.messages.push(assistantMsg);
338
302
  logger.info("[RedisConversationMemoryManager] Added new messages", {
339
- newMessageCount: conversation.messages.length,
340
- latestMessages: [
341
- {
342
- role: conversation.messages[conversation.messages.length - 2]?.role,
343
- contentLength: conversation.messages[conversation.messages.length - 2]?.content
344
- .length,
345
- },
346
- {
347
- role: conversation.messages[conversation.messages.length - 1]?.role,
348
- contentLength: conversation.messages[conversation.messages.length - 1]?.content
349
- .length,
350
- },
351
- ],
303
+ sessionId: conversation.sessionId,
304
+ userId: conversation.userId,
352
305
  });
353
- // Save updated conversation object
306
+ // Use per-request enableSummarization with higher priority than instance config
307
+ const shouldSummarize = options.enableSummarization !== undefined
308
+ ? options.enableSummarization
309
+ : this.config.enableSummarization;
310
+ if (shouldSummarize) {
311
+ const normalizedUserId = options.userId || "randomUser";
312
+ const summarizationKey = `${options.sessionId}:${normalizedUserId}`;
313
+ // Only trigger summarization if not already in progress for this session
314
+ if (!this.summarizationInProgress.has(summarizationKey)) {
315
+ setImmediate(async () => {
316
+ try {
317
+ await this.checkAndSummarize(conversation, tokenThreshold, options.sessionId, options.userId);
318
+ }
319
+ catch (error) {
320
+ logger.error("Background summarization failed", {
321
+ sessionId: conversation.sessionId,
322
+ error: error instanceof Error ? error.message : String(error),
323
+ });
324
+ }
325
+ });
326
+ }
327
+ else {
328
+ logger.debug("[RedisConversationMemoryManager] Summarization already in progress, skipping", {
329
+ sessionId: options.sessionId,
330
+ userId: normalizedUserId,
331
+ });
332
+ }
333
+ }
354
334
  const serializedData = serializeConversation(conversation);
355
- logger.debug("[RedisConversationMemoryManager] Saving conversation to Redis", {
356
- redisKey,
357
- messageCount: conversation.messages.length,
358
- serializedDataLength: serializedData.length,
359
- title: conversation.title,
360
- });
361
- logger.info("Storing conversation data to Redis", {
362
- sessionId,
363
- dataLength: serializedData.length,
364
- messageCount: conversation.messages.length,
365
- });
366
335
  await this.redisClient.set(redisKey, serializedData);
367
- // Set TTL if configured
368
336
  if (this.redisConfig.ttl > 0) {
369
- logger.debug("[RedisConversationMemoryManager] Setting Redis TTL", {
370
- redisKey,
371
- ttl: this.redisConfig.ttl,
372
- });
373
337
  await this.redisClient.expire(redisKey, this.redisConfig.ttl);
374
338
  }
375
- // Add session to user's session set
376
- if (userId) {
377
- await this.addUserSession(userId, sessionId);
339
+ if (options.userId) {
340
+ await this.addUserSession(options.userId, options.sessionId);
378
341
  }
379
342
  logger.debug("[RedisConversationMemoryManager] Successfully stored conversation turn", {
380
- sessionId,
343
+ sessionId: options.sessionId,
381
344
  totalMessages: conversation.messages.length,
382
345
  title: conversation.title,
383
346
  });
384
347
  }
385
348
  catch (error) {
386
- throw new ConversationMemoryError(`Failed to store conversation turn in Redis for session ${sessionId}`, "STORAGE_ERROR", {
349
+ throw new ConversationMemoryError(`Failed to store conversation turn in Redis for session ${options.sessionId}`, "STORAGE_ERROR", {
350
+ sessionId: options.sessionId,
351
+ error: error instanceof Error ? error.message : String(error),
352
+ });
353
+ }
354
+ }
355
+ /**
356
+ * Check if summarization is needed based on token count
357
+ */
358
+ async checkAndSummarize(conversation, threshold, sessionId, userId) {
359
+ const normalizedUserId = userId || "randomUser";
360
+ const summarizationKey = `${sessionId}:${normalizedUserId}`;
361
+ // Acquire lock - if already in progress, skip
362
+ if (this.summarizationInProgress.has(summarizationKey)) {
363
+ logger.debug("[RedisConversationMemoryManager] Summarization already in progress, skipping", {
387
364
  sessionId,
365
+ userId: normalizedUserId,
366
+ });
367
+ return;
368
+ }
369
+ this.summarizationInProgress.add(summarizationKey);
370
+ try {
371
+ const session = {
372
+ sessionId: conversation.sessionId,
373
+ userId: conversation.userId,
374
+ messages: conversation.messages,
375
+ summarizedUpToMessageId: conversation.summarizedUpToMessageId,
376
+ summarizedMessage: conversation.summarizedMessage,
377
+ tokenThreshold: conversation.tokenThreshold,
378
+ lastTokenCount: conversation.lastTokenCount,
379
+ lastCountedAt: conversation.lastCountedAt,
380
+ createdAt: new Date(conversation.createdAt).getTime(),
381
+ lastActivity: new Date(conversation.updatedAt).getTime(),
382
+ };
383
+ const contextMessages = buildContextFromPointer(session);
384
+ const tokenCount = this.estimateTokens(contextMessages);
385
+ conversation.lastTokenCount = tokenCount;
386
+ conversation.lastCountedAt = Date.now();
387
+ if (tokenCount >= threshold) {
388
+ await this.summarizeSessionTokenBased(conversation, threshold, sessionId, userId);
389
+ }
390
+ }
391
+ catch (error) {
392
+ logger.error("Token counting or summarization failed", {
393
+ sessionId: conversation.sessionId,
388
394
  error: error instanceof Error ? error.message : String(error),
389
395
  });
390
396
  }
397
+ finally {
398
+ // Release lock when done
399
+ this.summarizationInProgress.delete(summarizationKey);
400
+ }
401
+ }
402
+ /**
403
+ * Estimate total tokens for a list of messages
404
+ */
405
+ estimateTokens(messages) {
406
+ return messages.reduce((total, msg) => {
407
+ return total + TokenUtils.estimateTokenCount(msg.content);
408
+ }, 0);
391
409
  }
392
410
  /**
393
- * Build context messages for AI prompt injection
411
+ * Token-based summarization (pointer-based, non-destructive)
394
412
  */
395
- async buildContextMessages(sessionId, userId) {
413
+ async summarizeSessionTokenBased(conversation, threshold, sessionId, userId) {
414
+ const startIndex = conversation.summarizedUpToMessageId
415
+ ? conversation.messages.findIndex((m) => m.id === conversation.summarizedUpToMessageId) + 1
416
+ : 0;
417
+ const recentMessages = conversation.messages.slice(startIndex);
418
+ if (recentMessages.length === 0) {
419
+ return;
420
+ }
421
+ // We only want to include user, assistant, and system messages in summarization
422
+ const filteredRecentMessages = recentMessages.filter((msg) => msg.role !== "tool_call" && msg.role !== "tool_result");
423
+ const targetRecentTokens = threshold * RECENT_MESSAGES_RATIO;
424
+ const splitIndex = await this.findSplitIndexByTokens(filteredRecentMessages, targetRecentTokens);
425
+ const messagesToSummarize = filteredRecentMessages.slice(0, splitIndex);
426
+ if (messagesToSummarize.length === 0) {
427
+ return;
428
+ }
429
+ const summary = await generateSummary(messagesToSummarize, this.config, "[RedisConversationMemoryManager]", conversation.summarizedMessage);
430
+ if (!summary) {
431
+ logger.warn(`[RedisConversationMemoryManager] Summary generation failed for session ${conversation.sessionId}`);
432
+ return;
433
+ }
434
+ const lastSummarized = messagesToSummarize[messagesToSummarize.length - 1];
435
+ conversation.summarizedUpToMessageId = lastSummarized.id;
436
+ conversation.summarizedMessage = summary;
437
+ if (this.redisClient) {
438
+ const redisKey = getSessionKey(this.redisConfig, sessionId, userId);
439
+ const serializedData = serializeConversation(conversation);
440
+ await this.redisClient.set(redisKey, serializedData);
441
+ if (this.redisConfig.ttl > 0) {
442
+ await this.redisClient.expire(redisKey, this.redisConfig.ttl);
443
+ }
444
+ }
445
+ }
446
+ /**
447
+ * Find split index to keep recent messages within target token count
448
+ */
449
+ async findSplitIndexByTokens(messages, targetRecentTokens) {
450
+ let recentTokens = 0;
451
+ let splitIndex = messages.length;
452
+ for (let i = messages.length - 1; i >= 0; i--) {
453
+ const msgTokens = TokenUtils.estimateTokenCount(messages[i].content);
454
+ if (recentTokens + msgTokens > targetRecentTokens) {
455
+ splitIndex = i + 1;
456
+ break;
457
+ }
458
+ recentTokens += msgTokens;
459
+ }
460
+ // Ensure we're summarizing at least something
461
+ return Math.max(1, splitIndex);
462
+ }
463
+ /**
464
+ * Build context messages for AI prompt injection (TOKEN-BASED)
465
+ * Returns messages from pointer onwards (or all if no pointer)
466
+ * Filters out tool_call and tool_result messages when summarization is enabled
467
+ */
468
+ async buildContextMessages(sessionId, userId, enableSummarization) {
396
469
  logger.info("[RedisConversationMemoryManager] Building context messages", {
397
470
  sessionId,
398
471
  userId,
399
472
  method: "buildContextMessages",
400
473
  });
401
- const messages = await this.getUserSessionHistory(userId || "randomUser", sessionId);
402
- if (!messages) {
403
- logger.info("[RedisConversationMemoryManager] No context messages found", {
404
- sessionId,
405
- userId,
406
- });
474
+ const redisKey = getSessionKey(this.redisConfig, sessionId, userId);
475
+ const conversationData = await this.redisClient?.get(redisKey);
476
+ const conversation = deserializeConversation(conversationData || null);
477
+ if (!conversation) {
407
478
  return [];
408
479
  }
409
- logger.info("[RedisConversationMemoryManager] Retrieved messages", {
410
- messageCount: messages.length,
411
- hasMessages: messages.length > 0,
412
- });
480
+ const session = {
481
+ sessionId: conversation.sessionId,
482
+ userId: conversation.userId,
483
+ messages: conversation.messages,
484
+ summarizedUpToMessageId: conversation.summarizedUpToMessageId,
485
+ summarizedMessage: conversation.summarizedMessage,
486
+ tokenThreshold: conversation.tokenThreshold,
487
+ lastTokenCount: conversation.lastTokenCount,
488
+ lastCountedAt: conversation.lastCountedAt,
489
+ createdAt: new Date(conversation.createdAt).getTime(),
490
+ lastActivity: new Date(conversation.updatedAt).getTime(),
491
+ };
492
+ const contextMessages = buildContextFromPointer(session);
493
+ const isSummarizationEnabled = enableSummarization !== undefined
494
+ ? enableSummarization
495
+ : this.config.enableSummarization === true;
496
+ let finalMessages = contextMessages;
497
+ if (isSummarizationEnabled) {
498
+ finalMessages = contextMessages.filter((msg) => msg.role !== "tool_call" && msg.role !== "tool_result");
499
+ }
413
500
  logger.info("[RedisConversationMemoryManager] Retrieved context messages", {
414
501
  sessionId,
415
502
  userId,
416
- messageCount: messages.length,
417
- messageRoles: messages.map((m) => m.role),
418
- firstMessagePreview: messages[0]?.content?.substring(0, 50),
419
- lastMessagePreview: messages[messages.length - 1]?.content?.substring(0, 50),
420
503
  });
421
- return messages;
504
+ return finalMessages;
422
505
  }
423
506
  /**
424
507
  * Get session metadata for a specific user session (optimized for listing)
@@ -649,10 +732,17 @@ User message: "${userMessage}`;
649
732
  /**
650
733
  * Create summary system message
651
734
  */
652
- createSummarySystemMessage(content) {
735
+ createSummarySystemMessage(content, summarizesFrom, summarizesTo) {
653
736
  return {
737
+ id: `summary-${randomUUID()}`,
654
738
  role: "system",
655
739
  content: `Summary of previous conversation turns:\n\n${content}`,
740
+ timestamp: new Date().toISOString(),
741
+ metadata: {
742
+ isSummary: true,
743
+ summarizesFrom,
744
+ summarizesTo,
745
+ },
656
746
  };
657
747
  }
658
748
  /**
@@ -876,7 +966,7 @@ User message: "${userMessage}`;
876
966
  // Store in mapping for tool results
877
967
  toolCallMap.set(toolCallId, toolName);
878
968
  const toolCallMessage = {
879
- id: this.generateMessageId(conversation),
969
+ id: randomUUID(),
880
970
  timestamp: toolCall.timestamp?.toISOString() || this.generateTimestamp(),
881
971
  role: "tool_call",
882
972
  content: "", // Can be empty for tool calls
@@ -893,7 +983,7 @@ User message: "${userMessage}`;
893
983
  const toolCallId = String(toolResult.toolCallId || toolResult.id || "unknown");
894
984
  const toolName = toolCallMap.get(toolCallId) || "unknown";
895
985
  const toolResultMessage = {
896
- id: this.generateMessageId(conversation),
986
+ id: randomUUID(),
897
987
  timestamp: toolResult.timestamp?.toISOString() || this.generateTimestamp(),
898
988
  role: "tool_result",
899
989
  content: "", // Can be empty for tool results
@@ -60,4 +60,16 @@ export declare class ProviderImageAdapter {
60
60
  * Get all vision-capable providers
61
61
  */
62
62
  static getVisionProviders(): string[];
63
+ /**
64
+ * Count total "images" in a message (actual images + PDF pages)
65
+ * PDF pages count toward image limits for providers
66
+ */
67
+ static countImagesInMessage(images: Array<Buffer | string>, pdfPages?: number | null): number;
68
+ /**
69
+ * Extract page count from PDF metadata array
70
+ * Returns total pages across all PDFs
71
+ */
72
+ static countImagesInPages(pdfMetadataArray: Array<{
73
+ pageCount?: number | null;
74
+ }> | undefined): number;
63
75
  }
@@ -416,13 +416,19 @@ export class ProviderImageAdapter {
416
416
  adaptedPayload = this.formatForOpenAI(text, images);
417
417
  break;
418
418
  case "litellm":
419
- adaptedPayload = this.formatForOpenAI(text, images);
419
+ // LiteLLM uses same format as OpenAI but validate with litellm provider name
420
+ this.validateImageCount(images.length, "litellm");
421
+ adaptedPayload = this.formatForOpenAI(text, images, true);
420
422
  break;
421
423
  case "mistral":
422
- adaptedPayload = this.formatForOpenAI(text, images);
424
+ // Mistral uses same format as OpenAI but validate with mistral provider name
425
+ this.validateImageCount(images.length, "mistral");
426
+ adaptedPayload = this.formatForOpenAI(text, images, true);
423
427
  break;
424
428
  case "bedrock":
425
- adaptedPayload = this.formatForAnthropic(text, images);
429
+ // Bedrock uses same format as Anthropic but validate with bedrock provider name
430
+ this.validateImageCount(images.length, "bedrock");
431
+ adaptedPayload = this.formatForAnthropic(text, images, true);
426
432
  break;
427
433
  default:
428
434
  throw new Error(`Vision not supported for provider: ${provider}`);
@@ -666,5 +672,26 @@ export class ProviderImageAdapter {
666
672
  static getVisionProviders() {
667
673
  return Object.keys(VISION_CAPABILITIES);
668
674
  }
675
+ /**
676
+ * Count total "images" in a message (actual images + PDF pages)
677
+ * PDF pages count toward image limits for providers
678
+ */
679
+ static countImagesInMessage(images, pdfPages) {
680
+ const imageCount = images?.length || 0;
681
+ const pageCount = pdfPages ?? 0;
682
+ return imageCount + pageCount;
683
+ }
684
+ /**
685
+ * Extract page count from PDF metadata array
686
+ * Returns total pages across all PDFs
687
+ */
688
+ static countImagesInPages(pdfMetadataArray) {
689
+ if (!pdfMetadataArray || pdfMetadataArray.length === 0) {
690
+ return 0;
691
+ }
692
+ return pdfMetadataArray.reduce((total, pdf) => {
693
+ return total + (pdf.pageCount ?? 0);
694
+ }, 0);
695
+ }
669
696
  }
670
697
  //# sourceMappingURL=providerImageAdapter.js.map
@@ -20,12 +20,28 @@ export declare const MESSAGES_PER_TURN = 2;
20
20
  * Used to enhance system prompts when conversation history exists
21
21
  */
22
22
  export declare const CONVERSATION_INSTRUCTIONS = "\n\nIMPORTANT: You are continuing an ongoing conversation. The previous messages in this conversation contain important context including:\n- Names, personal information, and preferences shared by the user\n- Projects, tasks, and topics discussed previously \n- Any decisions, agreements, or conclusions reached\n\nAlways reference and build upon this conversation history when relevant. If the user asks about information mentioned earlier in the conversation, refer to those previous messages to provide accurate, contextual responses.";
23
+ /**
24
+ * Percentage of model context window to use for conversation memory threshold
25
+ * Default: 80% of model's context window
26
+ */
27
+ export declare const MEMORY_THRESHOLD_PERCENTAGE = 0.8;
28
+ /**
29
+ * Fallback token threshold if model context unknown
30
+ */
31
+ export declare const DEFAULT_FALLBACK_THRESHOLD = 50000;
32
+ /**
33
+ * Ratio of threshold to keep as recent unsummarized messages
34
+ * When summarization triggers, this percentage of tokens from the end
35
+ * are preserved as detailed messages, while older content gets summarized.
36
+ */
37
+ export declare const RECENT_MESSAGES_RATIO = 0.3;
23
38
  /**
24
39
  * Structured output instructions for JSON/structured output mode
25
40
  * Used to ensure AI providers output only valid JSON without conversational filler
26
41
  * This addresses the issue where models add text like "Excellent!" before JSON output
42
+ * and the case where tools are used but final output must still be pure JSON
27
43
  */
28
- export declare const STRUCTURED_OUTPUT_INSTRUCTIONS = "\n\nSTRUCTURED OUTPUT REQUIREMENT:\nYou MUST respond with ONLY a valid JSON object that matches the provided schema.\n- Do NOT include any text before the JSON (no greetings, acknowledgments, or preamble like \"Excellent!\", \"Sure!\", \"Here is the result:\", etc.)\n- Do NOT include any text after the JSON (no explanations, summaries, or follow-up comments)\n- Do NOT wrap the JSON in markdown code blocks\n- Output ONLY the raw JSON object, starting with { and ending with }\n- Ensure the JSON is valid and parseable";
44
+ export declare const STRUCTURED_OUTPUT_INSTRUCTIONS = "\nOutput ONLY valid JSON. No markdown, text, or decorations\u2014ever.\n\nFORBIDDEN: markdown code blocks, text before/after JSON, explanations, preambles, summaries, conversational text about tools.\n\nREQUIRED: response starts with { and ends with }, valid JSON only, no additional characters.\n\nIF YOU CALLED TOOLS: Incorporate data directly into the JSON structure. Do NOT explain what you did.\n\nWRONG: ```json\n{\"field\": \"value\"}\n```\nWRONG: Based on the data, here's the result: {\"field\": \"value\"}\nCORRECT: {\"field\": \"value\"}\n\nYour entire response = raw JSON object. Nothing else.";
29
45
  /**
30
46
  * Get default configuration values for conversation memory
31
47
  * Reads environment variables when called (not at module load time)
@@ -26,20 +26,43 @@ IMPORTANT: You are continuing an ongoing conversation. The previous messages in
26
26
  - Any decisions, agreements, or conclusions reached
27
27
 
28
28
  Always reference and build upon this conversation history when relevant. If the user asks about information mentioned earlier in the conversation, refer to those previous messages to provide accurate, contextual responses.`;
29
+ /**
30
+ * Percentage of model context window to use for conversation memory threshold
31
+ * Default: 80% of model's context window
32
+ */
33
+ export const MEMORY_THRESHOLD_PERCENTAGE = 0.8;
34
+ /**
35
+ * Fallback token threshold if model context unknown
36
+ */
37
+ export const DEFAULT_FALLBACK_THRESHOLD = 50000;
38
+ /**
39
+ * Ratio of threshold to keep as recent unsummarized messages
40
+ * When summarization triggers, this percentage of tokens from the end
41
+ * are preserved as detailed messages, while older content gets summarized.
42
+ */
43
+ export const RECENT_MESSAGES_RATIO = 0.3;
29
44
  /**
30
45
  * Structured output instructions for JSON/structured output mode
31
46
  * Used to ensure AI providers output only valid JSON without conversational filler
32
47
  * This addresses the issue where models add text like "Excellent!" before JSON output
48
+ * and the case where tools are used but final output must still be pure JSON
33
49
  */
34
50
  export const STRUCTURED_OUTPUT_INSTRUCTIONS = `
51
+ Output ONLY valid JSON. No markdown, text, or decorations—ever.
52
+
53
+ FORBIDDEN: markdown code blocks, text before/after JSON, explanations, preambles, summaries, conversational text about tools.
35
54
 
36
- STRUCTURED OUTPUT REQUIREMENT:
37
- You MUST respond with ONLY a valid JSON object that matches the provided schema.
38
- - Do NOT include any text before the JSON (no greetings, acknowledgments, or preamble like "Excellent!", "Sure!", "Here is the result:", etc.)
39
- - Do NOT include any text after the JSON (no explanations, summaries, or follow-up comments)
40
- - Do NOT wrap the JSON in markdown code blocks
41
- - Output ONLY the raw JSON object, starting with { and ending with }
42
- - Ensure the JSON is valid and parseable`;
55
+ REQUIRED: response starts with { and ends with }, valid JSON only, no additional characters.
56
+
57
+ IF YOU CALLED TOOLS: Incorporate data directly into the JSON structure. Do NOT explain what you did.
58
+
59
+ WRONG: \`\`\`json
60
+ {"field": "value"}
61
+ \`\`\`
62
+ WRONG: Based on the data, here's the result: {"field": "value"}
63
+ CORRECT: {"field": "value"}
64
+
65
+ Your entire response = raw JSON object. Nothing else.`;
43
66
  /**
44
67
  * Get default configuration values for conversation memory
45
68
  * Reads environment variables when called (not at module load time)
@@ -48,13 +71,17 @@ export function getConversationMemoryDefaults() {
48
71
  return {
49
72
  enabled: process.env.NEUROLINK_MEMORY_ENABLED === "true",
50
73
  maxSessions: Number(process.env.NEUROLINK_MEMORY_MAX_SESSIONS) || DEFAULT_MAX_SESSIONS,
74
+ enableSummarization: process.env.NEUROLINK_SUMMARIZATION_ENABLED !== "false",
75
+ tokenThreshold: process.env.NEUROLINK_TOKEN_THRESHOLD
76
+ ? Number(process.env.NEUROLINK_TOKEN_THRESHOLD)
77
+ : undefined,
78
+ summarizationProvider: process.env.NEUROLINK_SUMMARIZATION_PROVIDER || "vertex",
79
+ summarizationModel: process.env.NEUROLINK_SUMMARIZATION_MODEL || "gemini-2.5-flash",
80
+ // Deprecated (for backward compatibility)
51
81
  maxTurnsPerSession: Number(process.env.NEUROLINK_MEMORY_MAX_TURNS_PER_SESSION) ||
52
82
  DEFAULT_MAX_TURNS_PER_SESSION,
53
- enableSummarization: process.env.NEUROLINK_SUMMARIZATION_ENABLED === "true",
54
83
  summarizationThresholdTurns: Number(process.env.NEUROLINK_SUMMARIZATION_THRESHOLD_TURNS) || 20,
55
84
  summarizationTargetTurns: Number(process.env.NEUROLINK_SUMMARIZATION_TARGET_TURNS) || 10,
56
- summarizationProvider: process.env.NEUROLINK_SUMMARIZATION_PROVIDER || "vertex",
57
- summarizationModel: process.env.NEUROLINK_SUMMARIZATION_MODEL || "gemini-2.5-flash",
58
85
  };
59
86
  }
60
87
  //# sourceMappingURL=conversationMemory.js.map