@lobehub/lobehub 2.0.0-next.35 → 2.0.0-next.37

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 (156) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v1.json +18 -0
  3. package/next.config.ts +5 -6
  4. package/package.json +2 -2
  5. package/packages/agent-runtime/src/core/__tests__/runtime.test.ts +112 -77
  6. package/packages/agent-runtime/src/core/runtime.ts +63 -18
  7. package/packages/agent-runtime/src/types/generalAgent.ts +55 -0
  8. package/packages/agent-runtime/src/types/index.ts +1 -0
  9. package/packages/agent-runtime/src/types/instruction.ts +10 -3
  10. package/packages/const/src/user.ts +0 -1
  11. package/packages/context-engine/src/processors/GroupMessageFlatten.ts +8 -6
  12. package/packages/context-engine/src/processors/__tests__/GroupMessageFlatten.test.ts +12 -12
  13. package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/assistant-group-branches.json +249 -0
  14. package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/index.ts +4 -0
  15. package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/multi-assistant-group.json +260 -0
  16. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/active-index-1.json +4 -0
  17. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/assistant-group-branches.json +481 -0
  18. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/conversation.json +5 -1
  19. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/index.ts +4 -0
  20. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/multi-assistant-group.json +407 -0
  21. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/nested.json +18 -2
  22. package/packages/conversation-flow/src/__tests__/fixtures/outputs/complex-scenario.json +25 -3
  23. package/packages/conversation-flow/src/__tests__/parse.test.ts +12 -0
  24. package/packages/conversation-flow/src/index.ts +1 -1
  25. package/packages/conversation-flow/src/transformation/FlatListBuilder.ts +112 -34
  26. package/packages/conversation-flow/src/types/flatMessageList.ts +0 -12
  27. package/packages/conversation-flow/src/{types.ts → types/index.ts} +3 -14
  28. package/packages/database/src/models/__tests__/apiKey.test.ts +444 -0
  29. package/packages/database/src/models/message.ts +18 -19
  30. package/packages/types/src/aiChat.ts +2 -0
  31. package/packages/types/src/importer.ts +2 -2
  32. package/packages/types/src/message/ui/chat.ts +17 -1
  33. package/packages/types/src/message/ui/extra.ts +2 -2
  34. package/packages/types/src/message/ui/params.ts +2 -2
  35. package/packages/types/src/user/preference.ts +0 -4
  36. package/packages/utils/src/tokenizer/index.ts +3 -11
  37. package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/Desktop/MessageFromUrl.tsx +3 -3
  38. package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/V1Mobile/index.tsx +1 -1
  39. package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/V1Mobile/useSend.ts +3 -3
  40. package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/useSend.ts +6 -6
  41. package/src/app/[variants]/(main)/chat/components/conversation/features/ChatList/Content.tsx +5 -3
  42. package/src/app/[variants]/(main)/chat/components/conversation/features/ChatList/WelcomeChatItem/AgentWelcome/OpeningQuestions.tsx +2 -2
  43. package/src/app/[variants]/(main)/chat/components/conversation/features/ChatList/WelcomeChatItem/GroupWelcome/GroupUsageSuggest.tsx +2 -2
  44. package/src/app/[variants]/(main)/labs/page.tsx +0 -9
  45. package/src/features/ChatInput/ActionBar/STT/browser.tsx +3 -3
  46. package/src/features/ChatInput/ActionBar/STT/openai.tsx +3 -3
  47. package/src/features/Conversation/Error/AccessCodeForm.tsx +1 -1
  48. package/src/features/Conversation/Error/ChatInvalidApiKey.tsx +1 -1
  49. package/src/features/Conversation/Error/ClerkLogin/index.tsx +1 -1
  50. package/src/features/Conversation/Error/OAuthForm.tsx +1 -1
  51. package/src/features/Conversation/Error/index.tsx +0 -5
  52. package/src/features/Conversation/Messages/Assistant/Actions/index.tsx +13 -10
  53. package/src/features/Conversation/Messages/Assistant/Extra/index.test.tsx +3 -8
  54. package/src/features/Conversation/Messages/Assistant/Extra/index.tsx +2 -6
  55. package/src/features/Conversation/Messages/Assistant/MessageContent.tsx +7 -9
  56. package/src/features/Conversation/Messages/Assistant/Tool/Inspector/PluginResult.tsx +2 -2
  57. package/src/features/Conversation/Messages/Assistant/Tool/Inspector/PluginState.tsx +2 -2
  58. package/src/features/Conversation/Messages/Assistant/Tool/Render/PluginSettings.tsx +4 -1
  59. package/src/features/Conversation/Messages/Assistant/Tool/Render/index.tsx +2 -3
  60. package/src/features/Conversation/Messages/Assistant/index.tsx +57 -60
  61. package/src/features/Conversation/Messages/Default.tsx +1 -0
  62. package/src/features/Conversation/Messages/Group/Actions/WithContentId.tsx +38 -10
  63. package/src/features/Conversation/Messages/Group/Actions/index.tsx +1 -1
  64. package/src/features/Conversation/Messages/Group/ContentBlock.tsx +1 -3
  65. package/src/features/Conversation/Messages/Group/GroupChildren.tsx +12 -12
  66. package/src/features/Conversation/Messages/Group/MessageContent.tsx +7 -1
  67. package/src/features/Conversation/Messages/Group/Tool/Render/PluginSettings.tsx +1 -1
  68. package/src/features/Conversation/Messages/Group/index.tsx +2 -1
  69. package/src/features/Conversation/Messages/Supervisor/index.tsx +2 -2
  70. package/src/features/Conversation/Messages/User/{Actions.tsx → Actions/ActionsBar.tsx} +26 -25
  71. package/src/features/Conversation/Messages/User/Actions/MessageBranch.tsx +107 -0
  72. package/src/features/Conversation/Messages/User/Actions/index.tsx +42 -0
  73. package/src/features/Conversation/Messages/User/index.tsx +43 -44
  74. package/src/features/Conversation/Messages/index.tsx +3 -3
  75. package/src/features/Conversation/components/AutoScroll.tsx +3 -3
  76. package/src/features/Conversation/components/Extras/Usage/UsageDetail/AnimatedNumber.tsx +55 -0
  77. package/src/features/Conversation/components/Extras/Usage/UsageDetail/index.tsx +5 -2
  78. package/src/features/Conversation/components/VirtualizedList/index.tsx +29 -20
  79. package/src/features/Conversation/hooks/useChatListActionsBar.tsx +8 -10
  80. package/src/features/Portal/Thread/Chat/ChatInput/useSend.ts +3 -3
  81. package/src/hooks/useHotkeys/chatScope.ts +15 -7
  82. package/src/libs/trpc/client/lambda.ts +4 -3
  83. package/src/server/routers/lambda/__tests__/aiChat.test.ts +1 -1
  84. package/src/server/routers/lambda/__tests__/integration/message.integration.test.ts +0 -26
  85. package/src/server/routers/lambda/aiChat.ts +3 -2
  86. package/src/server/routers/lambda/message.ts +8 -16
  87. package/src/server/services/message/__tests__/index.test.ts +29 -39
  88. package/src/server/services/message/index.ts +41 -36
  89. package/src/services/electron/desktopNotification.ts +6 -6
  90. package/src/services/electron/file.ts +6 -6
  91. package/src/services/file/ClientS3/index.ts +8 -8
  92. package/src/services/message/__tests__/metadata-race-condition.test.ts +157 -0
  93. package/src/services/message/index.ts +21 -15
  94. package/src/services/upload.ts +11 -11
  95. package/src/services/utils/abortableRequest.test.ts +161 -0
  96. package/src/services/utils/abortableRequest.ts +67 -0
  97. package/src/store/chat/agents/GeneralChatAgent.ts +137 -0
  98. package/src/store/chat/agents/createAgentExecutors.ts +395 -0
  99. package/src/store/chat/helpers.test.ts +0 -99
  100. package/src/store/chat/helpers.ts +0 -11
  101. package/src/store/chat/slices/aiChat/actions/__tests__/conversationControl.test.ts +332 -0
  102. package/src/store/chat/slices/aiChat/actions/__tests__/conversationLifecycle.test.ts +257 -0
  103. package/src/store/chat/slices/aiChat/actions/__tests__/helpers.ts +11 -2
  104. package/src/store/chat/slices/aiChat/actions/__tests__/rag.test.ts +6 -6
  105. package/src/store/chat/slices/aiChat/actions/__tests__/streamingExecutor.test.ts +391 -0
  106. package/src/store/chat/slices/aiChat/actions/__tests__/streamingStates.test.ts +179 -0
  107. package/src/store/chat/slices/aiChat/actions/conversationControl.ts +157 -0
  108. package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +329 -0
  109. package/src/store/chat/slices/aiChat/actions/generateAIGroupChat.ts +14 -14
  110. package/src/store/chat/slices/aiChat/actions/index.ts +12 -6
  111. package/src/store/chat/slices/aiChat/actions/rag.ts +9 -6
  112. package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +604 -0
  113. package/src/store/chat/slices/aiChat/actions/streamingStates.ts +84 -0
  114. package/src/store/chat/slices/builtinTool/actions/__tests__/localSystem.test.ts +4 -4
  115. package/src/store/chat/slices/builtinTool/actions/__tests__/search.test.ts +11 -11
  116. package/src/store/chat/slices/builtinTool/actions/interpreter.ts +8 -8
  117. package/src/store/chat/slices/builtinTool/actions/localSystem.ts +2 -2
  118. package/src/store/chat/slices/builtinTool/actions/search.ts +8 -8
  119. package/src/store/chat/slices/message/action.test.ts +79 -68
  120. package/src/store/chat/slices/message/actions/index.ts +39 -0
  121. package/src/store/chat/slices/message/actions/internals.ts +77 -0
  122. package/src/store/chat/slices/message/actions/optimisticUpdate.ts +260 -0
  123. package/src/store/chat/slices/message/actions/publicApi.ts +224 -0
  124. package/src/store/chat/slices/message/actions/query.ts +120 -0
  125. package/src/store/chat/slices/message/actions/runtimeState.ts +108 -0
  126. package/src/store/chat/slices/message/initialState.ts +13 -0
  127. package/src/store/chat/slices/message/reducer.test.ts +48 -370
  128. package/src/store/chat/slices/message/reducer.ts +17 -81
  129. package/src/store/chat/slices/message/selectors/chat.test.ts +13 -50
  130. package/src/store/chat/slices/message/selectors/chat.ts +78 -242
  131. package/src/store/chat/slices/message/selectors/dbMessage.ts +140 -0
  132. package/src/store/chat/slices/message/selectors/displayMessage.ts +301 -0
  133. package/src/store/chat/slices/message/selectors/messageState.ts +5 -2
  134. package/src/store/chat/slices/plugin/action.test.ts +62 -64
  135. package/src/store/chat/slices/plugin/action.ts +34 -28
  136. package/src/store/chat/slices/thread/action.test.ts +28 -31
  137. package/src/store/chat/slices/thread/action.ts +13 -10
  138. package/src/store/chat/slices/thread/selectors/index.ts +8 -6
  139. package/src/store/chat/slices/topic/reducer.ts +11 -3
  140. package/src/store/chat/store.ts +1 -1
  141. package/src/store/user/slices/preference/selectors/labPrefer.ts +0 -3
  142. package/packages/database/src/models/__tests__/message.grouping.test.ts +0 -812
  143. package/packages/database/src/utils/__tests__/groupMessages.test.ts +0 -1132
  144. package/packages/database/src/utils/groupMessages.ts +0 -361
  145. package/packages/utils/src/tokenizer/client.ts +0 -35
  146. package/packages/utils/src/tokenizer/estimated.ts +0 -4
  147. package/packages/utils/src/tokenizer/server.ts +0 -11
  148. package/packages/utils/src/tokenizer/tokenizer.worker.ts +0 -12
  149. package/src/app/(backend)/webapi/tokenizer/index.test.ts +0 -32
  150. package/src/app/(backend)/webapi/tokenizer/route.ts +0 -8
  151. package/src/features/Conversation/Error/InvalidAccessCode.tsx +0 -79
  152. package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChat.test.ts +0 -975
  153. package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChatV2.test.ts +0 -1050
  154. package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +0 -720
  155. package/src/store/chat/slices/aiChat/actions/generateAIChatV2.ts +0 -849
  156. package/src/store/chat/slices/message/action.ts +0 -629
