@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,395 @@
1
+ import {
2
+ AgentEvent,
3
+ AgentInstruction,
4
+ AgentInstructionCallLlm,
5
+ AgentInstructionCallTool,
6
+ AgentRuntimeContext,
7
+ GeneralAgentCallLLMInstructionPayload,
8
+ GeneralAgentCallLLMResultPayload,
9
+ GeneralAgentCallToolResultPayload,
10
+ GeneralAgentCallingToolInstructionPayload,
11
+ InstructionExecutor,
12
+ UsageCounter,
13
+ } from '@lobechat/agent-runtime';
14
+ import type { ChatToolPayload, CreateMessageParams } from '@lobechat/types';
15
+ import debug from 'debug';
16
+
17
+ import type { ChatStore } from '@/store/chat/store';
18
+
19
+ const log = debug('lobe-store:agent-executors');
20
+
21
+ // Tool pricing configuration (USD per call)
22
+ const TOOL_PRICING: Record<string, number> = {
23
+ 'lobe-web-browsing/craw': 0.002,
24
+ 'lobe-web-browsing/search': 0.001,
25
+ };
26
+
27
+ /**
28
+ * Creates custom executors for the Chat Agent Runtime
29
+ * These executors wrap existing chat store methods to integrate with agent-runtime
30
+ */
31
+ export const createAgentExecutors = (context: {
32
+ get: () => ChatStore;
33
+ messageKey: string;
34
+ params: {
35
+ inPortalThread?: boolean;
36
+ inSearchWorkflow?: boolean;
37
+ ragQuery?: string;
38
+ threadId?: string;
39
+ traceId?: string;
40
+ };
41
+ parentId: string;
42
+ parentMessageType: 'user' | 'assistant';
43
+ }) => {
44
+ // 当通过 sendMessageInServer 的时候,已经有一条消息了,那么就不需要触发创建
45
+ let shouldSkipCreateMessage = context.parentMessageType === 'assistant';
46
+
47
+ const executors: Partial<Record<AgentInstruction['type'], InstructionExecutor>> = {
48
+ /**
49
+ * Custom call_llm executor
50
+ * Creates assistant message and calls internal_fetchAIChatMessage
51
+ */
52
+ call_llm: async (instruction, state) => {
53
+ const sessionLogId = `${state.sessionId}:${state.stepCount}`;
54
+ const stagePrefix = `[${sessionLogId}][call_llm]`;
55
+
56
+ const llmPayload = (instruction as AgentInstructionCallLlm)
57
+ .payload as GeneralAgentCallLLMInstructionPayload;
58
+
59
+ const events: AgentEvent[] = [];
60
+
61
+ log(`${stagePrefix} Starting session`);
62
+
63
+ let assistantMessageId: string;
64
+
65
+ if (shouldSkipCreateMessage) {
66
+ // 跳过第一次创建,后续就不再跳过了
67
+ assistantMessageId = context.parentId;
68
+ shouldSkipCreateMessage = false;
69
+ } else {
70
+ // 如果是 userMessage 的第一次 regenerated 创建, llmPayload 不存在 parentMessageId
71
+ // 因此用这种方式做个赋值
72
+ // TODO: 也许未来这个应该用 init 方法实现
73
+ if (!llmPayload.parentMessageId) {
74
+ llmPayload.parentMessageId = context.parentId;
75
+ }
76
+ // Create assistant message (following server-side pattern)
77
+ const assistantMessageItem = await context.get().optimisticCreateMessage({
78
+ content: '',
79
+ model: llmPayload.model,
80
+ parentId: llmPayload.parentMessageId,
81
+ provider: llmPayload.provider,
82
+ role: 'assistant',
83
+ sessionId: state.metadata!.sessionId!,
84
+ threadId: state.metadata?.threadId,
85
+ topicId: state.metadata?.topicId,
86
+ });
87
+
88
+ if (!assistantMessageItem) {
89
+ throw new Error('Failed to create assistant message');
90
+ }
91
+ assistantMessageId = assistantMessageItem.id;
92
+ }
93
+
94
+ log(`${stagePrefix} Created assistant message, id: %s`, assistantMessageId);
95
+
96
+ log(
97
+ `${stagePrefix} calling model-runtime chat (model: %s, messages: %d, tools: %d)`,
98
+ llmPayload.model,
99
+ llmPayload.messages.length,
100
+ llmPayload.tools?.length ?? 0,
101
+ );
102
+
103
+ // Call existing internal_fetchAIChatMessage
104
+ // This method already handles:
105
+ // - Stream processing (text, tool_calls, reasoning, grounding, base64_image)
106
+ // - UI updates via dispatchMessage
107
+ // - Loading state management
108
+ // - Error handling
109
+ // Use messages from state (already contains full conversation history)
110
+ const {
111
+ isFunctionCall,
112
+ content,
113
+ tools,
114
+ usage: currentStepUsage,
115
+ tool_calls,
116
+ } = await context.get().internal_fetchAIChatMessage({
117
+ messageId: assistantMessageId,
118
+ messages: llmPayload.messages,
119
+ model: llmPayload.model,
120
+ params: context.params,
121
+ provider: llmPayload.provider,
122
+ });
123
+
124
+ log(`[${sessionLogId}] finish model-runtime calling`);
125
+
126
+ // Get latest messages from store (already updated by internal_fetchAIChatMessage)
127
+ const latestMessages = context.get().dbMessagesMap[context.messageKey] || [];
128
+
129
+ // Get updated assistant message to extract usage/cost information
130
+ const assistantMessage = latestMessages.find((m) => m.id === assistantMessageId);
131
+
132
+ const toolCalls = tools || [];
133
+
134
+ if (content) {
135
+ log(`[${sessionLogId}][content]`, content);
136
+ }
137
+ if (assistantMessage?.reasoning?.content) {
138
+ log(`[${sessionLogId}][reasoning]`, assistantMessage.reasoning.content);
139
+ }
140
+ if (toolCalls.length > 0) {
141
+ log(`[${sessionLogId}][toolsCalling] `, toolCalls);
142
+ }
143
+
144
+ // Log usage
145
+ if (currentStepUsage) {
146
+ log(`[${sessionLogId}][usage] %O`, currentStepUsage);
147
+ }
148
+
149
+ // Add llm_stream events (similar to backend)
150
+ if (content) {
151
+ events.push({
152
+ chunk: { text: content, type: 'text' },
153
+ type: 'llm_stream',
154
+ });
155
+ }
156
+
157
+ if (assistantMessage?.reasoning?.content) {
158
+ events.push({
159
+ chunk: { text: assistantMessage.reasoning.content, type: 'reasoning' },
160
+ type: 'llm_stream',
161
+ });
162
+ }
163
+
164
+ events.push({
165
+ result: {
166
+ content,
167
+ reasoning: assistantMessage?.reasoning?.content,
168
+ tool_calls: toolCalls,
169
+ usage: currentStepUsage,
170
+ },
171
+ type: 'llm_result',
172
+ });
173
+
174
+ log('[%s:%d] call_llm completed', state.sessionId, state.stepCount);
175
+
176
+ // Accumulate usage and cost to state
177
+ const newState = { ...state, messages: latestMessages };
178
+
179
+ if (currentStepUsage) {
180
+ // Use UsageCounter to accumulate LLM usage and cost
181
+ const { usage, cost } = UsageCounter.accumulateLLM({
182
+ cost: state.cost,
183
+ model: llmPayload.model,
184
+ modelUsage: currentStepUsage,
185
+ provider: llmPayload.provider,
186
+ usage: state.usage,
187
+ });
188
+
189
+ newState.usage = usage;
190
+ if (cost) newState.cost = cost;
191
+ }
192
+
193
+ return {
194
+ events,
195
+ newState,
196
+ nextContext: {
197
+ payload: {
198
+ hasToolsCalling: isFunctionCall,
199
+ parentMessageId: assistantMessageId,
200
+ result: { content, tool_calls },
201
+ toolsCalling: toolCalls,
202
+ } as GeneralAgentCallLLMResultPayload,
203
+ phase: 'llm_result',
204
+ session: {
205
+ eventCount: events.length,
206
+ messageCount: newState.messages.length,
207
+ sessionId: state.sessionId,
208
+ status: 'running',
209
+ stepCount: state.stepCount + 1,
210
+ },
211
+ stepUsage: currentStepUsage,
212
+ } as AgentRuntimeContext,
213
+ };
214
+ },
215
+
216
+ /**
217
+ * Custom call_tool executor
218
+ * Wraps internal_invokeDifferentTypePlugin
219
+ * Follows server-side pattern: always create tool message before execution
220
+ */
221
+ call_tool: async (instruction, state) => {
222
+ const payload = (instruction as AgentInstructionCallTool)
223
+ .payload as GeneralAgentCallingToolInstructionPayload;
224
+
225
+ const events: AgentEvent[] = [];
226
+ const sessionLogId = `${state.sessionId}:${state.stepCount}`;
227
+
228
+ log('[%s][call_tool] Executor start, payload: %O', sessionLogId, payload);
229
+
230
+ // Convert CallingToolPayload to ChatToolPayload for ToolExecutionService
231
+ const chatToolPayload: ChatToolPayload = payload.toolCalling;
232
+
233
+ const toolName = `${chatToolPayload.identifier}/${chatToolPayload.apiName}`;
234
+ const startTime = performance.now();
235
+
236
+ try {
237
+ // Get assistant message to extract groupId
238
+ const latestMessages = context.get().dbMessagesMap[context.messageKey] || [];
239
+ // Find the last assistant message (should be created by call_llm)
240
+ const assistantMessage = latestMessages.findLast((m) => m.role === 'assistant');
241
+
242
+ // Always create new tool message (following server-side pattern)
243
+ // This ensures consistency and avoids duplicate execution
244
+ log(
245
+ '[%s][call_tool] Creating tool message for tool_call_id: %s',
246
+ sessionLogId,
247
+ chatToolPayload.id,
248
+ );
249
+
250
+ const toolMessageParams: CreateMessageParams = {
251
+ content: '',
252
+ groupId: assistantMessage?.groupId,
253
+ parentId: payload.parentMessageId,
254
+ plugin: chatToolPayload,
255
+ role: 'tool',
256
+ sessionId: context.get().activeId,
257
+ threadId: context.params.threadId,
258
+ tool_call_id: chatToolPayload.id,
259
+ topicId: context.get().activeTopicId,
260
+ };
261
+
262
+ const createResult = await context.get().optimisticCreateMessage(toolMessageParams);
263
+
264
+ if (!createResult) {
265
+ log(
266
+ '[%s][call_tool] ERROR: Failed to create tool message for tool_call_id: %s',
267
+ sessionLogId,
268
+ chatToolPayload.id,
269
+ );
270
+ throw new Error(`Failed to create tool message for tool_call_id: ${chatToolPayload.id}`);
271
+ }
272
+
273
+ const toolMessageId = createResult.id;
274
+ log('[%s][call_tool] Created tool message, id: %s', sessionLogId, toolMessageId);
275
+
276
+ // Execute tool
277
+ log('[%s][call_tool] Executing tool %s ...', sessionLogId, toolName);
278
+ // This method handles:
279
+ // - Tool execution (builtin, plugin, MCP)
280
+ // - Content updates via optimisticUpdateMessageContent
281
+ // - Error handling via internal_updateMessageError
282
+ const result = await context
283
+ .get()
284
+ .internal_invokeDifferentTypePlugin(toolMessageId, chatToolPayload);
285
+
286
+ const executionTime = Math.round(performance.now() - startTime);
287
+ const isSuccess = !result.error;
288
+
289
+ log(
290
+ '[%s][call_tool] Executing %s in %dms, result: %O',
291
+ sessionLogId,
292
+ toolName,
293
+ executionTime,
294
+ result,
295
+ );
296
+
297
+ events.push({ id: chatToolPayload.id, result, type: 'tool_result' });
298
+
299
+ // Get latest messages from store (already updated by internal_invokeDifferentTypePlugin)
300
+ const updatedMessages = context.get().dbMessagesMap[context.messageKey] || [];
301
+
302
+ const newState = { ...state, messages: updatedMessages };
303
+
304
+ // Get tool unit price
305
+ const toolCost = TOOL_PRICING[toolName] || 0;
306
+
307
+ // Use UsageCounter to accumulate tool usage
308
+ const { usage, cost } = UsageCounter.accumulateTool({
309
+ cost: state.cost,
310
+ executionTime,
311
+ success: isSuccess,
312
+ toolCost,
313
+ toolName,
314
+ usage: state.usage,
315
+ });
316
+
317
+ newState.usage = usage;
318
+ if (cost) newState.cost = cost;
319
+
320
+ // Find current tool statistics
321
+ const currentToolStats = usage.tools.byTool.find((t) => t.name === toolName);
322
+
323
+ // Log usage
324
+ log(
325
+ '[%s][tool usage] %s: calls=%d, time=%dms, success=%s, cost=$%s',
326
+ sessionLogId,
327
+ toolName,
328
+ currentToolStats?.calls || 0,
329
+ executionTime,
330
+ isSuccess,
331
+ toolCost.toFixed(4),
332
+ );
333
+
334
+ log('[%s][call_tool] Tool execution completed', sessionLogId);
335
+
336
+ return {
337
+ events,
338
+ newState,
339
+ nextContext: {
340
+ payload: {
341
+ data: result,
342
+ executionTime,
343
+ isSuccess,
344
+ parentMessageId: toolMessageId,
345
+ toolCall: chatToolPayload,
346
+ toolCallId: chatToolPayload.id,
347
+ } as GeneralAgentCallToolResultPayload,
348
+ phase: 'tool_result',
349
+ session: {
350
+ eventCount: events.length,
351
+ messageCount: newState.messages.length,
352
+ sessionId: state.sessionId,
353
+ status: 'running',
354
+ stepCount: state.stepCount + 1,
355
+ },
356
+ stepUsage: {
357
+ cost: toolCost,
358
+ toolName,
359
+ unitPrice: toolCost,
360
+ usageCount: 1,
361
+ },
362
+ } as AgentRuntimeContext,
363
+ };
364
+ } catch (error) {
365
+ log('[%s][call_tool] ERROR: Tool execution failed: %O', sessionLogId, error);
366
+
367
+ events.push({ error: error, type: 'error' });
368
+
369
+ // Return current state on error (no state change)
370
+ return { events, newState: state };
371
+ }
372
+ },
373
+
374
+ /**
375
+ * Finish executor
376
+ * Completes the runtime execution
377
+ */
378
+ finish: async (instruction, state) => {
379
+ const { reason, reasonDetail } = instruction as Extract<AgentInstruction, { type: 'finish' }>;
380
+ const sessionLogId = `${state.sessionId}:${state.stepCount}`;
381
+
382
+ log(`[${sessionLogId}] Finishing execution: (%s)`, reason);
383
+
384
+ const newState = structuredClone(state);
385
+ newState.lastModified = new Date().toISOString();
386
+ newState.status = 'done';
387
+
388
+ const events: AgentEvent[] = [{ finalState: newState, reason, reasonDetail, type: 'done' }];
389
+
390
+ return { events, newState };
391
+ },
392
+ };
393
+
394
+ return executors;
395
+ };
@@ -64,105 +64,6 @@ describe('chatHelpers', () => {
64
64
  const message = chatHelpers.getMessageById([], '1');
65
65
  expect(message).toBeUndefined();
66
66
  });
