@lobehub/lobehub 2.0.0-next.85 → 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.
Files changed (88) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/apps/desktop/src/main/modules/networkProxy/dispatcher.ts +16 -16
  3. package/apps/desktop/src/main/modules/networkProxy/tester.ts +11 -11
  4. package/apps/desktop/src/main/modules/networkProxy/urlBuilder.ts +3 -3
  5. package/apps/desktop/src/main/modules/networkProxy/validator.ts +10 -10
  6. package/changelog/v1.json +9 -0
  7. package/package.json +1 -1
  8. package/packages/agent-runtime/src/core/runtime.ts +36 -1
  9. package/packages/agent-runtime/src/types/event.ts +1 -0
  10. package/packages/agent-runtime/src/types/generalAgent.ts +16 -0
  11. package/packages/agent-runtime/src/types/instruction.ts +30 -0
  12. package/packages/agent-runtime/src/types/runtime.ts +7 -0
  13. package/packages/types/src/message/common/metadata.ts +3 -0
  14. package/packages/types/src/message/common/tools.ts +2 -2
  15. package/packages/types/src/tool/search/index.ts +8 -2
  16. package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/V1Mobile/index.tsx +2 -2
  17. package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/V1Mobile/useSend.ts +7 -2
  18. package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/useSend.ts +15 -14
  19. package/src/app/[variants]/(main)/chat/session/features/SessionListContent/List/Item/index.tsx +2 -2
  20. package/src/features/ChatInput/ActionBar/STT/browser.tsx +2 -2
  21. package/src/features/ChatInput/ActionBar/STT/openai.tsx +2 -2
  22. package/src/features/Conversation/Messages/Group/Tool/Inspector/index.tsx +1 -1
  23. package/src/features/Conversation/Messages/User/index.tsx +3 -3
  24. package/src/features/Conversation/Messages/index.tsx +3 -3
  25. package/src/features/Conversation/components/AutoScroll.tsx +2 -2
  26. package/src/services/search.ts +2 -2
  27. package/src/store/chat/agents/GeneralChatAgent.ts +98 -0
  28. package/src/store/chat/agents/__tests__/GeneralChatAgent.test.ts +366 -0
  29. package/src/store/chat/agents/__tests__/createAgentExecutors/call-llm.test.ts +1217 -0
  30. package/src/store/chat/agents/__tests__/createAgentExecutors/call-tool.test.ts +1976 -0
  31. package/src/store/chat/agents/__tests__/createAgentExecutors/finish.test.ts +453 -0
  32. package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/index.ts +4 -0
  33. package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockInstructions.ts +126 -0
  34. package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockMessages.ts +94 -0
  35. package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockOperations.ts +96 -0
  36. package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockStore.ts +138 -0
  37. package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/assertions.ts +185 -0
  38. package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/index.ts +3 -0
  39. package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/operationTestUtils.ts +94 -0
  40. package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/testExecutor.ts +139 -0
  41. package/src/store/chat/agents/__tests__/createAgentExecutors/request-human-approve.test.ts +545 -0
  42. package/src/store/chat/agents/__tests__/createAgentExecutors/resolve-aborted-tools.test.ts +686 -0
  43. package/src/store/chat/agents/createAgentExecutors.ts +313 -80
  44. package/src/store/chat/selectors.ts +1 -0
  45. package/src/store/chat/slices/aiChat/__tests__/ai-chat.integration.test.ts +667 -0
  46. package/src/store/chat/slices/aiChat/actions/__tests__/cancel-functionality.test.ts +137 -27
  47. package/src/store/chat/slices/aiChat/actions/__tests__/conversationControl.test.ts +163 -125
  48. package/src/store/chat/slices/aiChat/actions/__tests__/conversationLifecycle.test.ts +12 -2
  49. package/src/store/chat/slices/aiChat/actions/__tests__/fixtures.ts +0 -2
  50. package/src/store/chat/slices/aiChat/actions/__tests__/helpers.ts +0 -2
  51. package/src/store/chat/slices/aiChat/actions/__tests__/streamingExecutor.test.ts +286 -19
  52. package/src/store/chat/slices/aiChat/actions/__tests__/streamingStates.test.ts +0 -112
  53. package/src/store/chat/slices/aiChat/actions/conversationControl.ts +42 -99
  54. package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +90 -57
  55. package/src/store/chat/slices/aiChat/actions/generateAIGroupChat.ts +5 -25
  56. package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +220 -98
  57. package/src/store/chat/slices/aiChat/actions/streamingStates.ts +0 -34
  58. package/src/store/chat/slices/aiChat/initialState.ts +0 -28
  59. package/src/store/chat/slices/aiChat/selectors.test.ts +280 -0
  60. package/src/store/chat/slices/aiChat/selectors.ts +31 -7
  61. package/src/store/chat/slices/builtinTool/actions/__tests__/localSystem.test.ts +21 -30
  62. package/src/store/chat/slices/builtinTool/actions/__tests__/search.test.ts +29 -49
  63. package/src/store/chat/slices/builtinTool/actions/interpreter.ts +83 -48
  64. package/src/store/chat/slices/builtinTool/actions/localSystem.ts +78 -28
  65. package/src/store/chat/slices/builtinTool/actions/search.ts +146 -59
  66. package/src/store/chat/slices/builtinTool/selectors.test.ts +258 -0
  67. package/src/store/chat/slices/builtinTool/selectors.ts +25 -4
  68. package/src/store/chat/slices/message/action.test.ts +134 -16
  69. package/src/store/chat/slices/message/actions/internals.ts +33 -7
  70. package/src/store/chat/slices/message/actions/optimisticUpdate.ts +85 -52
  71. package/src/store/chat/slices/message/initialState.ts +0 -10
  72. package/src/store/chat/slices/message/selectors/messageState.ts +34 -12
  73. package/src/store/chat/slices/operation/__tests__/actions.test.ts +712 -16
  74. package/src/store/chat/slices/operation/__tests__/integration.test.ts +342 -0
  75. package/src/store/chat/slices/operation/__tests__/selectors.test.ts +257 -17
  76. package/src/store/chat/slices/operation/actions.ts +218 -11
  77. package/src/store/chat/slices/operation/selectors.ts +135 -6
  78. package/src/store/chat/slices/operation/types.ts +29 -3
  79. package/src/store/chat/slices/plugin/action.test.ts +30 -322
  80. package/src/store/chat/slices/plugin/actions/internals.ts +0 -14
  81. package/src/store/chat/slices/plugin/actions/optimisticUpdate.ts +21 -19
  82. package/src/store/chat/slices/plugin/actions/pluginTypes.ts +45 -27
  83. package/src/store/chat/slices/plugin/actions/publicApi.ts +3 -4
  84. package/src/store/chat/slices/plugin/actions/workflow.ts +0 -55
  85. package/src/store/chat/slices/thread/selectors/index.ts +4 -2
  86. package/src/store/chat/slices/translate/action.ts +54 -41
  87. package/src/tools/web-browsing/ExecutionRuntime/index.ts +5 -2
  88. 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
