@lobehub/lobehub 2.0.0-next.32 → 2.0.0-next.33

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 (81) hide show
  1. package/.github/workflows/test.yml +1 -0
  2. package/CHANGELOG.md +33 -0
  3. package/apps/desktop/package.json +1 -1
  4. package/changelog/v1.json +12 -0
  5. package/docker-compose/local/.env.example +3 -0
  6. package/docs/self-hosting/server-database/docker-compose.mdx +29 -0
  7. package/docs/self-hosting/server-database/docker-compose.zh-CN.mdx +29 -0
  8. package/package.json +1 -1
  9. package/packages/const/src/hotkeys.ts +3 -3
  10. package/packages/const/src/models.ts +2 -2
  11. package/packages/const/src/utils/merge.ts +3 -3
  12. package/packages/conversation-flow/package.json +13 -0
  13. package/packages/conversation-flow/src/__tests__/fixtures/index.ts +48 -0
  14. package/packages/conversation-flow/src/__tests__/fixtures/inputs/assistant-chain-with-followup.json +56 -0
  15. package/packages/conversation-flow/src/__tests__/fixtures/inputs/assistant-with-tools.json +144 -0
  16. package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/active-index-1.json +131 -0
  17. package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/assistant-branch.json +96 -0
  18. package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/assistant-user-branch.json +123 -0
  19. package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/conversation.json +128 -0
  20. package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/index.ts +14 -0
  21. package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/nested.json +179 -0
  22. package/packages/conversation-flow/src/__tests__/fixtures/inputs/compare/index.ts +8 -0
  23. package/packages/conversation-flow/src/__tests__/fixtures/inputs/compare/simple.json +85 -0
  24. package/packages/conversation-flow/src/__tests__/fixtures/inputs/compare/with-tools.json +169 -0
  25. package/packages/conversation-flow/src/__tests__/fixtures/inputs/complex-scenario.json +107 -0
  26. package/packages/conversation-flow/src/__tests__/fixtures/inputs/index.ts +14 -0
  27. package/packages/conversation-flow/src/__tests__/fixtures/inputs/linear-conversation.json +59 -0
  28. package/packages/conversation-flow/src/__tests__/fixtures/outputs/assistant-chain-with-followup.json +135 -0
  29. package/packages/conversation-flow/src/__tests__/fixtures/outputs/assistant-with-tools.json +340 -0
  30. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/active-index-1.json +242 -0
  31. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/assistant-branch.json +208 -0
  32. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/assistant-user-branch.json +254 -0
  33. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/conversation.json +260 -0
  34. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/index.ts +14 -0
  35. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/nested.json +389 -0
  36. package/packages/conversation-flow/src/__tests__/fixtures/outputs/compare/index.ts +8 -0
  37. package/packages/conversation-flow/src/__tests__/fixtures/outputs/compare/simple.json +224 -0
  38. package/packages/conversation-flow/src/__tests__/fixtures/outputs/compare/with-tools.json +418 -0
  39. package/packages/conversation-flow/src/__tests__/fixtures/outputs/complex-scenario.json +239 -0
  40. package/packages/conversation-flow/src/__tests__/fixtures/outputs/linear-conversation.json +138 -0
  41. package/packages/conversation-flow/src/__tests__/parse.test.ts +97 -0
  42. package/packages/conversation-flow/src/index.ts +17 -0
  43. package/packages/conversation-flow/src/indexing.ts +58 -0
  44. package/packages/conversation-flow/src/parse.ts +53 -0
  45. package/packages/conversation-flow/src/structuring.ts +38 -0
  46. package/packages/conversation-flow/src/transformation/BranchResolver.ts +66 -0
  47. package/packages/conversation-flow/src/transformation/ContextTreeBuilder.ts +292 -0
  48. package/packages/conversation-flow/src/transformation/FlatListBuilder.ts +421 -0
  49. package/packages/conversation-flow/src/transformation/MessageCollector.ts +166 -0
  50. package/packages/conversation-flow/src/transformation/MessageTransformer.ts +177 -0
  51. package/packages/conversation-flow/src/transformation/__tests__/BranchResolver.test.ts +151 -0
  52. package/packages/conversation-flow/src/transformation/__tests__/ContextTreeBuilder.test.ts +385 -0
  53. package/packages/conversation-flow/src/transformation/__tests__/FlatListBuilder.test.ts +511 -0
  54. package/packages/conversation-flow/src/transformation/__tests__/MessageCollector.test.ts +220 -0
  55. package/packages/conversation-flow/src/transformation/__tests__/MessageTransformer.test.ts +287 -0
  56. package/packages/conversation-flow/src/transformation/index.ts +78 -0
  57. package/packages/conversation-flow/src/types/contextTree.ts +65 -0
  58. package/packages/conversation-flow/src/types/flatMessageList.ts +66 -0
  59. package/packages/conversation-flow/src/types/shared.ts +63 -0
  60. package/packages/conversation-flow/src/types.ts +36 -0
  61. package/packages/conversation-flow/vitest.config.mts +10 -0
  62. package/packages/types/src/message/common/metadata.ts +5 -1
  63. package/src/app/[variants]/(main)/chat/@session/features/SessionListContent/List/Item/index.tsx +3 -4
  64. package/src/envs/__tests__/app.test.ts +47 -13
  65. package/src/envs/app.ts +6 -0
  66. package/src/server/routers/async/__tests__/caller.test.ts +333 -0
  67. package/src/server/routers/async/caller.ts +2 -1
  68. package/src/server/routers/lambda/__tests__/integration/message.integration.test.ts +57 -57
  69. package/src/server/routers/lambda/message.ts +2 -2
  70. package/src/server/services/message/__tests__/index.test.ts +4 -4
  71. package/src/server/services/message/index.ts +1 -1
  72. package/src/services/message/index.ts +2 -3
  73. package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChat.test.ts +8 -8
  74. package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChatV2.test.ts +8 -8
  75. package/src/store/chat/slices/aiChat/actions/__tests__/helpers.ts +1 -1
  76. package/src/store/chat/slices/aiChat/actions/generateAIChatV2.ts +1 -1
  77. package/src/store/chat/slices/message/action.test.ts +7 -7
  78. package/src/store/chat/slices/message/action.ts +2 -2
  79. package/src/store/chat/slices/plugin/action.test.ts +7 -7
  80. package/src/store/chat/slices/plugin/action.ts +1 -1
  81. package/packages/context-engine/ARCHITECTURE.md +0 -425
