@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.
- package/CHANGELOG.md +50 -0
- package/changelog/v1.json +18 -0
- package/next.config.ts +5 -6
- package/package.json +2 -2
- package/packages/agent-runtime/src/core/__tests__/runtime.test.ts +112 -77
- package/packages/agent-runtime/src/core/runtime.ts +63 -18
- package/packages/agent-runtime/src/types/generalAgent.ts +55 -0
- package/packages/agent-runtime/src/types/index.ts +1 -0
- package/packages/agent-runtime/src/types/instruction.ts +10 -3
- package/packages/const/src/user.ts +0 -1
- package/packages/context-engine/src/processors/GroupMessageFlatten.ts +8 -6
- package/packages/context-engine/src/processors/__tests__/GroupMessageFlatten.test.ts +12 -12
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/assistant-group-branches.json +249 -0
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/index.ts +4 -0
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/multi-assistant-group.json +260 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/active-index-1.json +4 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/assistant-group-branches.json +481 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/conversation.json +5 -1
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/index.ts +4 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/multi-assistant-group.json +407 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/nested.json +18 -2
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/complex-scenario.json +25 -3
- package/packages/conversation-flow/src/__tests__/parse.test.ts +12 -0
- package/packages/conversation-flow/src/index.ts +1 -1
- package/packages/conversation-flow/src/transformation/FlatListBuilder.ts +112 -34
- package/packages/conversation-flow/src/types/flatMessageList.ts +0 -12
- package/packages/conversation-flow/src/{types.ts → types/index.ts} +3 -14
- package/packages/database/src/models/__tests__/apiKey.test.ts +444 -0
- package/packages/database/src/models/message.ts +18 -19
- package/packages/types/src/aiChat.ts +2 -0
- package/packages/types/src/importer.ts +2 -2
- package/packages/types/src/message/ui/chat.ts +17 -1
- package/packages/types/src/message/ui/extra.ts +2 -2
- package/packages/types/src/message/ui/params.ts +2 -2
- package/packages/types/src/user/preference.ts +0 -4
- package/packages/utils/src/tokenizer/index.ts +3 -11
- package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/Desktop/MessageFromUrl.tsx +3 -3
- package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/V1Mobile/index.tsx +1 -1
- package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/V1Mobile/useSend.ts +3 -3
- package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/useSend.ts +6 -6
- package/src/app/[variants]/(main)/chat/components/conversation/features/ChatList/Content.tsx +5 -3
- package/src/app/[variants]/(main)/chat/components/conversation/features/ChatList/WelcomeChatItem/AgentWelcome/OpeningQuestions.tsx +2 -2
- package/src/app/[variants]/(main)/chat/components/conversation/features/ChatList/WelcomeChatItem/GroupWelcome/GroupUsageSuggest.tsx +2 -2
- package/src/app/[variants]/(main)/labs/page.tsx +0 -9
- package/src/features/ChatInput/ActionBar/STT/browser.tsx +3 -3
- package/src/features/ChatInput/ActionBar/STT/openai.tsx +3 -3
- package/src/features/Conversation/Error/AccessCodeForm.tsx +1 -1
- package/src/features/Conversation/Error/ChatInvalidApiKey.tsx +1 -1
- package/src/features/Conversation/Error/ClerkLogin/index.tsx +1 -1
- package/src/features/Conversation/Error/OAuthForm.tsx +1 -1
- package/src/features/Conversation/Error/index.tsx +0 -5
- package/src/features/Conversation/Messages/Assistant/Actions/index.tsx +13 -10
- package/src/features/Conversation/Messages/Assistant/Extra/index.test.tsx +3 -8
- package/src/features/Conversation/Messages/Assistant/Extra/index.tsx +2 -6
- package/src/features/Conversation/Messages/Assistant/MessageContent.tsx +7 -9
- package/src/features/Conversation/Messages/Assistant/Tool/Inspector/PluginResult.tsx +2 -2
- package/src/features/Conversation/Messages/Assistant/Tool/Inspector/PluginState.tsx +2 -2
- package/src/features/Conversation/Messages/Assistant/Tool/Render/PluginSettings.tsx +4 -1
- package/src/features/Conversation/Messages/Assistant/Tool/Render/index.tsx +2 -3
- package/src/features/Conversation/Messages/Assistant/index.tsx +57 -60
- package/src/features/Conversation/Messages/Default.tsx +1 -0
- package/src/features/Conversation/Messages/Group/Actions/WithContentId.tsx +38 -10
- package/src/features/Conversation/Messages/Group/Actions/index.tsx +1 -1
- package/src/features/Conversation/Messages/Group/ContentBlock.tsx +1 -3
- package/src/features/Conversation/Messages/Group/GroupChildren.tsx +12 -12
- package/src/features/Conversation/Messages/Group/MessageContent.tsx +7 -1
- package/src/features/Conversation/Messages/Group/Tool/Render/PluginSettings.tsx +1 -1
- package/src/features/Conversation/Messages/Group/index.tsx +2 -1
- package/src/features/Conversation/Messages/Supervisor/index.tsx +2 -2
- package/src/features/Conversation/Messages/User/{Actions.tsx → Actions/ActionsBar.tsx} +26 -25
- package/src/features/Conversation/Messages/User/Actions/MessageBranch.tsx +107 -0
- package/src/features/Conversation/Messages/User/Actions/index.tsx +42 -0
- package/src/features/Conversation/Messages/User/index.tsx +43 -44
- package/src/features/Conversation/Messages/index.tsx +3 -3
- package/src/features/Conversation/components/AutoScroll.tsx +3 -3
- package/src/features/Conversation/components/Extras/Usage/UsageDetail/AnimatedNumber.tsx +55 -0
- package/src/features/Conversation/components/Extras/Usage/UsageDetail/index.tsx +5 -2
- package/src/features/Conversation/components/VirtualizedList/index.tsx +29 -20
- package/src/features/Conversation/hooks/useChatListActionsBar.tsx +8 -10
- package/src/features/Portal/Thread/Chat/ChatInput/useSend.ts +3 -3
- package/src/hooks/useHotkeys/chatScope.ts +15 -7
- package/src/libs/trpc/client/lambda.ts +4 -3
- package/src/server/routers/lambda/__tests__/aiChat.test.ts +1 -1
- package/src/server/routers/lambda/__tests__/integration/message.integration.test.ts +0 -26
- package/src/server/routers/lambda/aiChat.ts +3 -2
- package/src/server/routers/lambda/message.ts +8 -16
- package/src/server/services/message/__tests__/index.test.ts +29 -39
- package/src/server/services/message/index.ts +41 -36
- package/src/services/electron/desktopNotification.ts +6 -6
- package/src/services/electron/file.ts +6 -6
- package/src/services/file/ClientS3/index.ts +8 -8
- package/src/services/message/__tests__/metadata-race-condition.test.ts +157 -0
- package/src/services/message/index.ts +21 -15
- package/src/services/upload.ts +11 -11
- package/src/services/utils/abortableRequest.test.ts +161 -0
- package/src/services/utils/abortableRequest.ts +67 -0
- package/src/store/chat/agents/GeneralChatAgent.ts +137 -0
- package/src/store/chat/agents/createAgentExecutors.ts +395 -0
- package/src/store/chat/helpers.test.ts +0 -99
- package/src/store/chat/helpers.ts +0 -11
- package/src/store/chat/slices/aiChat/actions/__tests__/conversationControl.test.ts +332 -0
- package/src/store/chat/slices/aiChat/actions/__tests__/conversationLifecycle.test.ts +257 -0
- package/src/store/chat/slices/aiChat/actions/__tests__/helpers.ts +11 -2
- package/src/store/chat/slices/aiChat/actions/__tests__/rag.test.ts +6 -6
- package/src/store/chat/slices/aiChat/actions/__tests__/streamingExecutor.test.ts +391 -0
- package/src/store/chat/slices/aiChat/actions/__tests__/streamingStates.test.ts +179 -0
- package/src/store/chat/slices/aiChat/actions/conversationControl.ts +157 -0
- package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +329 -0
- package/src/store/chat/slices/aiChat/actions/generateAIGroupChat.ts +14 -14
- package/src/store/chat/slices/aiChat/actions/index.ts +12 -6
- package/src/store/chat/slices/aiChat/actions/rag.ts +9 -6
- package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +604 -0
- package/src/store/chat/slices/aiChat/actions/streamingStates.ts +84 -0
- package/src/store/chat/slices/builtinTool/actions/__tests__/localSystem.test.ts +4 -4
- package/src/store/chat/slices/builtinTool/actions/__tests__/search.test.ts +11 -11
- package/src/store/chat/slices/builtinTool/actions/interpreter.ts +8 -8
- package/src/store/chat/slices/builtinTool/actions/localSystem.ts +2 -2
- package/src/store/chat/slices/builtinTool/actions/search.ts +8 -8
- package/src/store/chat/slices/message/action.test.ts +79 -68
- package/src/store/chat/slices/message/actions/index.ts +39 -0
- package/src/store/chat/slices/message/actions/internals.ts +77 -0
- package/src/store/chat/slices/message/actions/optimisticUpdate.ts +260 -0
- package/src/store/chat/slices/message/actions/publicApi.ts +224 -0
- package/src/store/chat/slices/message/actions/query.ts +120 -0
- package/src/store/chat/slices/message/actions/runtimeState.ts +108 -0
- package/src/store/chat/slices/message/initialState.ts +13 -0
- package/src/store/chat/slices/message/reducer.test.ts +48 -370
- package/src/store/chat/slices/message/reducer.ts +17 -81
- package/src/store/chat/slices/message/selectors/chat.test.ts +13 -50
- package/src/store/chat/slices/message/selectors/chat.ts +78 -242
- package/src/store/chat/slices/message/selectors/dbMessage.ts +140 -0
- package/src/store/chat/slices/message/selectors/displayMessage.ts +301 -0
- package/src/store/chat/slices/message/selectors/messageState.ts +5 -2
- package/src/store/chat/slices/plugin/action.test.ts +62 -64
- package/src/store/chat/slices/plugin/action.ts +34 -28
- package/src/store/chat/slices/thread/action.test.ts +28 -31
- package/src/store/chat/slices/thread/action.ts +13 -10
- package/src/store/chat/slices/thread/selectors/index.ts +8 -6
- package/src/store/chat/slices/topic/reducer.ts +11 -3
- package/src/store/chat/store.ts +1 -1
- package/src/store/user/slices/preference/selectors/labPrefer.ts +0 -3
- package/packages/database/src/models/__tests__/message.grouping.test.ts +0 -812
- package/packages/database/src/utils/__tests__/groupMessages.test.ts +0 -1132
- package/packages/database/src/utils/groupMessages.ts +0 -361
- package/packages/utils/src/tokenizer/client.ts +0 -35
- package/packages/utils/src/tokenizer/estimated.ts +0 -4
- package/packages/utils/src/tokenizer/server.ts +0 -11
- package/packages/utils/src/tokenizer/tokenizer.worker.ts +0 -12
- package/src/app/(backend)/webapi/tokenizer/index.test.ts +0 -32
- package/src/app/(backend)/webapi/tokenizer/route.ts +0 -8
- package/src/features/Conversation/Error/InvalidAccessCode.tsx +0 -79
- package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChat.test.ts +0 -975
- package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChatV2.test.ts +0 -1050
- package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +0 -720
- package/src/store/chat/slices/aiChat/actions/generateAIChatV2.ts +0 -849
- 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
|
|