@juspay/neurolink 7.37.1 → 7.38.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/core/baseProvider.d.ts +4 -0
- package/dist/core/baseProvider.js +40 -0
- package/dist/core/redisConversationMemoryManager.d.ts +98 -15
- package/dist/core/redisConversationMemoryManager.js +665 -203
- package/dist/lib/core/baseProvider.d.ts +4 -0
- package/dist/lib/core/baseProvider.js +40 -0
- package/dist/lib/core/redisConversationMemoryManager.d.ts +98 -15
- package/dist/lib/core/redisConversationMemoryManager.js +665 -203
- package/dist/lib/neurolink.d.ts +33 -1
- package/dist/lib/neurolink.js +64 -0
- package/dist/lib/providers/anthropic.js +8 -0
- package/dist/lib/providers/anthropicBaseProvider.js +8 -0
- package/dist/lib/providers/azureOpenai.js +8 -0
- package/dist/lib/providers/googleAiStudio.js +8 -0
- package/dist/lib/providers/googleVertex.js +10 -0
- package/dist/lib/providers/huggingFace.js +8 -0
- package/dist/lib/providers/litellm.js +8 -0
- package/dist/lib/providers/mistral.js +8 -0
- package/dist/lib/providers/openAI.js +12 -2
- package/dist/lib/providers/openaiCompatible.js +8 -0
- package/dist/lib/types/conversation.d.ts +52 -2
- package/dist/lib/utils/conversationMemory.js +3 -1
- package/dist/lib/utils/messageBuilder.d.ts +10 -2
- package/dist/lib/utils/messageBuilder.js +22 -1
- package/dist/lib/utils/redis.d.ts +10 -6
- package/dist/lib/utils/redis.js +71 -70
- package/dist/neurolink.d.ts +33 -1
- package/dist/neurolink.js +64 -0
- package/dist/providers/anthropic.js +8 -0
- package/dist/providers/anthropicBaseProvider.js +8 -0
- package/dist/providers/azureOpenai.js +8 -0
- package/dist/providers/googleAiStudio.js +8 -0
- package/dist/providers/googleVertex.js +10 -0
- package/dist/providers/huggingFace.js +8 -0
- package/dist/providers/litellm.js +8 -0
- package/dist/providers/mistral.js +8 -0
- package/dist/providers/openAI.js +12 -2
- package/dist/providers/openaiCompatible.js +8 -0
- package/dist/types/conversation.d.ts +52 -2
- package/dist/utils/conversationMemory.js +3 -1
- package/dist/utils/messageBuilder.d.ts +10 -2
- package/dist/utils/messageBuilder.js +22 -1
- package/dist/utils/redis.d.ts +10 -6
- package/dist/utils/redis.js +71 -70
- package/package.json +2 -2
@@ -2,20 +2,27 @@
|
|
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 { randomUUID } from "crypto";
|
5
6
|
import { ConversationMemoryError } from "../types/conversation.js";
|
6
|
-
import {
|
7
|
+
import { DEFAULT_MAX_SESSIONS, MESSAGES_PER_TURN, } from "../config/conversationMemory.js";
|
7
8
|
import { logger } from "../utils/logger.js";
|
8
9
|
import { NeuroLink } from "../neurolink.js";
|
9
|
-
import { createRedisClient, getSessionKey, getNormalizedConfig,
|
10
|
-
/**
|
11
|
-
* Redis-based implementation of the ConversationMemoryManager
|
12
|
-
* Uses the same interface but stores data in Redis
|
13
|
-
*/
|
10
|
+
import { createRedisClient, getSessionKey, getUserSessionsKey, getNormalizedConfig, serializeConversation, deserializeConversation, scanKeys, } from "../utils/redis.js";
|
14
11
|
export class RedisConversationMemoryManager {
|
15
12
|
config;
|
16
13
|
isInitialized = false;
|
17
14
|
redisConfig;
|
18
15
|
redisClient = null;
|
16
|
+
/**
|
17
|
+
* Temporary storage for tool execution data to prevent race conditions
|
18
|
+
* Key format: "${sessionId}:${userId}"
|
19
|
+
*/
|
20
|
+
pendingToolExecutions = new Map();
|
21
|
+
/**
|
22
|
+
* Track sessions currently generating titles to prevent race conditions
|
23
|
+
* Key format: "${sessionId}:${userId}"
|
24
|
+
*/
|
25
|
+
titleGenerationInProgress = new Set();
|
19
26
|
constructor(config, redisConfig = {}) {
|
20
27
|
this.config = config;
|
21
28
|
this.redisConfig = getNormalizedConfig(redisConfig);
|
@@ -61,6 +68,149 @@ export class RedisConversationMemoryManager {
|
|
61
68
|
throw new ConversationMemoryError("Failed to initialize Redis conversation memory", "CONFIG_ERROR", { error: error instanceof Error ? error.message : String(error) });
|
62
69
|
}
|
63
70
|
}
|
71
|
+
/**
|
72
|
+
* Get all sessions for a specific user
|
73
|
+
*/
|
74
|
+
async getUserSessions(userId) {
|
75
|
+
// Ensure initialization
|
76
|
+
await this.ensureInitialized();
|
77
|
+
if (!this.redisClient) {
|
78
|
+
logger.warn("[RedisConversationMemoryManager] Redis client not available", { userId });
|
79
|
+
return [];
|
80
|
+
}
|
81
|
+
try {
|
82
|
+
const userSessionsKey = getUserSessionsKey(this.redisConfig, userId);
|
83
|
+
const sessions = await this.redisClient.sMembers(userSessionsKey);
|
84
|
+
return sessions;
|
85
|
+
}
|
86
|
+
catch (error) {
|
87
|
+
logger.error("[RedisConversationMemoryManager] Failed to get user sessions", {
|
88
|
+
userId,
|
89
|
+
error: error instanceof Error ? error.message : String(error),
|
90
|
+
});
|
91
|
+
return [];
|
92
|
+
}
|
93
|
+
}
|
94
|
+
/**
|
95
|
+
* Add a session to user's session set (private method)
|
96
|
+
*/
|
97
|
+
async addUserSession(userId, sessionId) {
|
98
|
+
if (!this.redisClient || !userId) {
|
99
|
+
return;
|
100
|
+
}
|
101
|
+
try {
|
102
|
+
const userSessionsKey = getUserSessionsKey(this.redisConfig, userId);
|
103
|
+
await this.redisClient.sAdd(userSessionsKey, sessionId);
|
104
|
+
if (this.redisConfig.ttl > 0) {
|
105
|
+
await this.redisClient.expire(userSessionsKey, this.redisConfig.ttl);
|
106
|
+
}
|
107
|
+
}
|
108
|
+
catch (error) {
|
109
|
+
logger.error("[RedisConversationMemoryManager] Failed to add session to user set", {
|
110
|
+
userId,
|
111
|
+
sessionId,
|
112
|
+
error: error instanceof Error ? error.message : String(error),
|
113
|
+
});
|
114
|
+
}
|
115
|
+
}
|
116
|
+
/**
|
117
|
+
* Remove a session from user's session set (private method)
|
118
|
+
*/
|
119
|
+
async removeUserSession(userId, sessionId) {
|
120
|
+
if (!this.redisClient || !userId) {
|
121
|
+
return false;
|
122
|
+
}
|
123
|
+
try {
|
124
|
+
const userSessionsKey = getUserSessionsKey(this.redisConfig, userId);
|
125
|
+
const result = await this.redisClient.sRem(userSessionsKey, sessionId);
|
126
|
+
return result > 0;
|
127
|
+
}
|
128
|
+
catch (error) {
|
129
|
+
logger.error("[RedisConversationMemoryManager] Failed to remove session from user set", {
|
130
|
+
userId,
|
131
|
+
sessionId,
|
132
|
+
error: error instanceof Error ? error.message : String(error),
|
133
|
+
});
|
134
|
+
return false;
|
135
|
+
}
|
136
|
+
}
|
137
|
+
/**
|
138
|
+
* Generate next message ID for a conversation
|
139
|
+
*/
|
140
|
+
generateMessageId(conversation) {
|
141
|
+
const currentCount = conversation?.messages?.length || 0;
|
142
|
+
return `msg_${currentCount + 1}`;
|
143
|
+
}
|
144
|
+
/**
|
145
|
+
* Generate current timestamp in ISO format
|
146
|
+
*/
|
147
|
+
generateTimestamp() {
|
148
|
+
return new Date().toISOString();
|
149
|
+
}
|
150
|
+
/**
|
151
|
+
* Generate a unique conversation ID using UUID v4
|
152
|
+
*/
|
153
|
+
generateUniqueId() {
|
154
|
+
return randomUUID();
|
155
|
+
}
|
156
|
+
/**
|
157
|
+
* Store tool execution data for a session (temporarily to avoid race conditions)
|
158
|
+
*/
|
159
|
+
async storeToolExecution(sessionId, userId, toolCalls, toolResults) {
|
160
|
+
logger.debug("[RedisConversationMemoryManager] Storing tool execution temporarily", {
|
161
|
+
sessionId,
|
162
|
+
userId,
|
163
|
+
toolCallsCount: toolCalls?.length || 0,
|
164
|
+
toolResultsCount: toolResults?.length || 0,
|
165
|
+
});
|
166
|
+
try {
|
167
|
+
const normalizedUserId = userId || "randomUser";
|
168
|
+
const pendingKey = `${sessionId}:${normalizedUserId}`;
|
169
|
+
// Store tool execution data temporarily to prevent race conditions
|
170
|
+
const pendingData = {
|
171
|
+
toolCalls: toolCalls || [],
|
172
|
+
toolResults: toolResults || [],
|
173
|
+
timestamp: Date.now(),
|
174
|
+
};
|
175
|
+
// Check if there's existing pending data and merge
|
176
|
+
const existingData = this.pendingToolExecutions.get(pendingKey);
|
177
|
+
if (existingData) {
|
178
|
+
logger.debug("[RedisConversationMemoryManager] Merging with existing pending tool data", {
|
179
|
+
sessionId,
|
180
|
+
existingToolCalls: existingData.toolCalls.length,
|
181
|
+
existingToolResults: existingData.toolResults.length,
|
182
|
+
newToolCalls: toolCalls?.length || 0,
|
183
|
+
newToolResults: toolResults?.length || 0,
|
184
|
+
});
|
185
|
+
// Merge tool calls and results
|
186
|
+
pendingData.toolCalls = [
|
187
|
+
...existingData.toolCalls,
|
188
|
+
...pendingData.toolCalls,
|
189
|
+
];
|
190
|
+
pendingData.toolResults = [
|
191
|
+
...existingData.toolResults,
|
192
|
+
...pendingData.toolResults,
|
193
|
+
];
|
194
|
+
}
|
195
|
+
this.pendingToolExecutions.set(pendingKey, pendingData);
|
196
|
+
logger.debug("[RedisConversationMemoryManager] Tool execution stored temporarily", {
|
197
|
+
sessionId,
|
198
|
+
userId: normalizedUserId,
|
199
|
+
pendingKey,
|
200
|
+
totalToolCalls: pendingData.toolCalls.length,
|
201
|
+
totalToolResults: pendingData.toolResults.length,
|
202
|
+
});
|
203
|
+
// Clean up stale pending data (older than 5 minutes)
|
204
|
+
this.cleanupStalePendingData();
|
205
|
+
}
|
206
|
+
catch (error) {
|
207
|
+
logger.error("[RedisConversationMemoryManager] Failed to store tool execution temporarily", {
|
208
|
+
sessionId,
|
209
|
+
error: error instanceof Error ? error.message : String(error),
|
210
|
+
});
|
211
|
+
// Don't throw - tool storage failures shouldn't break generation
|
212
|
+
}
|
213
|
+
}
|
64
214
|
/**
|
65
215
|
* Store a conversation turn for a session
|
66
216
|
*/
|
@@ -77,69 +227,131 @@ export class RedisConversationMemoryManager {
|
|
77
227
|
throw new Error("Redis client not initialized");
|
78
228
|
}
|
79
229
|
// Generate Redis key
|
80
|
-
const redisKey = getSessionKey(this.redisConfig, sessionId);
|
81
|
-
// Get existing
|
82
|
-
const
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
230
|
+
const redisKey = getSessionKey(this.redisConfig, sessionId, userId);
|
231
|
+
// Get existing conversation object
|
232
|
+
const conversationData = await this.redisClient.get(redisKey);
|
233
|
+
let conversation = deserializeConversation(conversationData);
|
234
|
+
const currentTime = new Date().toISOString();
|
235
|
+
const normalizedUserId = userId || "randomUser";
|
236
|
+
// If no existing conversation, create a new one
|
237
|
+
if (!conversation) {
|
238
|
+
// Generate title asynchronously in the background (non-blocking)
|
239
|
+
const titleGenerationKey = `${sessionId}:${normalizedUserId}`;
|
240
|
+
setImmediate(async () => {
|
241
|
+
// Check if title generation is already in progress for this session
|
242
|
+
if (this.titleGenerationInProgress.has(titleGenerationKey)) {
|
243
|
+
logger.debug("[RedisConversationMemoryManager] Title generation already in progress, skipping", {
|
244
|
+
sessionId,
|
245
|
+
userId: normalizedUserId,
|
246
|
+
titleGenerationKey,
|
247
|
+
});
|
248
|
+
return;
|
249
|
+
}
|
250
|
+
// Mark title generation as in progress
|
251
|
+
this.titleGenerationInProgress.add(titleGenerationKey);
|
252
|
+
try {
|
253
|
+
const title = await this.generateConversationTitle(userMessage);
|
254
|
+
logger.info("[RedisConversationMemoryManager] Successfully generated conversation title", {
|
255
|
+
sessionId,
|
256
|
+
userId: normalizedUserId,
|
257
|
+
title,
|
258
|
+
});
|
259
|
+
const updatedRedisKey = getSessionKey(this.redisConfig, sessionId, userId || undefined);
|
260
|
+
const updatedConversationData = await this.redisClient?.get(updatedRedisKey);
|
261
|
+
const updatedConversation = deserializeConversation(updatedConversationData || null);
|
262
|
+
if (updatedConversation) {
|
263
|
+
updatedConversation.title = title;
|
264
|
+
updatedConversation.updatedAt = new Date().toISOString();
|
265
|
+
const serializedData = serializeConversation(updatedConversation);
|
266
|
+
await this.redisClient?.set(updatedRedisKey, serializedData);
|
267
|
+
if (this.redisConfig.ttl > 0) {
|
268
|
+
await this.redisClient?.expire(updatedRedisKey, this.redisConfig.ttl);
|
269
|
+
}
|
270
|
+
}
|
271
|
+
}
|
272
|
+
catch (titleError) {
|
273
|
+
logger.warn("[RedisConversationMemoryManager] Failed to generate conversation title in background", {
|
274
|
+
sessionId,
|
275
|
+
userId: normalizedUserId,
|
276
|
+
error: titleError instanceof Error
|
277
|
+
? titleError.message
|
278
|
+
: String(titleError),
|
279
|
+
});
|
280
|
+
}
|
281
|
+
finally {
|
282
|
+
// Always remove from tracking set when done (success or failure)
|
283
|
+
this.titleGenerationInProgress.delete(titleGenerationKey);
|
284
|
+
logger.debug("[RedisConversationMemoryManager] Title generation completed, removed from tracking", {
|
285
|
+
sessionId,
|
286
|
+
userId: normalizedUserId,
|
287
|
+
titleGenerationKey,
|
288
|
+
remainingInProgress: this.titleGenerationInProgress.size,
|
289
|
+
});
|
290
|
+
}
|
291
|
+
});
|
292
|
+
conversation = {
|
293
|
+
id: this.generateUniqueId(), // Generate unique UUID v4 for conversation
|
294
|
+
title: "New Conversation", // Temporary title until generated
|
295
|
+
sessionId,
|
296
|
+
userId: normalizedUserId,
|
297
|
+
createdAt: currentTime,
|
298
|
+
updatedAt: currentTime,
|
299
|
+
messages: [],
|
300
|
+
};
|
301
|
+
}
|
302
|
+
else {
|
303
|
+
// Update existing conversation timestamp
|
304
|
+
conversation.updatedAt = currentTime;
|
305
|
+
}
|
306
|
+
logger.info("[RedisConversationMemoryManager] Processing conversation", {
|
307
|
+
isNewConversation: !conversationData,
|
308
|
+
messageCount: conversation.messages.length,
|
309
|
+
sessionId: conversation.sessionId,
|
310
|
+
userId: conversation.userId,
|
87
311
|
});
|
88
|
-
// Add new messages
|
89
|
-
|
312
|
+
// Add new messages to conversation history with new format
|
313
|
+
const userMsg = {
|
314
|
+
id: this.generateMessageId(conversation),
|
315
|
+
timestamp: this.generateTimestamp(),
|
316
|
+
role: "user",
|
317
|
+
content: userMessage,
|
318
|
+
};
|
319
|
+
conversation.messages.push(userMsg);
|
320
|
+
await this.flushPendingToolData(conversation, sessionId, normalizedUserId);
|
321
|
+
const assistantMsg = {
|
322
|
+
id: this.generateMessageId(conversation),
|
323
|
+
timestamp: this.generateTimestamp(),
|
324
|
+
role: "assistant",
|
325
|
+
content: aiResponse,
|
326
|
+
};
|
327
|
+
conversation.messages.push(assistantMsg);
|
90
328
|
logger.info("[RedisConversationMemoryManager] Added new messages", {
|
91
|
-
newMessageCount: messages.length,
|
329
|
+
newMessageCount: conversation.messages.length,
|
92
330
|
latestMessages: [
|
93
331
|
{
|
94
|
-
role: messages[messages.length - 2]?.role,
|
95
|
-
contentLength: messages[messages.length - 2]?.content
|
332
|
+
role: conversation.messages[conversation.messages.length - 2]?.role,
|
333
|
+
contentLength: conversation.messages[conversation.messages.length - 2]?.content
|
334
|
+
.length,
|
96
335
|
},
|
97
336
|
{
|
98
|
-
role: messages[messages.length - 1]?.role,
|
99
|
-
contentLength: messages[messages.length - 1]?.content
|
337
|
+
role: conversation.messages[conversation.messages.length - 1]?.role,
|
338
|
+
contentLength: conversation.messages[conversation.messages.length - 1]?.content
|
339
|
+
.length,
|
100
340
|
},
|
101
341
|
],
|
102
342
|
});
|
103
|
-
//
|
104
|
-
|
105
|
-
|
106
|
-
const currentTurnCount = Math.floor(userAssistantCount / MESSAGES_PER_TURN);
|
107
|
-
logger.debug("[RedisConversationMemoryManager] Checking summarization threshold", {
|
108
|
-
userAssistantCount,
|
109
|
-
currentTurnCount,
|
110
|
-
summarizationThreshold: this.config.summarizationThresholdTurns || 20,
|
111
|
-
shouldSummarize: currentTurnCount >=
|
112
|
-
(this.config.summarizationThresholdTurns || 20),
|
113
|
-
});
|
114
|
-
if (currentTurnCount >= (this.config.summarizationThresholdTurns || 20)) {
|
115
|
-
await this._summarizeMessages(sessionId, userId, messages);
|
116
|
-
return;
|
117
|
-
}
|
118
|
-
}
|
119
|
-
else {
|
120
|
-
const maxMessages = (this.config.maxTurnsPerSession || DEFAULT_MAX_TURNS_PER_SESSION) *
|
121
|
-
MESSAGES_PER_TURN;
|
122
|
-
logger.debug("[RedisConversationMemoryManager] Checking message limit", {
|
123
|
-
currentMessageCount: messages.length,
|
124
|
-
maxMessages,
|
125
|
-
shouldTrimMessages: messages.length > maxMessages,
|
126
|
-
});
|
127
|
-
if (messages.length > maxMessages) {
|
128
|
-
const trimCount = messages.length - maxMessages;
|
129
|
-
logger.debug("[RedisConversationMemoryManager] Trimming messages", {
|
130
|
-
beforeCount: messages.length,
|
131
|
-
trimCount,
|
132
|
-
afterCount: maxMessages,
|
133
|
-
});
|
134
|
-
messages.splice(0, messages.length - maxMessages);
|
135
|
-
}
|
136
|
-
}
|
137
|
-
// Save updated messages
|
138
|
-
const serializedData = serializeMessages(messages);
|
139
|
-
logger.debug("[RedisConversationMemoryManager] Saving messages to Redis", {
|
343
|
+
// Save updated conversation object
|
344
|
+
const serializedData = serializeConversation(conversation);
|
345
|
+
logger.debug("[RedisConversationMemoryManager] Saving conversation to Redis", {
|
140
346
|
redisKey,
|
141
|
-
messageCount: messages.length,
|
347
|
+
messageCount: conversation.messages.length,
|
142
348
|
serializedDataLength: serializedData.length,
|
349
|
+
title: conversation.title,
|
350
|
+
});
|
351
|
+
logger.info("Storing conversation data to Redis", {
|
352
|
+
sessionId,
|
353
|
+
dataLength: serializedData.length,
|
354
|
+
messageCount: conversation.messages.length,
|
143
355
|
});
|
144
356
|
await this.redisClient.set(redisKey, serializedData);
|
145
357
|
// Set TTL if configured
|
@@ -150,11 +362,16 @@ export class RedisConversationMemoryManager {
|
|
150
362
|
});
|
151
363
|
await this.redisClient.expire(redisKey, this.redisConfig.ttl);
|
152
364
|
}
|
365
|
+
// Add session to user's session set
|
366
|
+
if (userId) {
|
367
|
+
await this.addUserSession(userId, sessionId);
|
368
|
+
}
|
153
369
|
// Enforce session limit
|
154
370
|
await this.enforceSessionLimit();
|
155
371
|
logger.debug("[RedisConversationMemoryManager] Successfully stored conversation turn", {
|
156
372
|
sessionId,
|
157
|
-
totalMessages: messages.length,
|
373
|
+
totalMessages: conversation.messages.length,
|
374
|
+
title: conversation.title,
|
158
375
|
});
|
159
376
|
}
|
160
377
|
catch (error) {
|
@@ -167,33 +384,27 @@ export class RedisConversationMemoryManager {
|
|
167
384
|
/**
|
168
385
|
* Build context messages for AI prompt injection
|
169
386
|
*/
|
170
|
-
async buildContextMessages(sessionId) {
|
387
|
+
async buildContextMessages(sessionId, userId) {
|
171
388
|
logger.info("[RedisConversationMemoryManager] Building context messages", {
|
172
389
|
sessionId,
|
390
|
+
userId,
|
173
391
|
method: "buildContextMessages",
|
174
392
|
});
|
175
|
-
await this.
|
176
|
-
if (!
|
177
|
-
logger.
|
393
|
+
const messages = await this.getUserSessionHistory(userId || "randomUser", sessionId);
|
394
|
+
if (!messages) {
|
395
|
+
logger.info("[RedisConversationMemoryManager] No context messages found", {
|
178
396
|
sessionId,
|
397
|
+
userId,
|
179
398
|
});
|
180
399
|
return [];
|
181
400
|
}
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
redisKey,
|
186
|
-
});
|
187
|
-
const messagesData = await this.redisClient.get(redisKey);
|
188
|
-
logger.info("[RedisConversationMemoryManager] Retrieved message data from Redis", {
|
189
|
-
sessionId,
|
190
|
-
redisKey,
|
191
|
-
hasData: !!messagesData,
|
192
|
-
dataLength: messagesData?.length || 0,
|
401
|
+
logger.info("[RedisConversationMemoryManager] Retrieved messages", {
|
402
|
+
messageCount: messages.length,
|
403
|
+
hasMessages: messages.length > 0,
|
193
404
|
});
|
194
|
-
|
195
|
-
logger.info("[RedisConversationMemoryManager] Deserialized messages for context", {
|
405
|
+
logger.info("[RedisConversationMemoryManager] Retrieved context messages", {
|
196
406
|
sessionId,
|
407
|
+
userId,
|
197
408
|
messageCount: messages.length,
|
198
409
|
messageRoles: messages.map((m) => m.role),
|
199
410
|
firstMessagePreview: messages[0]?.content?.substring(0, 50),
|
@@ -202,57 +413,229 @@ export class RedisConversationMemoryManager {
|
|
202
413
|
return messages;
|
203
414
|
}
|
204
415
|
/**
|
205
|
-
* Get session
|
416
|
+
* Get session metadata for a specific user session (optimized for listing)
|
417
|
+
* Fetches only essential metadata without heavy message arrays
|
418
|
+
*
|
419
|
+
* @param userId The user identifier
|
420
|
+
* @param sessionId The session identifier
|
421
|
+
* @returns Session metadata or null if session doesn't exist
|
206
422
|
*/
|
207
|
-
async
|
208
|
-
logger.debug("[RedisConversationMemoryManager] Getting session", {
|
423
|
+
async getUserSessionMetadata(userId, sessionId) {
|
424
|
+
logger.debug("[RedisConversationMemoryManager] Getting user session metadata", {
|
425
|
+
userId,
|
209
426
|
sessionId,
|
210
|
-
method: "getSession",
|
211
427
|
});
|
212
428
|
await this.ensureInitialized();
|
213
429
|
if (!this.redisClient) {
|
214
|
-
logger.warn("[RedisConversationMemoryManager] Redis client not available", {
|
430
|
+
logger.warn("[RedisConversationMemoryManager] Redis client not available", { userId, sessionId });
|
431
|
+
return null;
|
432
|
+
}
|
433
|
+
try {
|
434
|
+
const sessionKey = getSessionKey(this.redisConfig, sessionId, userId);
|
435
|
+
const conversationData = await this.redisClient.get(sessionKey);
|
436
|
+
if (!conversationData) {
|
437
|
+
logger.debug("[RedisConversationMemoryManager] No session data found", {
|
438
|
+
userId,
|
439
|
+
sessionId,
|
440
|
+
sessionKey,
|
441
|
+
});
|
442
|
+
return null;
|
443
|
+
}
|
444
|
+
// Deserialize conversation object but extract only metadata
|
445
|
+
const conversation = deserializeConversation(conversationData);
|
446
|
+
if (conversation) {
|
447
|
+
return {
|
448
|
+
id: conversation.sessionId,
|
449
|
+
title: conversation.title,
|
450
|
+
createdAt: conversation.createdAt,
|
451
|
+
updatedAt: conversation.updatedAt,
|
452
|
+
};
|
453
|
+
}
|
454
|
+
logger.debug("[RedisConversationMemoryManager] No valid conversation data found", {
|
455
|
+
userId,
|
215
456
|
sessionId,
|
457
|
+
sessionKey,
|
216
458
|
});
|
217
|
-
return
|
459
|
+
return null;
|
218
460
|
}
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
461
|
+
catch (error) {
|
462
|
+
logger.error("[RedisConversationMemoryManager] Failed to get user session metadata", {
|
463
|
+
userId,
|
464
|
+
sessionId,
|
465
|
+
error: error instanceof Error ? error.message : String(error),
|
466
|
+
stack: error instanceof Error ? error.stack : undefined,
|
467
|
+
});
|
468
|
+
return null;
|
469
|
+
}
|
470
|
+
}
|
471
|
+
/**
|
472
|
+
* Get conversation history for a specific user session
|
473
|
+
*
|
474
|
+
* @param userId The user identifier
|
475
|
+
* @param sessionId The session identifier
|
476
|
+
* @returns Array of chat messages or null if session doesn't exist
|
477
|
+
*/
|
478
|
+
async getUserSessionHistory(userId, sessionId) {
|
479
|
+
logger.debug("[RedisConversationMemoryManager] Getting user session history via getUserSessionObject", {
|
480
|
+
userId,
|
226
481
|
sessionId,
|
227
|
-
hasData: !!messagesData,
|
228
|
-
dataLength: messagesData?.length || 0,
|
229
482
|
});
|
230
|
-
|
231
|
-
|
483
|
+
try {
|
484
|
+
const sessionObject = await this.getUserSessionObject(userId, sessionId);
|
485
|
+
if (!sessionObject) {
|
486
|
+
logger.debug("[RedisConversationMemoryManager] No session object found, returning null", {
|
487
|
+
userId,
|
488
|
+
sessionId,
|
489
|
+
});
|
490
|
+
return null;
|
491
|
+
}
|
492
|
+
return sessionObject.messages;
|
493
|
+
}
|
494
|
+
catch (error) {
|
495
|
+
logger.error("[RedisConversationMemoryManager] Failed to get user session history via getUserSessionObject", {
|
496
|
+
userId,
|
232
497
|
sessionId,
|
233
|
-
|
498
|
+
error: error instanceof Error ? error.message : String(error),
|
499
|
+
errorName: error instanceof Error ? error.name : "UnknownError",
|
500
|
+
stack: error instanceof Error ? error.stack : undefined,
|
234
501
|
});
|
235
|
-
return
|
502
|
+
return null;
|
236
503
|
}
|
237
|
-
|
238
|
-
|
504
|
+
}
|
505
|
+
/**
|
506
|
+
* Get the complete conversation object for a specific user session
|
507
|
+
*
|
508
|
+
* This method returns the full conversation object including title, metadata,
|
509
|
+
* timestamps, and all chat messages. Unlike getUserSessionHistory() which returns
|
510
|
+
* only the messages array, this method provides the complete conversation context.
|
511
|
+
*
|
512
|
+
* @param userId The user identifier who owns the session
|
513
|
+
* @param sessionId The unique session identifier
|
514
|
+
* @returns Complete conversation object with all data, or null if session doesn't exist
|
515
|
+
*/
|
516
|
+
async getUserSessionObject(userId, sessionId) {
|
517
|
+
logger.debug("[RedisConversationMemoryManager] Getting complete user session object", {
|
518
|
+
userId,
|
239
519
|
sessionId,
|
240
|
-
|
241
|
-
messageRoles: messages.map((m) => m.role),
|
520
|
+
method: "getUserSessionObject",
|
242
521
|
});
|
243
|
-
//
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
}
|
251
|
-
|
252
|
-
sessionId,
|
253
|
-
|
522
|
+
// Validate input parameters
|
523
|
+
if (!userId || typeof userId !== "string") {
|
524
|
+
logger.warn("[RedisConversationMemoryManager] Invalid userId provided", {
|
525
|
+
userId,
|
526
|
+
sessionId,
|
527
|
+
});
|
528
|
+
return null;
|
529
|
+
}
|
530
|
+
if (!sessionId || typeof sessionId !== "string") {
|
531
|
+
logger.warn("[RedisConversationMemoryManager] Invalid sessionId provided", { userId, sessionId });
|
532
|
+
return null;
|
533
|
+
}
|
534
|
+
await this.ensureInitialized();
|
535
|
+
if (!this.redisClient) {
|
536
|
+
logger.warn("[RedisConversationMemoryManager] Redis client not available for getUserSessionObject", { userId, sessionId });
|
537
|
+
return null;
|
538
|
+
}
|
539
|
+
try {
|
540
|
+
const sessionKey = getSessionKey(this.redisConfig, sessionId, userId);
|
541
|
+
const conversationData = await this.redisClient.get(sessionKey);
|
542
|
+
if (!conversationData) {
|
543
|
+
logger.debug("[RedisConversationMemoryManager] No conversation data found in Redis", {
|
544
|
+
userId,
|
545
|
+
sessionId,
|
546
|
+
sessionKey,
|
547
|
+
});
|
548
|
+
return null;
|
549
|
+
}
|
550
|
+
// Deserialize the complete conversation object
|
551
|
+
const conversation = deserializeConversation(conversationData);
|
552
|
+
if (!conversation) {
|
553
|
+
logger.debug("[RedisConversationMemoryManager] Failed to deserialize conversation data", {
|
554
|
+
userId,
|
555
|
+
sessionId,
|
556
|
+
sessionKey,
|
557
|
+
dataLength: conversationData.length,
|
558
|
+
});
|
559
|
+
return null;
|
560
|
+
}
|
561
|
+
// Validate conversation object structure
|
562
|
+
if (!conversation.messages || !Array.isArray(conversation.messages)) {
|
563
|
+
logger.warn("[RedisConversationMemoryManager] Invalid conversation structure - missing messages array", {
|
564
|
+
userId,
|
565
|
+
sessionId,
|
566
|
+
hasMessages: !!conversation.messages,
|
567
|
+
messagesType: typeof conversation.messages,
|
568
|
+
});
|
569
|
+
return null;
|
570
|
+
}
|
571
|
+
return conversation;
|
572
|
+
}
|
573
|
+
catch (error) {
|
574
|
+
logger.error("[RedisConversationMemoryManager] Failed to get complete user session object", {
|
575
|
+
userId,
|
576
|
+
sessionId,
|
577
|
+
error: error instanceof Error ? error.message : String(error),
|
578
|
+
errorName: error instanceof Error ? error.name : "UnknownError",
|
579
|
+
stack: error instanceof Error ? error.stack : undefined,
|
580
|
+
});
|
581
|
+
return null;
|
582
|
+
}
|
583
|
+
}
|
584
|
+
/**
|
585
|
+
* Generate a conversation title from the first user message
|
586
|
+
* Uses AI to create a concise, descriptive title (5-8 words)
|
587
|
+
*/
|
588
|
+
async generateConversationTitle(userMessage) {
|
589
|
+
logger.debug("[RedisConversationMemoryManager] Generating conversation title", {
|
590
|
+
userMessageLength: userMessage.length,
|
591
|
+
userMessagePreview: userMessage.substring(0, 100),
|
254
592
|
});
|
255
|
-
|
593
|
+
try {
|
594
|
+
// Create a NeuroLink instance for title generation
|
595
|
+
const titleGenerator = new NeuroLink({
|
596
|
+
conversationMemory: { enabled: false },
|
597
|
+
});
|
598
|
+
const titlePrompt = `Generate a short, descriptive title (5-8 words maximum) for a conversation that starts with this user message. The title should capture the main topic or intent. Only return the title, nothing else.
|
599
|
+
|
600
|
+
User message: "${userMessage}"
|
601
|
+
|
602
|
+
Title:`;
|
603
|
+
const result = await titleGenerator.generate({
|
604
|
+
input: { text: titlePrompt },
|
605
|
+
provider: this.config.summarizationProvider || "vertex",
|
606
|
+
model: this.config.summarizationModel || "gemini-2.5-flashb",
|
607
|
+
disableTools: false,
|
608
|
+
});
|
609
|
+
// Clean up the generated title
|
610
|
+
let title = result.content?.trim() || "New Conversation";
|
611
|
+
// Remove common prefixes/suffixes that might be added by the AI
|
612
|
+
title = title.replace(/^(Title:|Here's a title:|The title is:)\s*/i, "");
|
613
|
+
title = title.replace(/['"]/g, ""); // Remove quotes
|
614
|
+
title = title.replace(/\.$/, ""); // Remove trailing period
|
615
|
+
if (title.length > 60) {
|
616
|
+
title = title.substring(0, 57) + "...";
|
617
|
+
}
|
618
|
+
if (title.length < 3) {
|
619
|
+
title = "New Conversation";
|
620
|
+
}
|
621
|
+
logger.debug("[RedisConversationMemoryManager] Generated conversation title", {
|
622
|
+
originalLength: result.content?.length || 0,
|
623
|
+
cleanedTitle: title,
|
624
|
+
titleLength: title.length,
|
625
|
+
});
|
626
|
+
return title;
|
627
|
+
}
|
628
|
+
catch (error) {
|
629
|
+
logger.error("[RedisConversationMemoryManager] Failed to generate conversation title", {
|
630
|
+
error: error instanceof Error ? error.message : String(error),
|
631
|
+
userMessagePreview: userMessage.substring(0, 100),
|
632
|
+
});
|
633
|
+
// Fallback to a simple title based on the user message
|
634
|
+
const fallbackTitle = userMessage.length > 30
|
635
|
+
? userMessage.substring(0, 30) + "..."
|
636
|
+
: userMessage || "New Conversation";
|
637
|
+
return fallbackTitle;
|
638
|
+
}
|
256
639
|
}
|
257
640
|
/**
|
258
641
|
* Create summary system message
|
@@ -292,9 +675,11 @@ export class RedisConversationMemoryManager {
|
|
292
675
|
// Count messages in each session
|
293
676
|
let totalTurns = 0;
|
294
677
|
for (const key of keys) {
|
295
|
-
const
|
296
|
-
const
|
297
|
-
|
678
|
+
const conversationData = await this.redisClient.get(key);
|
679
|
+
const conversation = deserializeConversation(conversationData);
|
680
|
+
if (conversation?.messages) {
|
681
|
+
totalTurns += conversation.messages.length / MESSAGES_PER_TURN;
|
682
|
+
}
|
298
683
|
}
|
299
684
|
return {
|
300
685
|
totalSessions: keys.length,
|
@@ -304,14 +689,18 @@ export class RedisConversationMemoryManager {
|
|
304
689
|
/**
|
305
690
|
* Clear a specific session
|
306
691
|
*/
|
307
|
-
async clearSession(sessionId) {
|
692
|
+
async clearSession(sessionId, userId) {
|
308
693
|
await this.ensureInitialized();
|
309
694
|
if (!this.redisClient) {
|
310
695
|
return false;
|
311
696
|
}
|
312
|
-
const redisKey = getSessionKey(this.redisConfig, sessionId);
|
697
|
+
const redisKey = getSessionKey(this.redisConfig, sessionId, userId);
|
313
698
|
const result = await this.redisClient.del(redisKey);
|
314
699
|
if (result > 0) {
|
700
|
+
// Remove session from user's session set
|
701
|
+
if (userId) {
|
702
|
+
await this.removeUserSession(userId, sessionId);
|
703
|
+
}
|
315
704
|
logger.info("Redis session cleared", { sessionId });
|
316
705
|
return true;
|
317
706
|
}
|
@@ -325,123 +714,196 @@ export class RedisConversationMemoryManager {
|
|
325
714
|
if (!this.redisClient) {
|
326
715
|
return;
|
327
716
|
}
|
328
|
-
const
|
717
|
+
const conversationPattern = `${this.redisConfig.keyPrefix}*`;
|
718
|
+
const userSessionsPattern = `${this.redisConfig.userSessionsKeyPrefix}*`;
|
329
719
|
// Use SCAN instead of KEYS to avoid blocking the server
|
330
|
-
const
|
331
|
-
|
332
|
-
|
333
|
-
|
720
|
+
const conversationKeys = await scanKeys(this.redisClient, conversationPattern);
|
721
|
+
const userSessionsKeys = await scanKeys(this.redisClient, userSessionsPattern);
|
722
|
+
const allKeys = [...conversationKeys, ...userSessionsKeys];
|
723
|
+
logger.debug("[RedisConversationMemoryManager] Got all keys with SCAN for clearing", {
|
724
|
+
conversationPattern,
|
725
|
+
userSessionsPattern,
|
726
|
+
conversationKeyCount: conversationKeys.length,
|
727
|
+
userSessionsKeyCount: userSessionsKeys.length,
|
728
|
+
totalKeyCount: allKeys.length,
|
334
729
|
});
|
335
|
-
if (
|
730
|
+
if (allKeys.length > 0) {
|
336
731
|
// Process keys in batches to avoid blocking Redis for too long
|
337
732
|
const batchSize = 100;
|
338
|
-
for (let i = 0; i <
|
339
|
-
const batch =
|
733
|
+
for (let i = 0; i < allKeys.length; i += batchSize) {
|
734
|
+
const batch = allKeys.slice(i, i + batchSize);
|
340
735
|
await this.redisClient.del(batch);
|
341
|
-
logger.debug("[RedisConversationMemoryManager] Cleared batch of sessions", {
|
736
|
+
logger.debug("[RedisConversationMemoryManager] Cleared batch of sessions and user mappings", {
|
342
737
|
batchIndex: Math.floor(i / batchSize) + 1,
|
343
738
|
batchSize: batch.length,
|
344
739
|
totalProcessed: i + batch.length,
|
345
|
-
totalKeys:
|
740
|
+
totalKeys: allKeys.length,
|
346
741
|
});
|
347
742
|
}
|
348
|
-
logger.info("All Redis sessions cleared", {
|
743
|
+
logger.info("All Redis sessions and user session mappings cleared", {
|
744
|
+
clearedCount: allKeys.length,
|
745
|
+
conversationSessions: conversationKeys.length,
|
746
|
+
userSessionMappings: userSessionsKeys.length,
|
747
|
+
});
|
349
748
|
}
|
350
749
|
}
|
351
750
|
/**
|
352
|
-
*
|
751
|
+
* Ensure Redis client is initialized
|
353
752
|
*/
|
354
|
-
async
|
355
|
-
logger.
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
messageCount: messages.length,
|
360
|
-
messageTypes: messages.map((m) => m.role),
|
361
|
-
});
|
362
|
-
const targetTurns = this.config.summarizationTargetTurns || 10;
|
363
|
-
const splitIndex = Math.max(0, messages.length - targetTurns * MESSAGES_PER_TURN);
|
364
|
-
const messagesToSummarize = messages.slice(0, splitIndex);
|
365
|
-
const recentMessages = messages.slice(splitIndex);
|
366
|
-
if (messagesToSummarize.length === 0) {
|
367
|
-
return;
|
753
|
+
async ensureInitialized() {
|
754
|
+
logger.debug("[RedisConversationMemoryManager] Ensuring initialization");
|
755
|
+
if (!this.isInitialized) {
|
756
|
+
logger.debug("[RedisConversationMemoryManager] Not initialized, initializing now");
|
757
|
+
await this.initialize();
|
368
758
|
}
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
759
|
+
else {
|
760
|
+
logger.debug("[RedisConversationMemoryManager] Already initialized");
|
761
|
+
}
|
762
|
+
}
|
763
|
+
/**
|
764
|
+
* Get session metadata for all sessions of a user (optimized for listing)
|
765
|
+
* Returns only essential metadata without heavy message arrays
|
766
|
+
*
|
767
|
+
* @param userId The user identifier
|
768
|
+
* @returns Array of session metadata objects
|
769
|
+
*/
|
770
|
+
async getUserAllSessionsHistory(userId) {
|
771
|
+
await this.ensureInitialized();
|
772
|
+
if (!this.redisClient) {
|
773
|
+
logger.warn("[RedisConversationMemoryManager] Redis client not available", { userId });
|
774
|
+
return [];
|
775
|
+
}
|
776
|
+
const results = [];
|
373
777
|
try {
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
mappedProvider = "googlevertex";
|
778
|
+
// Get all session IDs for the user using existing method
|
779
|
+
const sessionIds = await this.getUserSessions(userId);
|
780
|
+
if (sessionIds.length === 0) {
|
781
|
+
return results;
|
379
782
|
}
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
783
|
+
// Fetch metadata for each session using our optimized helper method
|
784
|
+
for (const sessionId of sessionIds) {
|
785
|
+
try {
|
786
|
+
const metadata = await this.getUserSessionMetadata(userId, sessionId);
|
787
|
+
if (metadata) {
|
788
|
+
results.push(metadata);
|
789
|
+
}
|
790
|
+
else {
|
791
|
+
logger.debug("[RedisConversationMemoryManager] Empty or missing session metadata", {
|
792
|
+
userId,
|
793
|
+
sessionId,
|
794
|
+
});
|
795
|
+
}
|
796
|
+
}
|
797
|
+
catch (sessionError) {
|
798
|
+
logger.error("[RedisConversationMemoryManager] Failed to get session metadata", {
|
799
|
+
userId,
|
800
|
+
sessionId,
|
801
|
+
error: sessionError instanceof Error
|
802
|
+
? sessionError.message
|
803
|
+
: String(sessionError),
|
804
|
+
});
|
805
|
+
// Continue with other sessions even if one fails
|
806
|
+
continue;
|
404
807
|
}
|
405
|
-
logger.info(`[RedisConversationMemory] Summarization complete for session ${sessionId}.`);
|
406
|
-
}
|
407
|
-
else {
|
408
|
-
logger.warn(`[RedisConversationMemory] Summarization failed for session ${sessionId}. History not modified.`);
|
409
808
|
}
|
809
|
+
return results;
|
410
810
|
}
|
411
811
|
catch (error) {
|
412
|
-
logger.error(
|
812
|
+
logger.error("[RedisConversationMemoryManager] Failed to get user all sessions metadata", {
|
813
|
+
userId,
|
814
|
+
error: error instanceof Error ? error.message : String(error),
|
815
|
+
stack: error instanceof Error ? error.stack : undefined,
|
816
|
+
});
|
817
|
+
return results;
|
413
818
|
}
|
414
819
|
}
|
415
820
|
/**
|
416
|
-
*
|
821
|
+
* Clean up stale pending tool execution data
|
822
|
+
* Removes data older than 5 minutes to prevent memory leaks
|
417
823
|
*/
|
418
|
-
|
419
|
-
const
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
824
|
+
cleanupStalePendingData() {
|
825
|
+
const fiveMinutesAgo = Date.now() - 5 * 60 * 1000;
|
826
|
+
const keysToDelete = [];
|
827
|
+
for (const [key, data] of this.pendingToolExecutions) {
|
828
|
+
if (data.timestamp < fiveMinutesAgo) {
|
829
|
+
keysToDelete.push(key);
|
830
|
+
}
|
831
|
+
}
|
832
|
+
if (keysToDelete.length > 0) {
|
833
|
+
logger.debug("[RedisConversationMemoryManager] Cleaning up stale pending tool data", {
|
834
|
+
stalePendingKeys: keysToDelete.length,
|
835
|
+
totalPendingKeys: this.pendingToolExecutions.size,
|
836
|
+
});
|
837
|
+
keysToDelete.forEach((key) => this.pendingToolExecutions.delete(key));
|
838
|
+
}
|
432
839
|
}
|
433
840
|
/**
|
434
|
-
*
|
841
|
+
* Flush pending tool execution data for a session and merge into conversation
|
435
842
|
*/
|
436
|
-
async
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
843
|
+
async flushPendingToolData(conversation, sessionId, userId) {
|
844
|
+
const pendingKey = `${sessionId}:${userId}`;
|
845
|
+
const pendingData = this.pendingToolExecutions.get(pendingKey);
|
846
|
+
if (!pendingData) {
|
847
|
+
logger.debug("[RedisConversationMemoryManager] No pending tool data to flush", {
|
848
|
+
sessionId,
|
849
|
+
userId,
|
850
|
+
pendingKey,
|
851
|
+
});
|
852
|
+
return;
|
441
853
|
}
|
442
|
-
|
443
|
-
|
854
|
+
logger.debug("[RedisConversationMemoryManager] Flushing pending tool data", {
|
855
|
+
sessionId,
|
856
|
+
userId,
|
857
|
+
toolCallsCount: pendingData.toolCalls.length,
|
858
|
+
toolResultsCount: pendingData.toolResults.length,
|
859
|
+
});
|
860
|
+
// Create a mapping from toolCallId to toolName for matching tool results
|
861
|
+
const toolCallMap = new Map();
|
862
|
+
// Create separate messages for tool calls and build the mapping
|
863
|
+
for (const toolCall of pendingData.toolCalls) {
|
864
|
+
const toolCallId = String(toolCall.toolCallId);
|
865
|
+
const toolName = String(toolCall.toolName);
|
866
|
+
// Store in mapping for tool results
|
867
|
+
toolCallMap.set(toolCallId, toolName);
|
868
|
+
const toolCallMessage = {
|
869
|
+
id: this.generateMessageId(conversation),
|
870
|
+
timestamp: this.generateTimestamp(),
|
871
|
+
role: "tool_call",
|
872
|
+
content: "", // Can be empty for tool calls
|
873
|
+
tool: toolName,
|
874
|
+
args: (toolCall.args ||
|
875
|
+
toolCall.arguments ||
|
876
|
+
toolCall.parameters ||
|
877
|
+
{}),
|
878
|
+
};
|
879
|
+
conversation.messages.push(toolCallMessage);
|
880
|
+
}
|
881
|
+
// Create separate messages for tool results using the mapping
|
882
|
+
for (const toolResult of pendingData.toolResults) {
|
883
|
+
const toolCallId = String(toolResult.toolCallId || toolResult.id || "unknown");
|
884
|
+
const toolName = toolCallMap.get(toolCallId) || "unknown";
|
885
|
+
const toolResultMessage = {
|
886
|
+
id: this.generateMessageId(conversation),
|
887
|
+
timestamp: this.generateTimestamp(),
|
888
|
+
role: "tool_result",
|
889
|
+
content: "", // Can be empty for tool results
|
890
|
+
tool: toolName, // Now correctly extracted from tool call mapping
|
891
|
+
result: {
|
892
|
+
success: !toolResult.error,
|
893
|
+
result: toolResult.result,
|
894
|
+
error: toolResult.error ? String(toolResult.error) : undefined,
|
895
|
+
},
|
896
|
+
};
|
897
|
+
conversation.messages.push(toolResultMessage);
|
444
898
|
}
|
899
|
+
// Remove the pending data now that it's been flushed
|
900
|
+
this.pendingToolExecutions.delete(pendingKey);
|
901
|
+
logger.debug("[RedisConversationMemoryManager] Successfully flushed pending tool data", {
|
902
|
+
sessionId,
|
903
|
+
userId,
|
904
|
+
toolMessagesAdded: pendingData.toolCalls.length + pendingData.toolResults.length,
|
905
|
+
totalMessages: conversation.messages.length,
|
906
|
+
});
|
445
907
|
}
|
446
908
|
/**
|
447
909
|
* Enforce session limit
|