- params: {
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
- content: LOADING_FLAT,
84
- model: llmPayload.model,
85
- parentId: llmPayload.parentMessageId,
86
- provider: llmPayload.provider,
87
- role: 'assistant',
88
- sessionId: state.metadata!.sessionId!,
89
- threadId: state.metadata?.threadId,
90
- topicId: state.metadata?.topicId,
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: 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
- // Add llm_stream events (similar to backend)
161
- if (content) {
162
- events.push({
163
- chunk: { text: content, type: 'text' },
164
- type: 'llm_stream',
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: state.metadata!.sessionId!,
282
- threadId: context.params.threadId,
368
+ sessionId: opContext.sessionId!,
369
+ threadId: opContext.threadId,
283
370
  tool_call_id: chatToolPayload.id,
284
- topicId: state.metadata?.topicId,
371
+ topicId: opContext.topicId ?? undefined,
285
372
  };
286
373
 
287
- const createResult = await context.get().optimisticCreateMessage(toolMessageParams, {
288
- sessionId: state.metadata!.sessionId!,
289
- topicId: state.metadata?.topicId,
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
- log(
294
- '[%s][call_tool] ERROR: Failed to create tool message for tool_call_id: %s',
295
- sessionLogId,
296
- chatToolPayload.id,
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: state.metadata!.sessionId!,
466
- threadId: context.params.threadId,
629
+ sessionId: opContext.sessionId!,
630
+ threadId: opContext.threadId,
467
631
  tool_call_id: toolPayload.id,
468
- topicId: state.metadata?.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';