@@ -0,0 +1,604 @@
1
+ /* eslint-disable sort-keys-fix/sort-keys-fix, typescript-sort-keys/interface */
2
+ // Disable the auto sort key eslint rule to make the code more logic and readable
3
+ import { AgentRuntime, type AgentRuntimeContext } from '@lobechat/agent-runtime';
4
+ import { isDesktop } from '@lobechat/const';
5
+ import { knowledgeBaseQAPrompts } from '@lobechat/prompts';
6
+ import {
7
+ ChatImageItem,
8
+ ChatToolPayload,
9
+ MessageToolCall,
10
+ ModelUsage,
11
+ TraceNameMap,
12
+ UIChatMessage,
13
+ } from '@lobechat/types';
14
+ import type { MessageSemanticSearchChunk } from '@lobechat/types';
15
+ import debug from 'debug';
16
+ import { t } from 'i18next';
17
+ import { throttle } from 'lodash-es';
18
+ import { StateCreator } from 'zustand/vanilla';
19
+
20
+ import { chatService } from '@/services/chat';
21
+ import { messageService } from '@/services/message';
22
+ import { agentChatConfigSelectors, agentSelectors } from '@/store/agent/selectors';
23
+ import { getAgentStoreState } from '@/store/agent/store';
24
+ import { GeneralChatAgent } from '@/store/chat/agents/GeneralChatAgent';
25
+ import { createAgentExecutors } from '@/store/chat/agents/createAgentExecutors';
26
+ import { ChatStore } from '@/store/chat/store';
27
+ import { getFileStoreState } from '@/store/file/store';
28
+ import { setNamespace } from '@/utils/storeDebug';
29
+
30
+ import { topicSelectors } from '../../../selectors';
31
+ import { messageMapKey } from '../../../utils/messageMapKey';
32
+
33
+ const n = setNamespace('ai');
34
+ const log = debug('lobe-store:streaming-executor');
35
+
36
+ interface ProcessMessageParams {
37
+ traceId?: string;
38
+ isWelcomeQuestion?: boolean;
39
+ inSearchWorkflow?: boolean;
40
+ /**
41
+ * the RAG query content, should be embedding and used in the semantic search
42
+ */
43
+ ragQuery?: string;
44
+ threadId?: string;
45
+ inPortalThread?: boolean;
46
+
47
+ groupId?: string;
48
+ agentId?: string;
49
+ agentConfig?: any; // Agent configuration for group chat agents
50
+ }
51
+
52
+ /**
53
+ * Core streaming execution actions for AI chat
54
+ */
55
+ export interface StreamingExecutorAction {
56
+ /**
57
+ * Retrieves an AI-generated chat message from the backend service with streaming
58
+ */
59
+ internal_fetchAIChatMessage: (input: {
60
+ messages: UIChatMessage[];
61
+ messageId: string;
62
+ params?: ProcessMessageParams;
63
+ model: string;
64
+ provider: string;
65
+ }) => Promise<{
66
+ isFunctionCall: boolean;
67
+ tools?: ChatToolPayload[];
68
+ tool_calls?: MessageToolCall[];
69
+ content: string;
70
+ traceId?: string;
71
+ usage?: ModelUsage;
72
+ }>;
73
+ /**
74
+ * Executes the core processing logic for AI messages
75
+ * including preprocessing and postprocessing steps
76
+ */
77
+ internal_execAgentRuntime: (params: {
78
+ messages: UIChatMessage[];
79
+ parentMessageId: string;
80
+ parentMessageType: 'user' | 'assistant';
81
+ inSearchWorkflow?: boolean;
82
+ /**
83
+ * the RAG query content, should be embedding and used in the semantic search
84
+ */
85
+ ragQuery?: string;
86
+ threadId?: string;
87
+ inPortalThread?: boolean;
88
+ traceId?: string;
89
+ ragMetadata?: { ragQueryId: string; fileChunks: MessageSemanticSearchChunk[] };
90
+ }) => Promise<void>;
91
+ }
92
+
93
+ export const streamingExecutor: StateCreator<
94
+ ChatStore,
95
+ [['zustand/devtools', never]],
96
+ [],
97
+ StreamingExecutorAction
98
+ > = (set, get) => ({
99
+ internal_fetchAIChatMessage: async ({ messages, messageId, params, provider, model }) => {
100
+ const {
101
+ internal_toggleChatLoading,
102
+ refreshMessages,
103
+ optimisticUpdateMessageContent,
104
+ internal_dispatchMessage,
105
+ internal_toggleToolCallingStreaming,
106
+ internal_toggleChatReasoning,
107
+ } = get();
108
+
109
+ const abortController = internal_toggleChatLoading(
110
+ true,
111
+ messageId,
112
+ n('generateMessage(start)', { messageId, messages }),
113
+ );
114
+
115
+ const agentConfig =
116
+ params?.agentConfig || agentSelectors.currentAgentConfig(getAgentStoreState());
117
+ const chatConfig = agentChatConfigSelectors.currentChatConfig(getAgentStoreState());
118
+
119
+ // ================================== //
120
+ // messages uniformly preprocess //
121
+ // ================================== //
122
+ // 4. handle max_tokens
123
+ agentConfig.params.max_tokens = chatConfig.enableMaxTokens
124
+ ? agentConfig.params.max_tokens
125
+ : undefined;
126
+
127
+ // 5. handle reasoning_effort
128
+ agentConfig.params.reasoning_effort = chatConfig.enableReasoningEffort
129
+ ? agentConfig.params.reasoning_effort
130
+ : undefined;
131
+
132
+ let isFunctionCall = false;
133
+ let tools: ChatToolPayload[] | undefined;
134
+ let tool_calls: MessageToolCall[] | undefined;
135
+ let finalUsage;
136
+ let msgTraceId: string | undefined;
137
+ let output = '';
138
+ let thinking = '';
139
+ let thinkingStartAt: number;
140
+ let duration: number;
141
+ // to upload image
142
+ const uploadTasks: Map<string, Promise<{ id?: string; url?: string }>> = new Map();
143
+
144
+ // Throttle tool_calls updates to prevent excessive re-renders (max once per 300ms)
145
+ const throttledUpdateToolCalls = throttle(
146
+ (toolCalls: any[]) => {
147
+ internal_dispatchMessage({
148
+ id: messageId,
149
+ type: 'updateMessage',
150
+ value: { tools: get().internal_transformToolCalls(toolCalls) },
151
+ });
152
+ },
153
+ 300,
154
+ { leading: true, trailing: true },
155
+ );
156
+
157
+ const historySummary = chatConfig.enableCompressHistory
158
+ ? topicSelectors.currentActiveTopicSummary(get())
159
+ : undefined;
160
+ await chatService.createAssistantMessageStream({
161
+ abortController,
162
+ params: {
163
+ messages,
164
+ model,
165
+ provider,
166
+ ...agentConfig.params,
167
+ plugins: agentConfig.plugins,
168
+ },
169
+ historySummary: historySummary?.content,
170
+ trace: {
171
+ traceId: params?.traceId,
172
+ sessionId: get().activeId,
173
+ topicId: get().activeTopicId,
174
+ traceName: TraceNameMap.Conversation,
175
+ },
176
+ onErrorHandle: async (error) => {
177
+ await messageService.updateMessageError(messageId, error);
178
+ await refreshMessages();
179
+ },
180
+ onFinish: async (
181
+ content,
182
+ { traceId, observationId, toolCalls, reasoning, grounding, usage, speed },
183
+ ) => {
184
+ // if there is traceId, update it
185
+ if (traceId) {
186
+ msgTraceId = traceId;
187
+ messageService.updateMessage(messageId, {
188
+ traceId,
189
+ observationId: observationId ?? undefined,
190
+ });
191
+ }
192
+
193
+ // 等待所有图片上传完成
194
+ let finalImages: ChatImageItem[] = [];
195
+
196
+ if (uploadTasks.size > 0) {
197
+ try {
198
+ // 等待所有上传任务完成
199
+ const uploadResults = await Promise.all(uploadTasks.values());
200
+
201
+ // 使用上传后的 S3 URL 替换原始图像数据
202
+ finalImages = uploadResults.filter((i) => !!i.url) as ChatImageItem[];
203
+ } catch (error) {
204
+ console.error('Error waiting for image uploads:', error);
205
+ }
206
+ }
207
+
208
+ let parsedToolCalls = toolCalls;
209
+ if (parsedToolCalls && parsedToolCalls.length > 0) {
210
+ // Flush any pending throttled updates before finalizing
211
+ throttledUpdateToolCalls.flush();
212
+ internal_toggleToolCallingStreaming(messageId, undefined);
213
+
214
+ tools = get().internal_transformToolCalls(parsedToolCalls);
215
+ tool_calls = toolCalls;
216
+
217
+ parsedToolCalls = parsedToolCalls.map((item) => ({
218
+ ...item,
219
+ function: {
220
+ ...item.function,
221
+ arguments: !!item.function.arguments ? item.function.arguments : '{}',
222
+ },
223
+ }));
224
+
225
+ isFunctionCall = true;
226
+ }
227
+
228
+ finalUsage = usage;
229
+ internal_toggleChatReasoning(false, messageId, n('toggleChatReasoning/false') as string);
230
+
231
+ // update the content after fetch result
232
+ await optimisticUpdateMessageContent(messageId, content, {
233
+ toolCalls: parsedToolCalls,
234
+ reasoning: !!reasoning ? { ...reasoning, duration } : undefined,
235
+ search: !!grounding?.citations ? grounding : undefined,
236
+ imageList: finalImages.length > 0 ? finalImages : undefined,
237
+ metadata: speed ? { ...usage, ...speed } : usage,
238
+ });
239
+ },
240
+ onMessageHandle: async (chunk) => {
241
+ switch (chunk.type) {
242
+ case 'grounding': {
243
+ // if there is no citations, then stop
244
+ if (
245
+ !chunk.grounding ||
246
+ !chunk.grounding.citations ||
247
+ chunk.grounding.citations.length <= 0
248
+ )
249
+ return;
250
+
251
+ internal_dispatchMessage({
252
+ id: messageId,
253
+ type: 'updateMessage',
254
+ value: {
255
+ search: {
256
+ citations: chunk.grounding.citations,
257
+ searchQueries: chunk.grounding.searchQueries,
258
+ },
259
+ },
260
+ });
261
+ break;
262
+ }
263
+
264
+ case 'base64_image': {
265
+ internal_dispatchMessage({
266
+ id: messageId,
267
+ type: 'updateMessage',
268
+ value: {
269
+ imageList: chunk.images.map((i) => ({ id: i.id, url: i.data, alt: i.id })),
270
+ },
271
+ });
272
+ const image = chunk.image;
273
+
274
+ const task = getFileStoreState()
275
+ .uploadBase64FileWithProgress(image.data)
276
+ .then((value) => ({
277
+ id: value?.id,
278
+ url: value?.url,
279
+ alt: value?.filename || value?.id,
280
+ }));
281
+
282
+ uploadTasks.set(image.id, task);
283
+
284
+ break;
285
+ }
286
+
287
+ case 'text': {
288
+ output += chunk.text;
289
+
290
+ // if there is no duration, it means the end of reasoning
291
+ if (!duration) {
292
+ duration = Date.now() - thinkingStartAt;
293
+
294
+ const isInChatReasoning = get().reasoningLoadingIds.includes(messageId);
295
+ if (isInChatReasoning) {
296
+ internal_toggleChatReasoning(
297
+ false,
298
+ messageId,
299
+ n('toggleChatReasoning/false') as string,
300
+ );
301
+ }
302
+ }
303
+
304
+ internal_dispatchMessage({
305
+ id: messageId,
306
+ type: 'updateMessage',
307
+ value: {
308
+ content: output,
309
+ reasoning: !!thinking ? { content: thinking, duration } : undefined,
310
+ },
311
+ });
312
+ break;
313
+ }
314
+
315
+ case 'reasoning': {
316
+ // if there is no thinkingStartAt, it means the start of reasoning
317
+ if (!thinkingStartAt) {
318
+ thinkingStartAt = Date.now();
319
+ internal_toggleChatReasoning(
320
+ true,
321
+ messageId,
322
+ n('toggleChatReasoning/true') as string,
323
+ );
324
+ }
325
+
326
+ thinking += chunk.text;
327
+
328
+ internal_dispatchMessage({
329
+ id: messageId,
330
+ type: 'updateMessage',
331
+ value: { reasoning: { content: thinking } },
332
+ });
333
+ break;
334
+ }
335
+
336
+ // is this message is just a tool call
337
+ case 'tool_calls': {
338
+ internal_toggleToolCallingStreaming(messageId, chunk.isAnimationActives);
339
+ throttledUpdateToolCalls(chunk.tool_calls);
340
+ isFunctionCall = true;
341
+ const isInChatReasoning = get().reasoningLoadingIds.includes(messageId);
342
+ if (isInChatReasoning) {
343
+ internal_toggleChatReasoning(
344
+ false,
345
+ messageId,
346
+ n('toggleChatReasoning/false') as string,
347
+ );
348
+ }
349
+ }
350
+ }
351
+ },
352
+ });
353
+
354
+ internal_toggleChatLoading(false, messageId, n('generateMessage(end)') as string);
355
+
356
+ return {
357
+ isFunctionCall,
358
+ traceId: msgTraceId,
359
+ content: output,
360
+ tools,
361
+ usage: finalUsage,
362
+ tool_calls,
363
+ };
364
+ },
365
+
366
+ internal_execAgentRuntime: async (params) => {
367
+ const { messages: originalMessages, parentMessageId, parentMessageType } = params;
368
+
369
+ log(
370
+ '[internal_execAgentRuntime] start, parentMessageId: %s,parentMessageType: %s, messages count: %d',
371
+ parentMessageId,
372
+ parentMessageType,
373
+ originalMessages.length,
374
+ );
375
+
376
+ const { activeId, activeTopicId } = get();
377
+ const messageKey = messageMapKey(activeId, activeTopicId);
378
+
379
+ // Create a new array to avoid modifying the original messages
380
+ let messages = [...originalMessages];
381
+
382
+ const agentStoreState = getAgentStoreState();
383
+ const agentConfigData = agentSelectors.currentAgentConfig(agentStoreState);
384
+ const { chatConfig } = agentConfigData;
385
+
386
+ // Use current agent config
387
+ const model = agentConfigData.model;
388
+ const provider = agentConfigData.provider;
389
+
390
+ // ===========================================
391
+ // Step 1: RAG Preprocessing (if enabled)
392
+ // ===========================================
393
+ if (params.ragQuery && parentMessageType === 'user') {
394
+ const userMessageId = parentMessageId;
395
+ log('[internal_execAgentRuntime] RAG preprocessing start');
396
+
397
+ // Get relevant chunks from semantic search
398
+ const {
399
+ chunks,
400
+ queryId: ragQueryId,
401
+ rewriteQuery,
402
+ } = await get().internal_retrieveChunks(
403
+ userMessageId,
404
+ params.ragQuery,
405
+ // Skip the last message content when building context
406
+ messages.map((m) => m.content).slice(0, messages.length - 1),
407
+ );
408
+
409
+ log('[internal_execAgentRuntime] RAG chunks retrieved: %d chunks', chunks.length);
410
+
411
+ const lastMsg = messages.pop() as UIChatMessage;
412
+
413
+ // Build RAG context and append to user query
414
+ const knowledgeBaseQAContext = knowledgeBaseQAPrompts({
415
+ chunks,
416
+ userQuery: lastMsg.content,
417
+ rewriteQuery,
418
+ knowledge: agentSelectors.currentEnabledKnowledge(agentStoreState),
419
+ });
420
+
421
+ messages.push({
422
+ ...lastMsg,
423
+ content: (lastMsg.content + '\n\n' + knowledgeBaseQAContext).trim(),
424
+ });
425
+
426
+ // Update assistant message with RAG metadata
427
+ const fileChunks: MessageSemanticSearchChunk[] = chunks.map((c) => ({
428
+ id: c.id,
429
+ similarity: c.similarity,
430
+ }));
431
+
432
+ if (fileChunks.length > 0) {
433
+ // Note: RAG metadata will be updated after assistant message is created by call_llm executor
434
+ // Store RAG data temporarily in params for later use
435
+ params.ragMetadata = { ragQueryId: ragQueryId!, fileChunks };
436
+ }
437
+
438
+ log('[internal_execAgentRuntime] RAG preprocessing completed');
439
+ }
440
+
441
+ // ===========================================
442
+ // Step 3: Create and Execute Agent Runtime
443
+ // ===========================================
444
+ log('[internal_execAgentRuntime] Creating agent runtime');
445
+
446
+ const agent = new GeneralChatAgent({
447
+ agentConfig: { maxSteps: 1000 },
448
+ sessionId: `${messageKey}/${params.parentMessageId}`,
449
+ modelRuntimeConfig: {
450
+ model,
451
+ provider: provider!,
452
+ },
453
+ });
454
+ const runtime = new AgentRuntime(agent, {
455
+ executors: createAgentExecutors({
456
+ get,
457
+ messageKey,
458
+ parentId: params.parentMessageId,
459
+ parentMessageType,
460
+ params,
461
+ }),
462
+ });
463
+
464
+ // Create initial state
465
+ let state = AgentRuntime.createInitialState({
466
+ sessionId: activeId,
467
+ messages,
468
+ maxSteps: 20, // Prevent infinite loops
469
+ metadata: {
470
+ sessionId: activeId,
471
+ topicId: activeTopicId,
472
+ threadId: params.threadId,
473
+ },
474
+ });
475
+
476
+ // Initial context - use 'init' phase since state already contains messages
477
+ let nextContext: AgentRuntimeContext = {
478
+ phase: 'init',
479
+ payload: { model, provider, parentMessageId: params.parentMessageId },
480
+ session: {
481
+ sessionId: activeId,
482
+ messageCount: messages.length,
483
+ status: state.status,
484
+ stepCount: 0,
485
+ },
486
+ };
487
+
488
+ log(
489
+ '[internal_execAgentRuntime] Agent runtime loop start, initial phase: %s',
490
+ nextContext.phase,
491
+ );
492
+
493
+ // Execute the agent runtime loop
494
+ let stepCount = 0;
495
+ while (state.status !== 'done' && state.status !== 'error') {
496
+ stepCount++;
497
+ log(
498
+ '[internal_execAgentRuntime][step-%d]: phase=%s, status=%s',
499
+ stepCount,
500
+ nextContext.phase,
501
+ state.status,
502
+ );
503
+
504
+ const result = await runtime.step(state, nextContext);
505
+
506
+ log(
507
+ '[internal_execAgentRuntime] Step %d completed, events: %d, newStatus=%s',
508
+ stepCount,
509
+ result.events.length,
510
+ result.newState.status,
511
+ );
512
+
513
+ // Handle completion and error events
514
+ for (const event of result.events) {
515
+ if (event.type === 'done') {
516
+ log('[internal_execAgentRuntime] Received done event, syncing to database');
517
+ // Sync final state to database
518
+ const finalMessages = get().messagesMap[messageKey] || [];
519
+ get().replaceMessages(finalMessages);
520
+ }
521
+
522
+ if (event.type === 'error') {
523
+ log('[internal_execAgentRuntime] Received error event: %o', event.error);
524
+ // Find the assistant message to update error
525
+ const currentMessages = get().messagesMap[messageKey] || [];
526
+ const assistantMessage = currentMessages.findLast((m) => m.role === 'assistant');
527
+ if (assistantMessage) {
528
+ await messageService.updateMessageError(assistantMessage.id, event.error);
529
+ }
530
+ const finalMessages = get().messagesMap[messageKey] || [];
531
+ get().replaceMessages(finalMessages);
532
+ }
533
+ }
534
+
535
+ state = result.newState;
536
+
537
+ // If no nextContext, stop execution
538
+ if (!result.nextContext) {
539
+ log('[internal_execAgentRuntime] No next context, stopping loop');
540
+ break;
541
+ }
542
+
543
+ nextContext = result.nextContext;
544
+ }
545
+
546
+ log(
547
+ '[internal_execAgentRuntime] Agent runtime loop finished, final status: %s, total steps: %d',
548
+ state.status,
549
+ stepCount,
550
+ );
551
+
552
+ // Update RAG metadata if available
553
+ if (params.ragMetadata) {
554
+ const finalMessages = get().messagesMap[messageKey] || [];
555
+ const assistantMessage = finalMessages.findLast((m) => m.role === 'assistant');
556
+ if (assistantMessage) {
557
+ await get().optimisticUpdateMessageRAG(assistantMessage.id, params.ragMetadata);
558
+ log('[internal_execAgentRuntime] RAG metadata updated for assistant message');
559
+ }
560
+ }
561
+
562
+ log('[internal_execAgentRuntime] completed');
563
+
564
+ // Desktop notification (if not in tools calling mode)
565
+ if (isDesktop) {
566
+ try {
567
+ const messageKey = `${activeId}_${activeTopicId ?? null}`;
568
+ const finalMessages = get().messagesMap[messageKey] || [];
569
+ const lastAssistant = finalMessages.findLast((m) => m.role === 'assistant');
570
+
571
+ // Only show notification if there's content and no tools
572
+ if (lastAssistant?.content && !lastAssistant?.tools) {
573
+ const { desktopNotificationService } = await import(
574
+ '@/services/electron/desktopNotification'
575
+ );
576
+
577
+ await desktopNotificationService.showNotification({
578
+ body: lastAssistant.content,
579
+ title: t('notification.finishChatGeneration', { ns: 'electron' }),
580
+ });
581
+ }
582
+ } catch (error) {
583
+ console.error('Desktop notification error:', error);
584
+ }
585
+ }
586
+
587
+ // Summary history if context messages is larger than historyCount
588
+ const historyCount = agentChatConfigSelectors.historyCount(agentStoreState);
589
+
590
+ if (
591
+ agentChatConfigSelectors.enableHistoryCount(agentStoreState) &&
592
+ chatConfig.enableCompressHistory &&
593
+ messages.length > historyCount
594
+ ) {
595
+ // after generation: [u1,a1,u2,a2,u3,a3]
596
+ // but the `messages` is still: [u1,a1,u2,a2,u3]
597
+ // So if historyCount=2, we need to summary [u1,a1,u2,a2]
598
+ // because user find UI is [u1,a1,u2,a2 | u3,a3]
599
+ const historyMessages = messages.slice(0, -historyCount + 1);
600
+
601
+ await get().internal_summaryHistory(historyMessages);
602
+ }
603
+ },
604
+ });
@@ -0,0 +1,84 @@
1
+ import isEqual from 'fast-deep-equal';
2
+ import { produce } from 'immer';
3
+ import { StateCreator } from 'zustand/vanilla';
4
+
5
+ import { ChatStore } from '@/store/chat/store';
6
+ import { Action } from '@/utils/storeDebug';
7
+
8
+ /**
9
+ * Manages loading states during streaming operations
10
+ */
11
+ export interface StreamingStatesAction {
12
+ /**
13
+ * Toggles the loading state for AI message generation, managing the UI feedback
14
+ */
15
+ internal_toggleChatLoading: (
16
+ loading: boolean,
17
+ id?: string,
18
+ action?: Action,
19
+ ) => AbortController | undefined;
20
+ /**
21
+ * Toggles the loading state for AI message reasoning, managing the UI feedback
22
+ */
23
+ internal_toggleChatReasoning: (
24
+ loading: boolean,
25
+ id?: string,
26
+ action?: string,
27
+ ) => AbortController | undefined;
28
+ /**
29
+ * Toggles the loading state for messages in tools calling
30
+ */
31
+ internal_toggleMessageInToolsCalling: (
32
+ loading: boolean,
33
+ id?: string,
34
+ action?: Action,
35
+ ) => AbortController | undefined;
36
+ /**
37
+ * Toggles the loading state for search workflow
38
+ */
39
+ internal_toggleSearchWorkflow: (loading: boolean, id?: string) => void;
40
+ /**
41
+ * Controls the streaming state of tool calling processes, updating the UI accordingly
42
+ */
43
+ internal_toggleToolCallingStreaming: (id: string, streaming: boolean[] | undefined) => void;
44
+ }
45
+
46
+ export const streamingStates: StateCreator<
47
+ ChatStore,
48
+ [['zustand/devtools', never]],
49
+ [],
50
+ StreamingStatesAction
51
+ > = (set, get) => ({
52
+ internal_toggleChatLoading: (loading, id, action) => {
53
+ return get().internal_toggleLoadingArrays('chatLoadingIds', loading, id, action);
54
+ },
55
+ internal_toggleChatReasoning: (loading, id, action) => {
56
+ return get().internal_toggleLoadingArrays('reasoningLoadingIds', loading, id, action);
57
+ },
58
+ internal_toggleMessageInToolsCalling: (loading, id) => {
59
+ return get().internal_toggleLoadingArrays('messageInToolsCallingIds', loading, id);
60
+ },
61
+ internal_toggleSearchWorkflow: (loading, id) => {
62
+ return get().internal_toggleLoadingArrays('searchWorkflowLoadingIds', loading, id);
63
+ },
64
+
65
+ internal_toggleToolCallingStreaming: (id, streaming) => {
66
+ const previous = get().toolCallingStreamIds;
67
+ const next = produce(previous, (draft) => {
68
+ if (!!streaming) {
69
+ draft[id] = streaming;
70
+ } else {
71
+ delete draft[id];
72
+ }
73
+ });
74
+
75
+ if (isEqual(previous, next)) return;
76
+
77
+ set(
78
+ { toolCallingStreamIds: next },
79
+
80
+ false,
81
+ `toggleToolCallingStreaming/${!!streaming ? 'start' : 'end'}`,
82
+ );
83
+ },
84
+ });