@juspay/neurolink 8.19.1 → 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 (37) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/cli/loop/optionsSchema.js +4 -0
  3. package/dist/config/conversationMemory.d.ts +15 -0
  4. package/dist/config/conversationMemory.js +22 -3
  5. package/dist/core/conversationMemoryFactory.js +0 -3
  6. package/dist/core/conversationMemoryInitializer.js +1 -9
  7. package/dist/core/conversationMemoryManager.d.ts +31 -8
  8. package/dist/core/conversationMemoryManager.js +174 -80
  9. package/dist/core/redisConversationMemoryManager.d.ts +28 -13
  10. package/dist/core/redisConversationMemoryManager.js +211 -121
  11. package/dist/lib/config/conversationMemory.d.ts +15 -0
  12. package/dist/lib/config/conversationMemory.js +22 -3
  13. package/dist/lib/core/conversationMemoryFactory.js +0 -3
  14. package/dist/lib/core/conversationMemoryInitializer.js +1 -9
  15. package/dist/lib/core/conversationMemoryManager.d.ts +31 -8
  16. package/dist/lib/core/conversationMemoryManager.js +174 -80
  17. package/dist/lib/core/redisConversationMemoryManager.d.ts +28 -13
  18. package/dist/lib/core/redisConversationMemoryManager.js +211 -121
  19. package/dist/lib/neurolink.js +29 -22
  20. package/dist/lib/types/conversation.d.ts +58 -9
  21. package/dist/lib/types/generateTypes.d.ts +1 -0
  22. package/dist/lib/types/sdkTypes.d.ts +1 -1
  23. package/dist/lib/types/streamTypes.d.ts +1 -0
  24. package/dist/lib/utils/conversationMemory.d.ts +43 -1
  25. package/dist/lib/utils/conversationMemory.js +181 -5
  26. package/dist/lib/utils/conversationMemoryUtils.js +16 -1
  27. package/dist/lib/utils/redis.js +0 -5
  28. package/dist/neurolink.js +29 -22
  29. package/dist/types/conversation.d.ts +58 -9
  30. package/dist/types/generateTypes.d.ts +1 -0
  31. package/dist/types/sdkTypes.d.ts +1 -1
  32. package/dist/types/streamTypes.d.ts +1 -0
  33. package/dist/utils/conversationMemory.d.ts +43 -1
  34. package/dist/utils/conversationMemory.js +181 -5
  35. package/dist/utils/conversationMemoryUtils.js +16 -1
  36. package/dist/utils/redis.js +0 -5
  37. package/package.json +1 -1
@@ -3,13 +3,19 @@
3
3
  * Handles in-memory conversation storage, session management, and context injection
4
4
  */
5
5
  import { ConversationMemoryError } from "../types/conversation.js";
6
- import { DEFAULT_MAX_TURNS_PER_SESSION, DEFAULT_MAX_SESSIONS, MESSAGES_PER_TURN, } from "../config/conversationMemory.js";
6
+ import { DEFAULT_MAX_SESSIONS, MEMORY_THRESHOLD_PERCENTAGE, RECENT_MESSAGES_RATIO, MESSAGES_PER_TURN, } from "../config/conversationMemory.js";
7
7
  import { logger } from "../utils/logger.js";
