@lobehub/lobehub 2.0.0-next.84 → 2.0.0-next.86
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/apps/desktop/src/main/modules/networkProxy/dispatcher.ts +16 -16
- package/apps/desktop/src/main/modules/networkProxy/tester.ts +11 -11
- package/apps/desktop/src/main/modules/networkProxy/urlBuilder.ts +3 -3
- package/apps/desktop/src/main/modules/networkProxy/validator.ts +10 -10
- package/changelog/v1.json +18 -0
- package/package.json +1 -1
- package/packages/agent-runtime/src/core/runtime.ts +36 -1
- package/packages/agent-runtime/src/types/event.ts +1 -0
- package/packages/agent-runtime/src/types/generalAgent.ts +16 -0
- package/packages/agent-runtime/src/types/instruction.ts +30 -0
- package/packages/agent-runtime/src/types/runtime.ts +7 -0
- package/packages/types/src/message/common/metadata.ts +3 -0
- package/packages/types/src/message/common/tools.ts +2 -2
- package/packages/types/src/tool/search/index.ts +8 -2
- package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/V1Mobile/index.tsx +2 -2
- package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/V1Mobile/useSend.ts +7 -2
- package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/useSend.ts +15 -14
- package/src/app/[variants]/(main)/chat/session/features/SessionListContent/List/Item/index.tsx +2 -2
- package/src/app/[variants]/(main)/discover/(list)/features/Pagination.tsx +1 -1
- package/src/features/ChatInput/ActionBar/STT/browser.tsx +2 -2
- package/src/features/ChatInput/ActionBar/STT/openai.tsx +2 -2
- package/src/features/Conversation/Messages/Group/Tool/Inspector/index.tsx +1 -1
- package/src/features/Conversation/Messages/User/index.tsx +3 -3
- package/src/features/Conversation/Messages/index.tsx +3 -3
- package/src/features/Conversation/components/AutoScroll.tsx +2 -2
- package/src/services/search.ts +2 -2
- package/src/store/chat/agents/GeneralChatAgent.ts +98 -0
- package/src/store/chat/agents/__tests__/GeneralChatAgent.test.ts +366 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/call-llm.test.ts +1217 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/call-tool.test.ts +1976 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/finish.test.ts +453 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/index.ts +4 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockInstructions.ts +126 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockMessages.ts +94 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockOperations.ts +96 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockStore.ts +138 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/assertions.ts +185 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/index.ts +3 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/operationTestUtils.ts +94 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/testExecutor.ts +139 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/request-human-approve.test.ts +545 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/resolve-aborted-tools.test.ts +686 -0
- package/src/store/chat/agents/createAgentExecutors.ts +313 -80
- package/src/store/chat/selectors.ts +1 -0
- package/src/store/chat/slices/aiChat/__tests__/ai-chat.integration.test.ts +667 -0
- package/src/store/chat/slices/aiChat/actions/__tests__/cancel-functionality.test.ts +137 -27
- package/src/store/chat/slices/aiChat/actions/__tests__/conversationControl.test.ts +163 -125
- package/src/store/chat/slices/aiChat/actions/__tests__/conversationLifecycle.test.ts +12 -2
- package/src/store/chat/slices/aiChat/actions/__tests__/fixtures.ts +0 -2
- package/src/store/chat/slices/aiChat/actions/__tests__/helpers.ts +0 -2
- package/src/store/chat/slices/aiChat/actions/__tests__/streamingExecutor.test.ts +286 -19
- package/src/store/chat/slices/aiChat/actions/__tests__/streamingStates.test.ts +0 -112
- package/src/store/chat/slices/aiChat/actions/conversationControl.ts +42 -99
- package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +90 -57
- package/src/store/chat/slices/aiChat/actions/generateAIGroupChat.ts +5 -25
- package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +220 -98
- package/src/store/chat/slices/aiChat/actions/streamingStates.ts +0 -34
- package/src/store/chat/slices/aiChat/initialState.ts +0 -28
- package/src/store/chat/slices/aiChat/selectors.test.ts +280 -0
- package/src/store/chat/slices/aiChat/selectors.ts +31 -7
- package/src/store/chat/slices/builtinTool/actions/__tests__/localSystem.test.ts +21 -30
- package/src/store/chat/slices/builtinTool/actions/__tests__/search.test.ts +29 -49
- package/src/store/chat/slices/builtinTool/actions/interpreter.ts +83 -48
- package/src/store/chat/slices/builtinTool/actions/localSystem.ts +78 -28
- package/src/store/chat/slices/builtinTool/actions/search.ts +146 -59
- package/src/store/chat/slices/builtinTool/selectors.test.ts +258 -0
- package/src/store/chat/slices/builtinTool/selectors.ts +25 -4
- package/src/store/chat/slices/message/action.test.ts +134 -16
- package/src/store/chat/slices/message/actions/internals.ts +33 -7
- package/src/store/chat/slices/message/actions/optimisticUpdate.ts +85 -52
- package/src/store/chat/slices/message/initialState.ts +0 -10
- package/src/store/chat/slices/message/selectors/messageState.ts +34 -12
- package/src/store/chat/slices/operation/__tests__/actions.test.ts +712 -16
- package/src/store/chat/slices/operation/__tests__/integration.test.ts +342 -0
- package/src/store/chat/slices/operation/__tests__/selectors.test.ts +257 -17
- package/src/store/chat/slices/operation/actions.ts +218 -11
- package/src/store/chat/slices/operation/selectors.ts +135 -6
- package/src/store/chat/slices/operation/types.ts +29 -3
- package/src/store/chat/slices/plugin/action.test.ts +30 -322
- package/src/store/chat/slices/plugin/actions/internals.ts +0 -14
- package/src/store/chat/slices/plugin/actions/optimisticUpdate.ts +21 -19
- package/src/store/chat/slices/plugin/actions/pluginTypes.ts +45 -27
- package/src/store/chat/slices/plugin/actions/publicApi.ts +3 -4
- package/src/store/chat/slices/plugin/actions/workflow.ts +0 -55
- package/src/store/chat/slices/thread/selectors/index.ts +4 -2
- package/src/store/chat/slices/translate/action.ts +54 -41
- package/src/tools/web-browsing/ExecutionRuntime/index.ts +5 -2
- package/src/tools/web-browsing/Portal/Search/Footer.tsx +11 -9
|
@@ -29,24 +29,34 @@ const TOOL_PRICING: Record<string, number> = {
|
|
|
29
29
|
/**
|
|
30
30
|
* Creates custom executors for the Chat Agent Runtime
|
|
31
31
|
* These executors wrap existing chat store methods to integrate with agent-runtime
|
|
32
|
+
*
|
|
33
|
+
* @param context.operationId - Operation ID to get business context (sessionId, topicId, etc.)
|
|
34
|
+
* @param context.get - Store getter function
|
|
35
|
+
* @param context.messageKey - Message map key
|
|
36
|
+
* @param context.parentId - Parent message ID
|
|
37
|
+
* @param context.skipCreateFirstMessage - Skip first message creation
|
|
32
38
|
*/
|
|
33
39
|
export const createAgentExecutors = (context: {
|
|
34
40
|
get: () => ChatStore;
|
|
35
41
|
messageKey: string;
|
|
36
|
-
|
|
37
|
-
inPortalThread?: boolean;
|
|
38
|
-
inSearchWorkflow?: boolean;
|
|
39
|
-
ragQuery?: string;
|
|
40
|
-
sessionId?: string;
|
|
41
|
-
threadId?: string;
|
|
42
|
-
topicId?: string | null;
|
|
43
|
-
traceId?: string;
|
|
44
|
-
};
|
|
42
|
+
operationId: string;
|
|
45
43
|
parentId: string;
|
|
46
44
|
skipCreateFirstMessage?: boolean;
|
|
47
45
|
}) => {
|
|
48
46
|
let shouldSkipCreateMessage = context.skipCreateFirstMessage;
|
|
49
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Get operation context via closure
|
|
50
|
+
* Returns the business context (sessionId, topicId, etc.) captured by the operation
|
|
51
|
+
*/
|
|
52
|
+
const getOperationContext = () => {
|
|
53
|
+
const operation = context.get().operations[context.operationId];
|
|
54
|
+
if (!operation) {
|
|
55
|
+
throw new Error(`Operation not found: ${context.operationId}`);
|
|
56
|
+
}
|
|
57
|
+
return operation.context;
|
|
58
|
+
};
|
|
59
|
+
|
|
50
60
|
/* eslint-disable sort-keys-fix/sort-keys-fix */
|
|
51
61
|
const executors: Partial<Record<AgentInstruction['type'], InstructionExecutor>> = {
|
|
52
62
|
/**
|
|
@@ -60,8 +70,6 @@ export const createAgentExecutors = (context: {
|
|
|
60
70
|
const llmPayload = (instruction as AgentInstructionCallLlm)
|
|
61
71
|
.payload as GeneralAgentCallLLMInstructionPayload;
|
|
62
72
|
|
|
63
|
-
const events: AgentEvent[] = [];
|
|
64
|
-
|
|
65
73
|
log(`${stagePrefix} Starting session`);
|
|
66
74
|
|
|
67
75
|
let assistantMessageId: string;
|
|
@@ -71,6 +79,9 @@ export const createAgentExecutors = (context: {
|
|
|
71
79
|
assistantMessageId = context.parentId;
|
|
72
80
|
shouldSkipCreateMessage = false;
|
|
73
81
|
} else {
|
|
82
|
+
// Get context from operation
|
|
83
|
+
const opContext = getOperationContext();
|
|
84
|
+
|
|
74
85
|
// 如果是 userMessage 的第一次 regenerated 创建, llmPayload 不存在 parentMessageId
|
|
75
86
|
// 因此用这种方式做个赋值
|
|
76
87
|
// TODO: 也许未来这个应该用 init 方法实现
|
|
@@ -78,27 +89,24 @@ export const createAgentExecutors = (context: {
|
|
|
78
89
|
llmPayload.parentMessageId = context.parentId;
|
|
79
90
|
}
|
|
80
91
|
// Create assistant message (following server-side pattern)
|
|
81
|
-
const assistantMessageItem = await context.get().optimisticCreateMessage(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
},
|
|
92
|
-
{
|
|
93
|
-
sessionId: state.metadata!.sessionId!,
|
|
94
|
-
topicId: state.metadata?.topicId,
|
|
95
|
-
},
|
|
96
|
-
);
|
|
92
|
+
const assistantMessageItem = await context.get().optimisticCreateMessage({
|
|
93
|
+
content: LOADING_FLAT,
|
|
94
|
+
model: llmPayload.model,
|
|
95
|
+
parentId: llmPayload.parentMessageId,
|
|
96
|
+
provider: llmPayload.provider,
|
|
97
|
+
role: 'assistant',
|
|
98
|
+
sessionId: opContext.sessionId!,
|
|
99
|
+
threadId: opContext.threadId,
|
|
100
|
+
topicId: opContext.topicId ?? undefined,
|
|
101
|
+
});
|
|
97
102
|
|
|
98
103
|
if (!assistantMessageItem) {
|
|
99
104
|
throw new Error('Failed to create assistant message');
|
|
100
105
|
}
|
|
101
106
|
assistantMessageId = assistantMessageItem.id;
|
|
107
|
+
|
|
108
|
+
// Associate the assistant message with the operation for UI loading states
|
|
109
|
+
context.get().associateMessageWithOperation(assistantMessageId, context.operationId);
|
|
102
110
|
}
|
|
103
111
|
|
|
104
112
|
log(`${stagePrefix} Created assistant message, id: %s`, assistantMessageId);
|
|
@@ -124,12 +132,13 @@ export const createAgentExecutors = (context: {
|
|
|
124
132
|
tools,
|
|
125
133
|
usage: currentStepUsage,
|
|
126
134
|
tool_calls,
|
|
135
|
+
finishType,
|
|
127
136
|
} = await context.get().internal_fetchAIChatMessage({
|
|
128
137
|
messageId: assistantMessageId,
|
|
129
|
-
messages
|
|
138
|
+
messages,
|
|
130
139
|
model: llmPayload.model,
|
|
131
|
-
params: context.params,
|
|
132
140
|
provider: llmPayload.provider,
|
|
141
|
+
operationId: context.operationId,
|
|
133
142
|
});
|
|
134
143
|
|
|
135
144
|
log(`[${sessionLogId}] finish model-runtime calling`);
|
|
@@ -142,6 +151,7 @@ export const createAgentExecutors = (context: {
|
|
|
142
151
|
|
|
143
152
|
const toolCalls = tools || [];
|
|
144
153
|
|
|
154
|
+
// Log llm result
|
|
145
155
|
if (content) {
|
|
146
156
|
log(`[${sessionLogId}][content]`, content);
|
|
147
157
|
}
|
|
@@ -157,32 +167,12 @@ export const createAgentExecutors = (context: {
|
|
|
157
167
|
log(`[${sessionLogId}][usage] %O`, currentStepUsage);
|
|
158
168
|
}
|
|
159
169
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (assistantMessage?.reasoning?.content) {
|
|
169
|
-
events.push({
|
|
170
|
-
chunk: { text: assistantMessage.reasoning.content, type: 'reasoning' },
|
|
171
|
-
type: 'llm_stream',
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
events.push({
|
|
176
|
-
result: {
|
|
177
|
-
content,
|
|
178
|
-
reasoning: assistantMessage?.reasoning?.content,
|
|
179
|
-
tool_calls: toolCalls,
|
|
180
|
-
usage: currentStepUsage,
|
|
181
|
-
},
|
|
182
|
-
type: 'llm_result',
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
log('[%s:%d] call_llm completed', state.sessionId, state.stepCount);
|
|
170
|
+
log(
|
|
171
|
+
'[%s:%d] call_llm completed, finishType: %s',
|
|
172
|
+
state.sessionId,
|
|
173
|
+
state.stepCount,
|
|
174
|
+
finishType,
|
|
175
|
+
);
|
|
186
176
|
|
|
187
177
|
// Accumulate usage and cost to state
|
|
188
178
|
const newState = { ...state, messages: latestMessages };
|
|
@@ -201,8 +191,38 @@ export const createAgentExecutors = (context: {
|
|
|
201
191
|
if (cost) newState.cost = cost;
|
|
202
192
|
}
|
|
203
193
|
|
|
194
|
+
// If operation was aborted, enter human_abort phase to let agent decide how to handle
|
|
195
|
+
if (finishType === 'abort') {
|
|
196
|
+
log(
|
|
197
|
+
'[%s:%d] call_llm aborted by user, entering human_abort phase',
|
|
198
|
+
state.sessionId,
|
|
199
|
+
state.stepCount,
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
events: [],
|
|
204
|
+
newState,
|
|
205
|
+
nextContext: {
|
|
206
|
+
payload: {
|
|
207
|
+
reason: 'user_cancelled',
|
|
208
|
+
parentMessageId: assistantMessageId,
|
|
209
|
+
hasToolsCalling: isFunctionCall,
|
|
210
|
+
toolsCalling: toolCalls,
|
|
211
|
+
result: { content, tool_calls },
|
|
212
|
+
},
|
|
213
|
+
phase: 'human_abort',
|
|
214
|
+
session: {
|
|
215
|
+
messageCount: newState.messages.length,
|
|
216
|
+
sessionId: state.sessionId,
|
|
217
|
+
status: 'running',
|
|
218
|
+
stepCount: state.stepCount + 1,
|
|
219
|
+
},
|
|
220
|
+
} as AgentRuntimeContext,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
204
224
|
return {
|
|
205
|
-
events,
|
|
225
|
+
events: [],
|
|
206
226
|
newState,
|
|
207
227
|
nextContext: {
|
|
208
228
|
payload: {
|
|
@@ -213,7 +233,6 @@ export const createAgentExecutors = (context: {
|
|
|
213
233
|
} as GeneralAgentCallLLMResultPayload,
|
|
214
234
|
phase: 'llm_result',
|
|
215
235
|
session: {
|
|
216
|
-
eventCount: events.length,
|
|
217
236
|
messageCount: newState.messages.length,
|
|
218
237
|
sessionId: state.sessionId,
|
|
219
238
|
status: 'running',
|
|
@@ -244,6 +263,27 @@ export const createAgentExecutors = (context: {
|
|
|
244
263
|
const toolName = `${chatToolPayload.identifier}/${chatToolPayload.apiName}`;
|
|
245
264
|
const startTime = performance.now();
|
|
246
265
|
|
|
266
|
+
// Get context from operation
|
|
267
|
+
const opContext = getOperationContext();
|
|
268
|
+
|
|
269
|
+
let toolOperationId: string | undefined;
|
|
270
|
+
// ============ Create toolCalling operation (top-level) ============
|
|
271
|
+
const { operationId } = context.get().startOperation({
|
|
272
|
+
type: 'toolCalling',
|
|
273
|
+
context: {
|
|
274
|
+
sessionId: opContext.sessionId!,
|
|
275
|
+
topicId: opContext.topicId,
|
|
276
|
+
},
|
|
277
|
+
parentOperationId: context.operationId,
|
|
278
|
+
metadata: {
|
|
279
|
+
startTime: Date.now(),
|
|
280
|
+
identifier: chatToolPayload.identifier,
|
|
281
|
+
apiName: chatToolPayload.apiName,
|
|
282
|
+
tool_call_id: chatToolPayload.id,
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
toolOperationId = operationId;
|
|
286
|
+
|
|
247
287
|
try {
|
|
248
288
|
// Get assistant message to extract groupId
|
|
249
289
|
const latestMessages = context.get().dbMessagesMap[context.messageKey] || [];
|
|
@@ -272,29 +312,76 @@ export const createAgentExecutors = (context: {
|
|
|
272
312
|
chatToolPayload.id,
|
|
273
313
|
);
|
|
274
314
|
|
|
315
|
+
// ============ Sub-operation 1: Create tool message ============
|
|
316
|
+
const createToolMsgOpId = context.get().startOperation({
|
|
317
|
+
type: 'createToolMessage',
|
|
318
|
+
context: {
|
|
319
|
+
sessionId: opContext.sessionId!,
|
|
320
|
+
topicId: opContext.topicId,
|
|
321
|
+
},
|
|
322
|
+
parentOperationId: toolOperationId,
|
|
323
|
+
metadata: {
|
|
324
|
+
startTime: Date.now(),
|
|
325
|
+
tool_call_id: chatToolPayload.id,
|
|
326
|
+
},
|
|
327
|
+
}).operationId;
|
|
328
|
+
|
|
329
|
+
// Register cancel handler: Ensure message creation completes, then mark as aborted
|
|
330
|
+
context.get().onOperationCancel(createToolMsgOpId, async ({ metadata }) => {
|
|
331
|
+
log(
|
|
332
|
+
'[%s][call_tool] createToolMessage cancelled, ensuring creation completes',
|
|
333
|
+
sessionLogId,
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
// Wait for message creation to complete (ensure-complete strategy)
|
|
337
|
+
const createResult = await metadata?.createMessagePromise;
|
|
338
|
+
if (createResult) {
|
|
339
|
+
const msgId = createResult.id;
|
|
340
|
+
// Update message to aborted state
|
|
341
|
+
await Promise.all([
|
|
342
|
+
context
|
|
343
|
+
.get()
|
|
344
|
+
.optimisticUpdateMessageContent(
|
|
345
|
+
msgId,
|
|
346
|
+
'Tool execution was cancelled by user.',
|
|
347
|
+
undefined,
|
|
348
|
+
{ operationId: createToolMsgOpId },
|
|
349
|
+
),
|
|
350
|
+
context
|
|
351
|
+
.get()
|
|
352
|
+
.optimisticUpdateMessagePlugin(
|
|
353
|
+
msgId,
|
|
354
|
+
{ intervention: { status: 'aborted' } },
|
|
355
|
+
{ operationId: createToolMsgOpId },
|
|
356
|
+
),
|
|
357
|
+
]);
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// Execute creation and save Promise to metadata
|
|
275
362
|
const toolMessageParams: CreateMessageParams = {
|
|
276
363
|
content: '',
|
|
277
364
|
groupId: assistantMessage?.groupId,
|
|
278
365
|
parentId: payload.parentMessageId,
|
|
279
366
|
plugin: chatToolPayload,
|
|
280
367
|
role: 'tool',
|
|
281
|
-
sessionId:
|
|
282
|
-
threadId:
|
|
368
|
+
sessionId: opContext.sessionId!,
|
|
369
|
+
threadId: opContext.threadId,
|
|
283
370
|
tool_call_id: chatToolPayload.id,
|
|
284
|
-
topicId:
|
|
371
|
+
topicId: opContext.topicId ?? undefined,
|
|
285
372
|
};
|
|
286
373
|
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
374
|
+
const createPromise = context.get().optimisticCreateMessage(toolMessageParams);
|
|
375
|
+
context.get().updateOperationMetadata(createToolMsgOpId, {
|
|
376
|
+
createMessagePromise: createPromise,
|
|
290
377
|
});
|
|
378
|
+
const createResult = await createPromise;
|
|
291
379
|
|
|
292
380
|
if (!createResult) {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
);
|
|
381
|
+
context.get().failOperation(createToolMsgOpId, {
|
|
382
|
+
type: 'CreateMessageError',
|
|
383
|
+
message: `Failed to create tool message for tool_call_id: ${chatToolPayload.id}`,
|
|
384
|
+
});
|
|
298
385
|
throw new Error(
|
|
299
386
|
`Failed to create tool message for tool_call_id: ${chatToolPayload.id}`,
|
|
300
387
|
);
|
|
@@ -302,18 +389,80 @@ export const createAgentExecutors = (context: {
|
|
|
302
389
|
|
|
303
390
|
toolMessageId = createResult.id;
|
|
304
391
|
log('[%s][call_tool] Created tool message, id: %s', sessionLogId, toolMessageId);
|
|
392
|
+
context.get().completeOperation(createToolMsgOpId);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Check if parent operation was cancelled while creating message
|
|
396
|
+
const toolOperation = toolOperationId
|
|
397
|
+
? context.get().operations[toolOperationId]
|
|
398
|
+
: undefined;
|
|
399
|
+
if (toolOperation?.abortController.signal.aborted) {
|
|
400
|
+
log('[%s][call_tool] Parent operation cancelled, skipping tool execution', sessionLogId);
|
|
401
|
+
// Message already created with aborted status by cancel handler
|
|
402
|
+
return { events, newState: state };
|
|
305
403
|
}
|
|
306
404
|
|
|
307
|
-
// Execute tool
|
|
405
|
+
// ============ Sub-operation 2: Execute tool call ============
|
|
406
|
+
// Auto-associates message with this operation via messageId in context
|
|
407
|
+
const { operationId: executeToolOpId } = context.get().startOperation({
|
|
408
|
+
type: 'executeToolCall',
|
|
409
|
+
context: {
|
|
410
|
+
messageId: toolMessageId,
|
|
411
|
+
},
|
|
412
|
+
parentOperationId: toolOperationId,
|
|
413
|
+
metadata: {
|
|
414
|
+
startTime: Date.now(),
|
|
415
|
+
tool_call_id: chatToolPayload.id,
|
|
416
|
+
},
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
log(
|
|
420
|
+
'[%s][call_tool] Created executeToolCall operation %s for message %s',
|
|
421
|
+
sessionLogId,
|
|
422
|
+
executeToolOpId,
|
|
423
|
+
toolMessageId,
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
// Register cancel handler: Just update message (message already exists)
|
|
427
|
+
context.get().onOperationCancel(executeToolOpId, async () => {
|
|
428
|
+
log('[%s][call_tool] executeToolCall cancelled, updating message', sessionLogId);
|
|
429
|
+
|
|
430
|
+
// Update message to aborted state (cleanup strategy)
|
|
431
|
+
await Promise.all([
|
|
432
|
+
context
|
|
433
|
+
.get()
|
|
434
|
+
.optimisticUpdateMessageContent(
|
|
435
|
+
toolMessageId,
|
|
436
|
+
'Tool execution was cancelled by user.',
|
|
437
|
+
undefined,
|
|
438
|
+
{ operationId: executeToolOpId },
|
|
439
|
+
),
|
|
440
|
+
context
|
|
441
|
+
.get()
|
|
442
|
+
.optimisticUpdateMessagePlugin(
|
|
443
|
+
toolMessageId,
|
|
444
|
+
{ intervention: { status: 'aborted' } },
|
|
445
|
+
{ operationId: executeToolOpId },
|
|
446
|
+
),
|
|
447
|
+
]);
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
// Execute tool - abort handling is done by cancel handler
|
|
308
451
|
log('[%s][call_tool] Executing tool %s ...', sessionLogId, toolName);
|
|
309
|
-
// This method handles:
|
|
310
|
-
// - Tool execution (builtin, plugin, MCP)
|
|
311
|
-
// - Content updates via optimisticUpdateMessageContent
|
|
312
|
-
// - Error handling via internal_updateMessageError
|
|
313
452
|
const result = await context
|
|
314
453
|
.get()
|
|
315
454
|
.internal_invokeDifferentTypePlugin(toolMessageId, chatToolPayload);
|
|
316
455
|
|
|
456
|
+
// Check if operation was cancelled during tool execution
|
|
457
|
+
const executeToolOperation = context.get().operations[executeToolOpId];
|
|
458
|
+
if (executeToolOperation?.abortController.signal.aborted) {
|
|
459
|
+
log('[%s][call_tool] Tool execution completed but operation was cancelled', sessionLogId);
|
|
460
|
+
// Don't complete - cancel handler already updated message to aborted
|
|
461
|
+
return { events, newState: state };
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
context.get().completeOperation(executeToolOpId);
|
|
465
|
+
|
|
317
466
|
const executionTime = Math.round(performance.now() - startTime);
|
|
318
467
|
const isSuccess = !result.error;
|
|
319
468
|
|
|
@@ -325,6 +474,18 @@ export const createAgentExecutors = (context: {
|
|
|
325
474
|
result,
|
|
326
475
|
);
|
|
327
476
|
|
|
477
|
+
// Complete or fail the toolCalling operation
|
|
478
|
+
if (toolOperationId) {
|
|
479
|
+
if (isSuccess) {
|
|
480
|
+
context.get().completeOperation(toolOperationId);
|
|
481
|
+
} else {
|
|
482
|
+
context.get().failOperation(toolOperationId, {
|
|
483
|
+
type: 'ToolExecutionError',
|
|
484
|
+
message: result.error || 'Tool execution failed',
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
328
489
|
events.push({ id: chatToolPayload.id, result, type: 'tool_result' });
|
|
329
490
|
|
|
330
491
|
// Get latest messages from store (already updated by internal_invokeDifferentTypePlugin)
|
|
@@ -443,6 +604,9 @@ export const createAgentExecutors = (context: {
|
|
|
443
604
|
// Resumption mode: Tool messages already exist, just verify them
|
|
444
605
|
log('[%s][request_human_approve] Resuming with existing tool messages', sessionLogId);
|
|
445
606
|
} else {
|
|
607
|
+
// Get context from operation
|
|
608
|
+
const opContext = getOperationContext();
|
|
609
|
+
|
|
446
610
|
// Create tool messages for each pending tool call with intervention status
|
|
447
611
|
await pMap(pendingToolsCalling, async (toolPayload) => {
|
|
448
612
|
const toolName = `${toolPayload.identifier}/${toolPayload.apiName}`;
|
|
@@ -462,16 +626,13 @@ export const createAgentExecutors = (context: {
|
|
|
462
626
|
},
|
|
463
627
|
pluginIntervention: { status: 'pending' },
|
|
464
628
|
role: 'tool',
|
|
465
|
-
sessionId:
|
|
466
|
-
threadId:
|
|
629
|
+
sessionId: opContext.sessionId!,
|
|
630
|
+
threadId: opContext.threadId,
|
|
467
631
|
tool_call_id: toolPayload.id,
|
|
468
|
-
topicId:
|
|
632
|
+
topicId: opContext.topicId ?? undefined,
|
|
469
633
|
};
|
|
470
634
|
|
|
471
|
-
const createResult = await context.get().optimisticCreateMessage(toolMessageParams
|
|
472
|
-
sessionId: state.metadata!.sessionId!,
|
|
473
|
-
topicId: state.metadata?.topicId,
|
|
474
|
-
});
|
|
635
|
+
const createResult = await context.get().optimisticCreateMessage(toolMessageParams);
|
|
475
636
|
|
|
476
637
|
if (!createResult) {
|
|
477
638
|
log(
|
|
@@ -504,6 +665,78 @@ export const createAgentExecutors = (context: {
|
|
|
504
665
|
|
|
505
666
|
return { events, newState };
|
|
506
667
|
},
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Resolve aborted tools executor
|
|
671
|
+
* Creates tool messages with 'aborted' intervention status for cancelled tools
|
|
672
|
+
*/
|
|
673
|
+
resolve_aborted_tools: async (instruction, state) => {
|
|
674
|
+
const { parentMessageId, toolsCalling } = (
|
|
675
|
+
instruction as Extract<AgentInstruction, { type: 'resolve_aborted_tools' }>
|
|
676
|
+
).payload;
|
|
677
|
+
|
|
678
|
+
const events: AgentEvent[] = [];
|
|
679
|
+
const sessionLogId = `${state.sessionId}:${state.stepCount}`;
|
|
680
|
+
const newState = structuredClone(state);
|
|
681
|
+
|
|
682
|
+
log(
|
|
683
|
+
'[%s][resolve_aborted_tools] Resolving %d aborted tools',
|
|
684
|
+
sessionLogId,
|
|
685
|
+
toolsCalling.length,
|
|
686
|
+
);
|
|
687
|
+
|
|
688
|
+
// Get context from operation
|
|
689
|
+
const opContext = getOperationContext();
|
|
690
|
+
|
|
691
|
+
// Create tool messages for each aborted tool
|
|
692
|
+
await pMap(toolsCalling, async (toolPayload) => {
|
|
693
|
+
const toolName = `${toolPayload.identifier}/${toolPayload.apiName}`;
|
|
694
|
+
log(
|
|
695
|
+
'[%s][resolve_aborted_tools] Creating aborted tool message for %s',
|
|
696
|
+
sessionLogId,
|
|
697
|
+
toolName,
|
|
698
|
+
);
|
|
699
|
+
|
|
700
|
+
const toolMessageParams: CreateMessageParams = {
|
|
701
|
+
content: 'Tool execution was aborted by user.',
|
|
702
|
+
parentId: parentMessageId,
|
|
703
|
+
plugin: toolPayload,
|
|
704
|
+
pluginIntervention: { status: 'aborted' },
|
|
705
|
+
role: 'tool',
|
|
706
|
+
sessionId: opContext.sessionId!,
|
|
707
|
+
threadId: opContext.threadId,
|
|
708
|
+
tool_call_id: toolPayload.id,
|
|
709
|
+
topicId: opContext.topicId ?? undefined,
|
|
710
|
+
};
|
|
711
|
+
|
|
712
|
+
const createResult = await context.get().optimisticCreateMessage(toolMessageParams);
|
|
713
|
+
|
|
714
|
+
if (createResult) {
|
|
715
|
+
log(
|
|
716
|
+
'[%s][resolve_aborted_tools] Created aborted tool message: %s for %s',
|
|
717
|
+
sessionLogId,
|
|
718
|
+
createResult.id,
|
|
719
|
+
toolName,
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
log('[%s][resolve_aborted_tools] All aborted tool messages created', sessionLogId);
|
|
725
|
+
|
|
726
|
+
// Mark state as done since we're finishing after abort
|
|
727
|
+
newState.lastModified = new Date().toISOString();
|
|
728
|
+
newState.status = 'done';
|
|
729
|
+
|
|
730
|
+
events.push({
|
|
731
|
+
finalState: newState,
|
|
732
|
+
reason: 'user_aborted',
|
|
733
|
+
reasonDetail: 'User aborted operation with pending tool calls',
|
|
734
|
+
type: 'done',
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
return { events, newState };
|
|
738
|
+
},
|
|
739
|
+
|
|
507
740
|
/**
|
|
508
741
|
* Finish executor
|
|
509
742
|
* Completes the runtime execution
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { aiChatSelectors } from './slices/aiChat/selectors';
|
|
2
2
|
export { chatToolSelectors } from './slices/builtinTool/selectors';
|
|
3
3
|
export * from './slices/message/selectors';
|
|
4
|
+
export * from './slices/operation/selectors';
|
|
4
5
|
export * from './slices/portal/selectors';
|
|
5
6
|
export { threadSelectors } from './slices/thread/selectors';
|
|
6
7
|
export { topicSelectors } from './slices/topic/selectors';
|