@@ -0,0 +1,421 @@
1
+ import type { AssistantContentBlock, ChatToolPayloadWithResult } from '@lobechat/types';
2
+
3
+ import type { Message, MessageGroupMetadata } from '../types';
4
+ import type { BranchResolver } from './BranchResolver';
5
+ import type { MessageCollector } from './MessageCollector';
6
+ import type { MessageTransformer } from './MessageTransformer';
7
+
8
+ /**
9
+ * FlatListBuilder - Builds flat message list following the active path
10
+ *
11
+ * Handles:
12
+ * 1. Recursive traversal following active branches
13
+ * 2. Creating virtual messages for Compare and AssistantGroup
14
+ * 3. Processing different message types with priority
15
+ */
16
+ export class FlatListBuilder {
17
+ constructor(
18
+ private messageMap: Map<string, Message>,
19
+ private messageGroupMap: Map<string, MessageGroupMetadata>,
20
+ private childrenMap: Map<string | null, string[]>,
21
+ private branchResolver: BranchResolver,
22
+ private messageCollector: MessageCollector,
23
+ private messageTransformer: MessageTransformer,
24
+ ) {}
25
+
26
+ /**
27
+ * Generate flatList from messages array
28
+ * Only includes messages in the active path
29
+ */
30
+ flatten(messages: Message[]): Message[] {
31
+ const flatList: Message[] = [];
32
+ const processedIds = new Set<string>();
33
+
34
+ // Build the active path by traversing from root
35
+ this.buildFlatListRecursive(null, flatList, processedIds, messages);
36
+
37
+ return flatList;
38
+ }
39
+
40
+ /**
41
+ * Recursively build flatList following the active path
42
+ */
43
+ private buildFlatListRecursive(
44
+ parentId: string | null,
45
+ flatList: Message[],
46
+ processedIds: Set<string>,
47
+ allMessages: Message[],
48
+ ): void {
49
+ const children = this.childrenMap.get(parentId) ?? [];
50
+
51
+ for (const childId of children) {
52
+ if (processedIds.has(childId)) continue;
53
+
54
+ const message = this.messageMap.get(childId);
55
+ if (!message) continue;
56
+
57
+ // Priority 1: Compare message group
58
+ const messageGroup = message.groupId ? this.messageGroupMap.get(message.groupId) : undefined;
59
+
60
+ if (messageGroup && messageGroup.mode === 'compare' && !processedIds.has(messageGroup.id)) {
61
+ const groupMembers = this.messageCollector.collectGroupMembers(
62
+ message.groupId!,
63
+ allMessages,
64
+ );
65
+ const compareMessage = this.createCompareMessage(messageGroup, groupMembers);
66
+ flatList.push(compareMessage);
67
+ groupMembers.forEach((m) => processedIds.add(m.id));
68
+ processedIds.add(messageGroup.id);
69
+
70
+ // Continue with active column's children (if any)
71
+ if ((compareMessage as any).activeColumnId) {
72
+ this.buildFlatListRecursive(
73
+ (compareMessage as any).activeColumnId,
74
+ flatList,
75
+ processedIds,
76
+ allMessages,
77
+ );
78
+ }
79
+ continue;
80
+ }
81
+
82
+ // Priority 2: AssistantGroup (assistant + tools)
83
+ if (message.role === 'assistant' && message.tools && message.tools.length > 0) {
84
+ // Collect the entire assistant group chain
85
+ const assistantChain: Message[] = [];
86
+ const allToolMessages: Message[] = [];
87
+ this.messageCollector.collectAssistantChain(
88
+ message,
89
+ allMessages,
90
+ assistantChain,
91
+ allToolMessages,
92
+ processedIds,
93
+ );
94
+
95
+ // Create assistantGroup virtual message
96
+ const groupMessage = this.createAssistantGroupMessage(
97
+ assistantChain[0],
98
+ assistantChain,
99
+ allToolMessages,
100
+ );
101
+ flatList.push(groupMessage);
102
+
103
+ // Mark all as processed
104
+ assistantChain.forEach((m) => processedIds.add(m.id));
105
+ allToolMessages.forEach((m) => processedIds.add(m.id));
106
+
107
+ // Continue after the assistant chain
108
+ // Priority 1: If last assistant has non-tool children, continue from it
109
+ // Priority 2: Otherwise continue from tools (for cases where user replies to tool)
110
+ const lastAssistant = assistantChain.at(-1);
111
+ const toolIds = new Set(allToolMessages.map((t) => t.id));
112
+
113
+ const lastAssistantNonToolChildren = lastAssistant
114
+ ? this.childrenMap.get(lastAssistant.id)?.filter((childId) => !toolIds.has(childId))
115
+ : undefined;
116
+
117
+ if (
118
+ lastAssistantNonToolChildren &&
119
+ lastAssistantNonToolChildren.length > 0 &&
120
+ lastAssistant
121
+ ) {
122
+ // Follow-up messages exist after the last assistant (not tools)
123
+ this.buildFlatListRecursive(lastAssistant.id, flatList, processedIds, allMessages);
124
+ } else {
125
+ // No non-tool children of last assistant, check tools for children
126
+ for (const toolMsg of allToolMessages) {
127
+ this.buildFlatListRecursive(toolMsg.id, flatList, processedIds, allMessages);
128
+ }
129
+ }
130
+ continue;
131
+ }
132
+
133
+ // Priority 3a: Compare mode from user message metadata
134
+ const childMessages = this.childrenMap.get(message.id) ?? [];
135
+ if (this.isCompareMode(message) && childMessages.length > 1) {
136
+ // Add user message
137
+ flatList.push(message);
138
+ processedIds.add(message.id);
139
+
140
+ // Create compare virtual message with proper handling of AssistantGroups
141
+ const compareMessage = this.createCompareMessageFromChildIds(
142
+ message,
143
+ childMessages,
144
+ allMessages,
145
+ processedIds,
146
+ );
147
+ flatList.push(compareMessage);
148
+
149
+ // Continue with active column's children (if any)
150
+ if ((compareMessage as any).activeColumnId) {
151
+ this.buildFlatListRecursive(
152
+ (compareMessage as any).activeColumnId,
153
+ flatList,
154
+ processedIds,
155
+ allMessages,
156
+ );
157
+ }
158
+ continue;
159
+ }
160
+
161
+ // Priority 3b: User message with branches
162
+ if (message.role === 'user' && childMessages.length > 1) {
163
+ const activeBranchId = this.branchResolver.getActiveBranchIdFromMetadata(
164
+ message,
165
+ childMessages,
166
+ this.childrenMap,
167
+ );
168
+ const userWithBranches = this.createUserMessageWithBranches(message);
169
+ flatList.push(userWithBranches);
170
+ processedIds.add(message.id);
171
+
172
+ // Continue with active branch and process its message
173
+ const activeBranchMsg = this.messageMap.get(activeBranchId);
174
+ if (activeBranchMsg) {
175
+ flatList.push(activeBranchMsg);
176
+ processedIds.add(activeBranchId);
177
+
178
+ // Continue with active branch's children
179
+ this.buildFlatListRecursive(activeBranchId, flatList, processedIds, allMessages);
180
+ }
181
+ continue;
182
+ }
183
+
184
+ // Priority 3c: Assistant message with branches
185
+ if (message.role === 'assistant' && childMessages.length > 1) {
186
+ const activeBranchId = this.branchResolver.getActiveBranchIdFromMetadata(
187
+ message,
188
+ childMessages,
189
+ this.childrenMap,
190
+ );
191
+ // Add the assistant message itself
192
+ flatList.push(message);
193
+ processedIds.add(message.id);
194
+
195
+ // Continue with active branch and process its message
196
+ const activeBranchMsg = this.messageMap.get(activeBranchId);
197
+ if (activeBranchMsg) {
198
+ flatList.push(activeBranchMsg);
199
+ processedIds.add(activeBranchId);
200
+
201
+ // Continue with active branch's children
202
+ this.buildFlatListRecursive(activeBranchId, flatList, processedIds, allMessages);
203
+ }
204
+ continue;
205
+ }
206
+
207
+ // Priority 4: Regular message
208
+ flatList.push(message);
209
+ processedIds.add(message.id);
210
+
211
+ // Continue with children
212
+ this.buildFlatListRecursive(message.id, flatList, processedIds, allMessages);
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Check if message has compare mode in metadata
218
+ */
219
+ private isCompareMode(message: Message): boolean {
220
+ return (message.metadata as any)?.compare === true;
221
+ }
222
+
223
+ /**
224
+ * Create compare virtual message from child IDs with AssistantGroup support
225
+ */
226
+ private createCompareMessageFromChildIds(
227
+ parentMessage: Message,
228
+ childIds: string[],
229
+ allMessages: Message[],
230
+ processedIds: Set<string>,
231
+ ): Message {
232
+ const columns: Message[][] = [];
233
+ const columnFirstIds: string[] = [];
234
+ let activeColumnId: string | undefined;
235
+
236
+ // Process each child (column)
237
+ for (const childId of childIds) {
238
+ const childMessage = this.messageMap.get(childId);
239
+ if (!childMessage) continue;
240
+
241
+ columnFirstIds.push(childId);
242
+
243
+ // Check if this message is marked as active column
244
+ if ((childMessage.metadata as any)?.activeColumn === true) {
245
+ activeColumnId = childId;
246
+ }
247
+
248
+ // Check if this child is an AssistantGroup
249
+ if (
250
+ childMessage.role === 'assistant' &&
251
+ childMessage.tools &&
252
+ childMessage.tools.length > 0
253
+ ) {
254
+ // Collect the entire assistant group chain for this column
255
+ const assistantChain: Message[] = [];
256
+ const allToolMessages: Message[] = [];
257
+ const columnProcessedIds = new Set<string>();
258
+
259
+ this.messageCollector.collectAssistantChain(
260
+ childMessage,
261
+ allMessages,
262
+ assistantChain,
263
+ allToolMessages,
264
+ columnProcessedIds,
265
+ );
266
+
267
+ // Create assistantGroup virtual message for this column
268
+ const groupMessage = this.createAssistantGroupMessage(
269
+ assistantChain[0],
270
+ assistantChain,
271
+ allToolMessages,
272
+ );
273
+
274
+ columns.push([groupMessage]);
275
+
276
+ // Mark all as processed
277
+ assistantChain.forEach((m) => processedIds.add(m.id));
278
+ allToolMessages.forEach((m) => processedIds.add(m.id));
279
+ } else {
280
+ // Regular message (not an AssistantGroup)
281
+ columns.push([childMessage]);
282
+ processedIds.add(childId);
283
+ }
284
+ }
285
+
286
+ // Generate ID with all column first message IDs
287
+ const columnIdsStr = columnFirstIds.join('-');
288
+ const compareId = `compare-${parentMessage.id}-${columnIdsStr}`;
289
+
290
+ // Calculate timestamps from first column's messages
291
+ const firstColumnMessages = childIds.map((id) => this.messageMap.get(id)).filter(Boolean);
292
+ const createdAt =
293
+ firstColumnMessages.length > 0
294
+ ? Math.min(...firstColumnMessages.map((m) => m!.createdAt))
295
+ : parentMessage.createdAt;
296
+ const updatedAt =
297
+ firstColumnMessages.length > 0
298
+ ? Math.max(...firstColumnMessages.map((m) => m!.updatedAt))
299
+ : parentMessage.updatedAt;
300
+
301
+ return {
302
+ activeColumnId,
303
+ columns: columns as any,
304
+ content: '',
305
+ createdAt,
306
+ extra: {
307
+ parentMessageId: parentMessage.id,
308
+ },
309
+ id: compareId,
310
+ meta: parentMessage.meta || {},
311
+ role: 'compare' as any,
312
+ updatedAt,
313
+ } as Message;
314
+ }
315
+
316
+ /**
317
+ * Create compare virtual message (for group-based compare)
318
+ */
319
+ private createCompareMessage(group: MessageGroupMetadata, members: Message[]): Message {
320
+ // Find active column ID from members metadata
321
+ const activeColumnId = members.find((msg) => (msg.metadata as any)?.activeColumn === true)?.id;
322
+
323
+ // columns contain full Message objects
324
+ const columns: Message[][] = members.map((msg) => [msg]);
325
+
326
+ return {
327
+ activeColumnId,
328
+ columns: columns as any,
329
+ content: '',
330
+ createdAt: Math.min(...members.map((m) => m.createdAt)),
331
+ extra: {
332
+ groupMode: group.mode,
333
+ parentMessageId: group.parentMessageId,
334
+ },
335
+ id: group.id,
336
+ meta: members[0]?.meta || {},
337
+ role: 'compare' as any,
338
+ updatedAt: Math.max(...members.map((m) => m.updatedAt)),
339
+ } as Message;
340
+ }
341
+
342
+ /**
343
+ * Create assistant group virtual message from entire chain
344
+ */
345
+ private createAssistantGroupMessage(
346
+ firstAssistant: Message,
347
+ assistantChain: Message[],
348
+ allToolMessages: Message[],
349
+ ): Message {
350
+ const children: AssistantContentBlock[] = [];
351
+
352
+ // Create tool map for lookup
353
+ const toolMap = new Map<string, Message>();
354
+ allToolMessages.forEach((tm) => {
355
+ if (tm.tool_call_id) {
356
+ toolMap.set(tm.tool_call_id, tm);
357
+ }
358
+ });
359
+
360
+ // Process each assistant in the chain
361
+ for (const assistant of assistantChain) {
362
+ // Build toolsWithResults for this assistant
363
+ const toolsWithResults: ChatToolPayloadWithResult[] =
364
+ assistant.tools?.map((tool) => {
365
+ const toolMsg = toolMap.get(tool.id);
366
+ if (toolMsg) {
367
+ return {
368
+ ...tool,
369
+ result: {
370
+ content: toolMsg.content || '',
371
+ error: toolMsg.error,
372
+ id: toolMsg.id,
373
+ state: toolMsg.pluginState,
374
+ },
375
+ result_msg_id: toolMsg.id,
376
+ };
377
+ }
378
+ return tool;
379
+ }) || [];
380
+
381
+ const { usage: msgUsage, performance: msgPerformance } =
382
+ this.messageTransformer.splitMetadata(assistant.metadata);
383
+
384
+ children.push({
385
+ content: assistant.content || '',
386
+ error: assistant.error,
387
+ id: assistant.id,
388
+ imageList:
389
+ assistant.imageList && assistant.imageList.length > 0 ? assistant.imageList : undefined,
390
+ performance: msgPerformance,
391
+ reasoning: assistant.reasoning || undefined,
392
+ tools: toolsWithResults.length > 0 ? toolsWithResults : undefined,
393
+ usage: msgUsage,
394
+ });
395
+ }
396
+
397
+ const aggregated = this.messageTransformer.aggregateMetadata(children);
398
+
399
+ return {
400
+ ...firstAssistant,
401
+ children,
402
+ content: '',
403
+ imageList: undefined,
404
+ metadata: undefined,
405
+ performance: aggregated.performance,
406
+ reasoning: undefined,
407
+ role: 'assistantGroup' as any,
408
+ tools: undefined,
409
+ usage: aggregated.usage,
410
+ };
411
+ }
412
+
413
+ /**
414
+ * Create user message with branch metadata
415
+ */
416
+ private createUserMessageWithBranches(user: Message): Message {
417
+ // Just return the original user message with its metadata.activeBranchId
418
+ // No need to add extra.branches
419
+ return { ...user };
420
+ }
421
+ }
@@ -0,0 +1,166 @@
1
+ import type { ContextNode, IdNode, Message, MessageNode } from '../types';
2
+
3
+ /**
4
+ * MessageCollector - Handles collection of related messages
5
+ *
6
+ * Provides utilities for:
7
+ * 1. Collecting messages in a group
8
+ * 2. Collecting tool messages
9
+ * 3. Collecting assistant chains
10
+ * 4. Finding next messages in sequences
11
+ */
12
+ export class MessageCollector {
13
+ constructor(
14
+ private messageMap: Map<string, Message>,
15
+ private childrenMap: Map<string | null, string[]>,
16
+ ) {}
17
+
18
+ /**
19
+ * Collect all messages belonging to a message group
20
+ */
21
+ collectGroupMembers(groupId: string, messages: Message[]): Message[] {
22
+ return messages.filter((m) => m.groupId === groupId);
23
+ }
24
+
25
+ /**
26
+ * Collect tool messages related to an assistant message
27
+ */
28
+ collectToolMessages(assistant: Message, messages: Message[]): Message[] {
29
+ const toolCallIds = new Set(assistant.tools?.map((t) => t.id) || []);
30
+ return messages.filter(
31
+ (m) => m.role === 'tool' && m.tool_call_id && toolCallIds.has(m.tool_call_id),
32
+ );
33
+ }
34
+
35
+ /**
36
+ * Recursively collect the entire assistant chain
37
+ * (assistant -> tools -> assistant -> tools -> ...)
38
+ */
39
+ collectAssistantChain(
40
+ currentAssistant: Message,
41
+ allMessages: Message[],
42
+ assistantChain: Message[],
43
+ allToolMessages: Message[],
44
+ processedIds: Set<string>,
45
+ ): void {
46
+ if (processedIds.has(currentAssistant.id)) return;
47
+
48
+ // Add current assistant to chain
49
+ assistantChain.push(currentAssistant);
50
+
51
+ // Collect its tool messages
52
+ const toolMessages = this.collectToolMessages(currentAssistant, allMessages);
53
+ allToolMessages.push(...toolMessages);
54
+
55
+ // Find next assistant after tools
56
+ for (const toolMsg of toolMessages) {
57
+ const nextMessages = allMessages.filter((m) => m.parentId === toolMsg.id);
58
+
59
+ for (const nextMsg of nextMessages) {
60
+ if (nextMsg.role === 'assistant' && nextMsg.tools && nextMsg.tools.length > 0) {
61
+ // Continue the chain
62
+ this.collectAssistantChain(
63
+ nextMsg,
64
+ allMessages,
65
+ assistantChain,
66
+ allToolMessages,
67
+ processedIds,
68
+ );
69
+ return;
70
+ } else if (nextMsg.role === 'assistant') {
71
+ // Final assistant without tools
72
+ assistantChain.push(nextMsg);
73
+ return;
74
+ }
75
+ }
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Recursively collect assistant messages for an AssistantGroup (contextTree version)
81
+ */
82
+ collectAssistantGroupMessages(
83
+ message: Message,
84
+ idNode: IdNode,
85
+ children: ContextNode[],
86
+ ): void {
87
+ // Get tool message IDs if this assistant has tools
88
+ const toolIds = idNode.children
89
+ .filter((child) => {
90
+ const childMsg = this.messageMap.get(child.id);
91
+ return childMsg?.role === 'tool';
92
+ })
93
+ .map((child) => child.id);
94
+
95
+ // Add current assistant message node
96
+ const messageNode: MessageNode = {
97
+ id: message.id,
98
+ type: 'message',
99
+ };
100
+ if (toolIds.length > 0) {
101
+ messageNode.tools = toolIds;
102
+ }
103
+ children.push(messageNode);
104
+
105
+ // Find next assistant message after tools
106
+ for (const toolNode of idNode.children) {
107
+ const toolMsg = this.messageMap.get(toolNode.id);
108
+ if (toolMsg?.role !== 'tool') continue;
109
+
110
+ // Check if tool has an assistant child
111
+ if (toolNode.children.length > 0) {
112
+ const nextChild = toolNode.children[0];
113
+ const nextMsg = this.messageMap.get(nextChild.id);
114
+
115
+ if (nextMsg?.role === 'assistant') {
116
+ // Recursively collect this assistant and its descendants
117
+ this.collectAssistantGroupMessages(nextMsg, nextChild, children);
118
+ return; // Only follow one path
119
+ }
120
+ }
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Find next message after tools in an assistant group
126
+ */
127
+ findNextAfterTools(assistantMsg: Message, idNode: IdNode): IdNode | null {
128
+ // Recursively find the last message in the assistant group
129
+ const lastNode = this.findLastNodeInAssistantGroup(idNode);
130
+ if (lastNode && lastNode.children.length > 0) {
131
+ return lastNode.children[0];
132
+ }
133
+ return null;
134
+ }
135
+
136
+ /**
137
+ * Find the last node in an AssistantGroup sequence
138
+ */
139
+ findLastNodeInAssistantGroup(idNode: IdNode): IdNode | null {
140
+ // Check if has tool children
141
+ const toolChildren = idNode.children.filter((child) => {
142
+ const childMsg = this.messageMap.get(child.id);
143
+ return childMsg?.role === 'tool';
144
+ });
145
+
146
+ if (toolChildren.length === 0) {
147
+ return idNode;
148
+ }
149
+
150
+ // Check if any tool has an assistant child
151
+ for (const toolNode of toolChildren) {
152
+ if (toolNode.children.length > 0) {
153
+ const nextChild = toolNode.children[0];
154
+ const nextMsg = this.messageMap.get(nextChild.id);
155
+
156
+ if (nextMsg?.role === 'assistant') {
157
+ // Continue following the assistant chain
158
+ return this.findLastNodeInAssistantGroup(nextChild);
159
+ }
160
+ }
161
+ }
162
+
163
+ // No more assistant messages, return the last tool node
164
+ return toolChildren.at(-1) ?? null;
165
+ }
166
+ }