8
- import { NeuroLink } from "../neurolink.js";
8
+ import { randomUUID } from "crypto";
9
+ import { TokenUtils } from "../constants/tokens.js";
10
+ import { buildContextFromPointer, getEffectiveTokenThreshold, generateSummary, } from "../utils/conversationMemory.js";
9
11
  export class ConversationMemoryManager {
10
12
  sessions = new Map();
11
13
  config;
12
14
  isInitialized = false;
15
+ /**
16
+ * Track sessions currently being summarized to prevent race conditions
17
+ */
18
+ summarizationInProgress = new Set();
13
19
  constructor(config) {
14
20
  this.config = config;
15
21
  }
@@ -34,121 +40,209 @@ export class ConversationMemoryManager {
34
40
  }
35
41
  /**
36
42
  * Store a conversation turn for a session
37
- * ULTRA-OPTIMIZED: Direct ChatMessage[] storage with zero conversion overhead
43
+ * TOKEN-BASED: Validates message size and triggers summarization based on tokens
38
44
  */
39
- async storeConversationTurn(sessionId, userId, userMessage, aiResponse, _startTimeStamp) {
45
+ async storeConversationTurn(options) {
40
46
  await this.ensureInitialized();
41
47
  try {
42
48
  // Get or create session
43
- let session = this.sessions.get(sessionId);
49
+ let session = this.sessions.get(options.sessionId);
44
50
  if (!session) {
45
- session = this.createNewSession(sessionId, userId);
46
- this.sessions.set(sessionId, session);
51
+ session = this.createNewSession(options.sessionId, options.userId);
52
+ this.sessions.set(options.sessionId, session);
47
53
  }
48
- // ULTRA-OPTIMIZED: Direct message storage - no intermediate objects
49
- session.messages.push({ role: "user", content: userMessage }, { role: "assistant", content: aiResponse });
54
+ const tokenThreshold = options.providerDetails
55
+ ? getEffectiveTokenThreshold(options.providerDetails.provider, options.providerDetails.model, this.config.tokenThreshold, session.tokenThreshold)
56
+ : this.config.tokenThreshold || 50000;
57
+ const userMsg = await this.validateAndPrepareMessage(options.userMessage, "user", tokenThreshold);
58
+ const assistantMsg = await this.validateAndPrepareMessage(options.aiResponse, "assistant", tokenThreshold);
59
+ session.messages.push(userMsg, assistantMsg);
50
60
  session.lastActivity = Date.now();
51
- if (this.config.enableSummarization) {
52
- const userAssistantCount = session.messages.filter((msg) => msg.role === "user" || msg.role === "assistant").length;
53
- const currentTurnCount = Math.floor(userAssistantCount / MESSAGES_PER_TURN);
54
- if (currentTurnCount >= (this.config.summarizationThresholdTurns || 20)) {
55
- await this._summarizeSession(session);
61
+ const shouldSummarize = options.enableSummarization !== undefined
62
+ ? options.enableSummarization
63
+ : this.config.enableSummarization;
64
+ if (shouldSummarize) {
65
+ // Only trigger summarization if not already in progress for this session
66
+ if (!this.summarizationInProgress.has(options.sessionId)) {
67
+ setImmediate(async () => {
68
+ try {
69
+ await this.checkAndSummarize(session, tokenThreshold);
70
+ }
71
+ catch (error) {
72
+ logger.error("Background summarization failed", {
73
+ sessionId: session.sessionId,
74
+ error: error instanceof Error ? error.message : String(error),
75
+ });
76
+ }
77
+ });
56
78
  }
57
- }
58
- else {
59
- const maxMessages = (this.config.maxTurnsPerSession || DEFAULT_MAX_TURNS_PER_SESSION) *
60
- MESSAGES_PER_TURN;
61
- if (session.messages.length > maxMessages) {
62
- session.messages = session.messages.slice(-maxMessages);
79
+ else {
80
+ logger.debug("[ConversationMemoryManager] Summarization already in progress, skipping", {
81
+ sessionId: options.sessionId,
82
+ });
63
83
  }
64
84
  }
65
85
  this.enforceSessionLimit();
66
86
  }
67
87
  catch (error) {
68
- throw new ConversationMemoryError(`Failed to store conversation turn for session ${sessionId}`, "STORAGE_ERROR", {
69
- sessionId,
88
+ throw new ConversationMemoryError(`Failed to store conversation turn for session ${options.sessionId}`, "STORAGE_ERROR", {
89
+ sessionId: options.sessionId,
70
90
  error: error instanceof Error ? error.message : String(error),
71
91
  });
72
92
  }
73
93
  }
74
94
  /**
75
- * Build context messages for AI prompt injection (ULTRA-OPTIMIZED)
76
- * Returns pre-stored message array with zero conversion overhead
95
+ * Validate and prepare a message before adding to session
96
+ * Truncates if message exceeds token limit
97
+ */
98
+ async validateAndPrepareMessage(content, role, threshold) {
99
+ const id = randomUUID();
100
+ const tokenCount = TokenUtils.estimateTokenCount(content);
101
+ const maxMessageSize = Math.floor(threshold * MEMORY_THRESHOLD_PERCENTAGE);
102
+ if (tokenCount > maxMessageSize) {
103
+ const truncated = TokenUtils.truncateToTokenLimit(content, maxMessageSize);
104
+ logger.warn("Message truncated due to token limit", {
105
+ id,
106
+ role,
107
+ originalTokens: tokenCount,
108
+ threshold,
109
+ truncatedTo: maxMessageSize,
110
+ });
111
+ return {
112
+ id,
113
+ role,
114
+ content: truncated,
115
+ timestamp: new Date().toISOString(),
116
+ metadata: {
117
+ truncated: true,
118
+ },
119
+ };
120
+ }
121
+ return {
122
+ id,
123
+ role,
124
+ content,
125
+ timestamp: new Date().toISOString(),
126
+ };
127
+ }
128
+ /**
129
+ * Check if summarization is needed based on token count
130
+ */
131
+ async checkAndSummarize(session, threshold) {
132
+ // Acquire lock - if already in progress, skip
133
+ if (this.summarizationInProgress.has(session.sessionId)) {
134
+ logger.debug("[ConversationMemoryManager] Summarization already in progress, skipping", {
135
+ sessionId: session.sessionId,
136
+ });
137
+ return;
138
+ }
139
+ this.summarizationInProgress.add(session.sessionId);
140
+ try {
141
+ const contextMessages = buildContextFromPointer(session);
142
+ const tokenCount = this.estimateTokens(contextMessages);
143
+ session.lastTokenCount = tokenCount;
144
+ session.lastCountedAt = Date.now();
145
+ logger.debug("Token count check", {
146
+ sessionId: session.sessionId,
147
+ tokenCount,
148
+ threshold,
149
+ needsSummarization: tokenCount >= threshold,
150
+ });
151
+ if (tokenCount >= threshold) {
152
+ await this.summarizeSessionTokenBased(session, threshold);
153
+ }
154
+ }
155
+ catch (error) {
156
+ logger.error("Token counting or summarization failed", {
157
+ sessionId: session.sessionId,
158
+ error: error instanceof Error ? error.message : String(error),
159
+ });
160
+ }
161
+ finally {
162
+ // Release lock when done
163
+ this.summarizationInProgress.delete(session.sessionId);
164
+ }
165
+ }
166
+ /**
167
+ * Estimate total tokens for a list of messages
168
+ */
169
+ estimateTokens(messages) {
170
+ return messages.reduce((total, msg) => {
171
+ return total + TokenUtils.estimateTokenCount(msg.content);
172
+ }, 0);
173
+ }
174
+ /**
175
+ * Build context messages for AI prompt injection (TOKEN-BASED)
176
+ * Returns messages from pointer onwards (or all if no pointer)
77
177
  * Now consistently async to match Redis implementation
78
178
  */
79
179
  async buildContextMessages(sessionId) {
80
180
  const session = this.sessions.get(sessionId);
81
- return session ? session.messages : [];
181
+ return session ? buildContextFromPointer(session) : [];
82
182
  }
83
183
  getSession(sessionId) {
84
184
  return this.sessions.get(sessionId);
85
185
  }
86
- createSummarySystemMessage(content) {
186
+ createSummarySystemMessage(content, summarizesFrom, summarizesTo) {
87
187
  return {
188
+ id: `summary-${randomUUID()}`,
88
189
  role: "system",
89
190
  content: `Summary of previous conversation turns:\n\n${content}`,
191
+ timestamp: new Date().toISOString(),
192
+ metadata: {
193
+ isSummary: true,
194
+ summarizesFrom,
195
+ summarizesTo,
196
+ },
90
197
  };
91
198
  }
92
- async _summarizeSession(session) {
93
- logger.info(`[ConversationMemory] Summarizing session ${session.sessionId}...`);
94
- const targetTurns = this.config.summarizationTargetTurns || 10;
95
- const splitIndex = Math.max(0, session.messages.length - targetTurns * MESSAGES_PER_TURN);
96
- const messagesToSummarize = session.messages.slice(0, splitIndex);
97
- const recentMessages = session.messages.slice(splitIndex);
199
+ /**
200
+ * Token-based summarization (pointer-based, non-destructive)
201
+ */
202
+ async summarizeSessionTokenBased(session, threshold) {
203
+ const startIndex = session.summarizedUpToMessageId
204
+ ? session.messages.findIndex((m) => m.id === session.summarizedUpToMessageId) + 1
205
+ : 0;
206
+ const recentMessages = session.messages.slice(startIndex);
207
+ if (recentMessages.length === 0) {
208
+ return;
209
+ }
210
+ const targetRecentTokens = threshold * RECENT_MESSAGES_RATIO;
211
+ const splitIndex = await this.findSplitIndexByTokens(recentMessages, targetRecentTokens);
212
+ const messagesToSummarize = recentMessages.slice(0, splitIndex);
98
213
  if (messagesToSummarize.length === 0) {
99
214
  return;
100
215
  }
101
- const summarizationPrompt = this._createSummarizationPrompt(messagesToSummarize);
102
- const summarizer = new NeuroLink({
103
- conversationMemory: { enabled: false },
216
+ const summary = await generateSummary(messagesToSummarize, this.config, "[ConversationMemory]", session.summarizedMessage);
217
+ if (!summary) {
218
+ logger.warn(`[ConversationMemory] Summary generation failed for session ${session.sessionId}`);
219
+ return;
220
+ }
221
+ const lastSummarized = messagesToSummarize[messagesToSummarize.length - 1];
222
+ session.summarizedUpToMessageId = lastSummarized.id;
223
+ session.summarizedMessage = summary; // Store summary separately
224
+ logger.info(`[ConversationMemory] Summarization complete for session ${session.sessionId}`, {
225
+ summarizedCount: messagesToSummarize.length,
226
+ totalMessages: session.messages.length,
227
+ pointer: session.summarizedUpToMessageId,
104
228
  });
105
- try {
106
- const providerName = this.config.summarizationProvider;
107
- // Map provider names to correct format
108
- let mappedProvider = providerName;
109
- if (providerName === "vertex") {
110
- mappedProvider = "googlevertex";
111
- }
112
- if (!mappedProvider) {
113
- logger.error(`[ConversationMemory] Missing summarization provider`);
114
- return;
115
- }
116
- logger.debug(`[ConversationMemory] Using provider: ${mappedProvider} for summarization`);
117
- const summaryResult = await summarizer.generate({
118
- input: { text: summarizationPrompt },
119
- provider: mappedProvider,
120
- model: this.config.summarizationModel,
121
- disableTools: true,
122
- });
123
- if (summaryResult.content) {
124
- session.messages = [
125
- this.createSummarySystemMessage(summaryResult.content),
126
- ...recentMessages,
127
- ];
128
- logger.info(`[ConversationMemory] Summarization complete for session ${session.sessionId}.`);
129
- }
130
- else {
131
- logger.warn(`[ConversationMemory] Summarization failed for session ${session.sessionId}. History not modified.`);
229
+ }
230
+ /**
231
+ * Find split index to keep recent messages within target token count
232
+ */
233
+ async findSplitIndexByTokens(messages, targetRecentTokens) {
234
+ let recentTokens = 0;
235
+ let splitIndex = messages.length;
236
+ for (let i = messages.length - 1; i >= 0; i--) {
237
+ const msgTokens = TokenUtils.estimateTokenCount(messages[i].content);
238
+ if (recentTokens + msgTokens > targetRecentTokens) {
239
+ splitIndex = i + 1;
240
+ break;
132
241
  }
242
+ recentTokens += msgTokens;
133
243
  }
134
- catch (error) {
135
- logger.error(`[ConversationMemory] Error during summarization for session ${session.sessionId}`, { error });
136
- }
137
- }
138
- _createSummarizationPrompt(history) {
139
- const formattedHistory = history
140
- .map((msg) => `${msg.role}: ${msg.content}`)
141
- .join("\n\n");
142
- return `
143
- You are a context summarization AI. Your task is to condense the following conversation history for another AI assistant.
144
- The summary must be a concise, third-person narrative that retains all critical information, including key entities, technical details, decisions made, and any specific dates or times mentioned.
145
- Ensure the summary flows logically and is ready to be used as context for the next turn in the conversation.
146
-
147
- Conversation History to Summarize:
148
- ---
149
- ${formattedHistory}
150
- ---
151
- `.trim();
244
+ // To ensure at least one message is summarized
245
+ return Math.max(1, splitIndex);
152
246
  }
153
247
  async ensureInitialized() {
154
248
  if (!this.isInitialized) {
@@ -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 { ConversationMemoryConfig, ConversationMemoryStats, ChatMessage, RedisStorageConfig, SessionMetadata, RedisConversationObject } from "../types/conversation.js";
5
+ import type { ConversationMemoryConfig, ConversationMemoryStats, ChatMessage, RedisStorageConfig, SessionMetadata, RedisConversationObject, StoreConversationTurnOptions } from "../types/conversation.js";
6
6
  /**
7
7
  * Redis-based implementation of the ConversationMemoryManager
8
8
  * Uses the same interface but stores data in Redis
@@ -22,6 +22,11 @@ export declare class RedisConversationMemoryManager {
22
22
  * Key format: "${sessionId}:${userId}"
23
23
  */
24
24
  private titleGenerationInProgress;
25
+ /**
26
+ * Track sessions currently being summarized to prevent race conditions
27
+ * Key format: "${sessionId}:${userId}"
28
+ */
29
+ private summarizationInProgress;
25
30
  constructor(config: ConversationMemoryConfig, redisConfig?: RedisStorageConfig);
26
31
  /**
27
32
  * Initialize the memory manager with Redis connection
@@ -39,18 +44,10 @@ export declare class RedisConversationMemoryManager {
39
44
  * Remove a session from user's session set (private method)
40
45
  */
41
46
  private removeUserSession;
42
- /**
43
- * Generate next message ID for a conversation
44
- */
45
- private generateMessageId;
46
47
  /**
47
48
  * Generate current timestamp in ISO format
48
49
  */
49
50
  private generateTimestamp;
50
- /**
51
- * Generate a unique conversation ID using UUID v4
52
- */
53
- private generateUniqueId;
54
51
  /**
55
52
  * Store tool execution data for a session (temporarily to avoid race conditions)
56
53
  */
@@ -68,11 +65,29 @@ export declare class RedisConversationMemoryManager {
68
65
  /**
69
66
  * Store a conversation turn for a session
70
67
  */
71
- storeConversationTurn(sessionId: string, userId: string | undefined, userMessage: string, aiResponse: string, startTimeStamp: Date | undefined): Promise<void>;
68
+ storeConversationTurn(options: StoreConversationTurnOptions): Promise<void>;
69
+ /**
70
+ * Check if summarization is needed based on token count
71
+ */
72
+ private checkAndSummarize;
73
+ /**
74
+ * Estimate total tokens for a list of messages
75
+ */
76
+ private estimateTokens;
77
+ /**
78
+ * Token-based summarization (pointer-based, non-destructive)
79
+ */
80
+ private summarizeSessionTokenBased;
81
+ /**
82
+ * Find split index to keep recent messages within target token count
83
+ */
84
+ private findSplitIndexByTokens;
72
85
  /**
73
- * Build context messages for AI prompt injection
86
+ * Build context messages for AI prompt injection (TOKEN-BASED)
87
+ * Returns messages from pointer onwards (or all if no pointer)
88
+ * Filters out tool_call and tool_result messages when summarization is enabled
74
89
  */
75
- buildContextMessages(sessionId: string, userId?: string): Promise<ChatMessage[]>;
90
+ buildContextMessages(sessionId: string, userId?: string, enableSummarization?: boolean): Promise<ChatMessage[]>;
76
91
  /**
77
92
  * Get session metadata for a specific user session (optimized for listing)
78
93
  * Fetches only essential metadata without heavy message arrays
@@ -110,7 +125,7 @@ export declare class RedisConversationMemoryManager {
110
125
  /**
111
126
  * Create summary system message
112
127
  */
113
- createSummarySystemMessage(content: string): ChatMessage;
128
+ createSummarySystemMessage(content: string, summarizesFrom?: string, summarizesTo?: string): ChatMessage;
114
129
  /**
115
130
  * Close Redis connection
116
131
  */