67
-
68
- it('finds a block within a group message', () => {
69
- const messagesWithGroup = [
70
- { id: '1', content: 'Hello' },
71
- {
72
- id: 'group1',
73
- role: 'group',
74
- content: '',
75
- children: [
76
- { id: 'block1', content: 'First block' },
77
- { id: 'block2', content: 'Second block' },
78
- ],
79
- },
80
- ] as UIChatMessage[];
81
-
82
- const block = chatHelpers.getMessageById(messagesWithGroup, 'block1');
83
- expect(block).toBeDefined();
84
- expect(block?.id).toBe('block1');
85
- expect(block?.content).toBe('First block');
86
- });
87
-
88
- it('returns block with parentId set to group message id', () => {
89
- const messagesWithGroup = [
90
- {
91
- id: 'group1',
92
- role: 'group',
93
- content: '',
94
- children: [{ id: 'block1', content: 'Block content' }],
95
- },
96
- ] as UIChatMessage[];
97
-
98
- const block = chatHelpers.getMessageById(messagesWithGroup, 'block1');
99
- expect(block).toBeDefined();
100
- expect(block?.parentId).toBe('group1');
101
- });
102
-
103
- it('searches across multiple group messages', () => {
104
- const messagesWithGroups = [
105
- {
106
- id: 'group1',
107
- role: 'group',
108
- content: '',
109
- children: [{ id: 'block1', content: 'First group block' }],
110
- },
111
- {
112
- id: 'group2',
113
- role: 'group',
114
- content: '',
115
- children: [{ id: 'block2', content: 'Second group block' }],
116
- },
117
- ] as UIChatMessage[];
118
-
119
- const block = chatHelpers.getMessageById(messagesWithGroups, 'block2');
120
- expect(block).toBeDefined();
121
- expect(block?.id).toBe('block2');
122
- expect(block?.parentId).toBe('group2');
123
- expect(block?.content).toBe('Second group block');
124
- });
125
-
126
- it('prioritizes top-level message over block with same id', () => {
127
- const messagesWithConflict = [
128
- { id: 'duplicate', content: 'Top-level message', role: 'user' },
129
- {
130
- id: 'group1',
131
- role: 'group',
132
- content: '',
133
- children: [{ id: 'duplicate', content: 'Block message' }],
134
- },
135
- ] as UIChatMessage[];
136
-
137
- const message = chatHelpers.getMessageById(messagesWithConflict, 'duplicate');
138
- expect(message).toBeDefined();
139
- expect(message?.content).toBe('Top-level message');
140
- expect(message?.role).toBe('user');
141
- expect(message?.parentId).toBeUndefined();
142
- });
143
-
144
- it('returns undefined when block is not found in any group', () => {
145
- const messagesWithGroup = [
146
- {
147
- id: 'group1',
148
- role: 'group',
149
- content: '',
150
- children: [{ id: 'block1', content: 'Block content' }],
151
- },
152
- ] as UIChatMessage[];
153
-
154
- const block = chatHelpers.getMessageById(messagesWithGroup, 'nonexistent');
155
- expect(block).toBeUndefined();
156
- });
157
-
158
- it('handles group message without children', () => {
159
- const messagesWithEmptyGroup = [
160
- { id: 'group1', role: 'group', content: '' },
161
- ] as UIChatMessage[];
162
-
163
- const block = chatHelpers.getMessageById(messagesWithEmptyGroup, 'block1');
164
- expect(block).toBeUndefined();
165
- });
166
67
  });
167
68
 
168
69
  describe('getSlicedMessages', () => {
@@ -13,17 +13,6 @@ export const getMessageById = (
13
13
  const directMatch = messages.find((m) => m.id === id);
14
14
  if (directMatch) return directMatch;
15
15
 
16
- // If not found, search in group message children (blocks)
17
- for (const message of messages) {
18
- if (message.role === 'group' && message.children) {
19
- const blockMatch = message.children.find((block) => block.id === id);
20
- if (blockMatch) {
21
- // Return the block with parentId set to group message ID
22
- return { ...blockMatch, parentId: message.id } as UIChatMessage;
23
- }
24
- }
25
- }
26
-
27
16
  return undefined;
28
17
  };
29
18