@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.
Files changed (89) hide show
  1. package/CHANGELOG.md +50 -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 +18 -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/app/[variants]/(main)/discover/(list)/features/Pagination.tsx +1 -1
  21. package/src/features/ChatInput/ActionBar/STT/browser.tsx +2 -2
  22. package/src/features/ChatInput/ActionBar/STT/openai.tsx +2 -2
  23. package/src/features/Conversation/Messages/Group/Tool/Inspector/index.tsx +1 -1
  24. package/src/features/Conversation/Messages/User/index.tsx +3 -3
  25. package/src/features/Conversation/Messages/index.tsx +3 -3
  26. package/src/features/Conversation/components/AutoScroll.tsx +2 -2
  27. package/src/services/search.ts +2 -2
  28. package/src/store/chat/agents/GeneralChatAgent.ts +98 -0
  29. package/src/store/chat/agents/__tests__/GeneralChatAgent.test.ts +366 -0
  30. package/src/store/chat/agents/__tests__/createAgentExecutors/call-llm.test.ts +1217 -0
  31. package/src/store/chat/agents/__tests__/createAgentExecutors/call-tool.test.ts +1976 -0
  32. package/src/store/chat/agents/__tests__/createAgentExecutors/finish.test.ts +453 -0
  33. package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/index.ts +4 -0
  34. package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockInstructions.ts +126 -0
  35. package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockMessages.ts +94 -0
  36. package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockOperations.ts +96 -0
  37. package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockStore.ts +138 -0
  38. package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/assertions.ts +185 -0
  39. package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/index.ts +3 -0
  40. package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/operationTestUtils.ts +94 -0
  41. package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/testExecutor.ts +139 -0
  42. package/src/store/chat/agents/__tests__/createAgentExecutors/request-human-approve.test.ts +545 -0
  43. package/src/store/chat/agents/__tests__/createAgentExecutors/resolve-aborted-tools.test.ts +686 -0
  44. package/src/store/chat/agents/createAgentExecutors.ts +313 -80
  45. package/src/store/chat/selectors.ts +1 -0
  46. package/src/store/chat/slices/aiChat/__tests__/ai-chat.integration.test.ts +667 -0
  47. package/src/store/chat/slices/aiChat/actions/__tests__/cancel-functionality.test.ts +137 -27
  48. package/src/store/chat/slices/aiChat/actions/__tests__/conversationControl.test.ts +163 -125
  49. package/src/store/chat/slices/aiChat/actions/__tests__/conversationLifecycle.test.ts +12 -2
  50. package/src/store/chat/slices/aiChat/actions/__tests__/fixtures.ts +0 -2
  51. package/src/store/chat/slices/aiChat/actions/__tests__/helpers.ts +0 -2
  52. package/src/store/chat/slices/aiChat/actions/__tests__/streamingExecutor.test.ts +286 -19
  53. package/src/store/chat/slices/aiChat/actions/__tests__/streamingStates.test.ts +0 -112
  54. package/src/store/chat/slices/aiChat/actions/conversationControl.ts +42 -99
  55. package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +90 -57
  56. package/src/store/chat/slices/aiChat/actions/generateAIGroupChat.ts +5 -25
  57. package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +220 -98
  58. package/src/store/chat/slices/aiChat/actions/streamingStates.ts +0 -34
  59. package/src/store/chat/slices/aiChat/initialState.ts +0 -28
  60. package/src/store/chat/slices/aiChat/selectors.test.ts +280 -0
  61. package/src/store/chat/slices/aiChat/selectors.ts +31 -7
  62. package/src/store/chat/slices/builtinTool/actions/__tests__/localSystem.test.ts +21 -30
  63. package/src/store/chat/slices/builtinTool/actions/__tests__/search.test.ts +29 -49
  64. package/src/store/chat/slices/builtinTool/actions/interpreter.ts +83 -48
  65. package/src/store/chat/slices/builtinTool/actions/localSystem.ts +78 -28
  66. package/src/store/chat/slices/builtinTool/actions/search.ts +146 -59
  67. package/src/store/chat/slices/builtinTool/selectors.test.ts +258 -0
  68. package/src/store/chat/slices/builtinTool/selectors.ts +25 -4
  69. package/src/store/chat/slices/message/action.test.ts +134 -16
  70. package/src/store/chat/slices/message/actions/internals.ts +33 -7
  71. package/src/store/chat/slices/message/actions/optimisticUpdate.ts +85 -52
  72. package/src/store/chat/slices/message/initialState.ts +0 -10
  73. package/src/store/chat/slices/message/selectors/messageState.ts +34 -12
  74. package/src/store/chat/slices/operation/__tests__/actions.test.ts +712 -16
  75. package/src/store/chat/slices/operation/__tests__/integration.test.ts +342 -0
  76. package/src/store/chat/slices/operation/__tests__/selectors.test.ts +257 -17
  77. package/src/store/chat/slices/operation/actions.ts +218 -11
  78. package/src/store/chat/slices/operation/selectors.ts +135 -6
  79. package/src/store/chat/slices/operation/types.ts +29 -3
  80. package/src/store/chat/slices/plugin/action.test.ts +30 -322
  81. package/src/store/chat/slices/plugin/actions/internals.ts +0 -14
  82. package/src/store/chat/slices/plugin/actions/optimisticUpdate.ts +21 -19
  83. package/src/store/chat/slices/plugin/actions/pluginTypes.ts +45 -27
  84. package/src/store/chat/slices/plugin/actions/publicApi.ts +3 -4
  85. package/src/store/chat/slices/plugin/actions/workflow.ts +0 -55
  86. package/src/store/chat/slices/thread/selectors/index.ts +4 -2
  87. package/src/store/chat/slices/translate/action.ts +54 -41
  88. package/src/tools/web-browsing/ExecutionRuntime/index.ts +5 -2
  89. package/src/tools/web-browsing/Portal/Search/Footer.tsx +11 -9
@@ -28,39 +28,12 @@ import { ChatStore } from '@/store/chat/store';
28
28
  import { getFileStoreState } from '@/store/file/store';
29
29
  import { toolInterventionSelectors } from '@/store/user/selectors';
30
30
  import { getUserStoreState } from '@/store/user/store';
31
- import { setNamespace } from '@/utils/storeDebug';
32
31
 
33
32
  import { topicSelectors } from '../../../selectors';
34
33
  import { messageMapKey } from '../../../utils/messageMapKey';
35
34
 
36
- const n = setNamespace('ai');
37
35
  const log = debug('lobe-store:streaming-executor');
38
36
 
39
- interface ProcessMessageParams {
40
- traceId?: string;
41
- isWelcomeQuestion?: boolean;
42
- inSearchWorkflow?: boolean;
43
- /**
44
- * the RAG query content, should be embedding and used in the semantic search
45
- */
46
- ragQuery?: string;
47
- threadId?: string;
48
- inPortalThread?: boolean;
49
-
50
- groupId?: string;
51
- agentId?: string;
52
- agentConfig?: any; // Agent configuration for group chat agents
53
-
54
- /**
55
- * Explicit sessionId for this execution (avoids using global activeId)
56
- */
57
- sessionId?: string;
58
- /**
59
- * Explicit topicId for this execution (avoids using global activeTopicId)
60
- */
61
- topicId?: string | null;
62
- }
63
-
64
37
  /**
65
38
  * Core streaming execution actions for AI chat
66
39
  */
@@ -89,18 +62,21 @@ export interface StreamingExecutorAction {
89
62
  /**
90
63
  * Retrieves an AI-generated chat message from the backend service with streaming
91
64
  */
92
- internal_fetchAIChatMessage: (input: {
93
- messages: UIChatMessage[];
65
+ internal_fetchAIChatMessage: (params: {
94
66
  messageId: string;
95
- params?: ProcessMessageParams;
67
+ messages: UIChatMessage[];
96
68
  model: string;
97
69
  provider: string;
70
+ operationId?: string;
71
+ agentConfig?: any;
72
+ traceId?: string;
98
73
  }) => Promise<{
99
74
  isFunctionCall: boolean;
100
75
  tools?: ChatToolPayload[];
101
76
  tool_calls?: MessageToolCall[];
102
77
  content: string;
103
78
  traceId?: string;
79
+ finishType?: 'done' | 'error' | 'abort';
104
80
  usage?: ModelUsage;
105
81
  }>;
106
82
  /**
@@ -119,6 +95,14 @@ export interface StreamingExecutorAction {
119
95
  * Explicit topicId for this execution (avoids using global activeTopicId)
120
96
  */
121
97
  topicId?: string | null;
98
+ /**
99
+ * Operation ID for this execution (automatically created if not provided)
100
+ */
101
+ operationId?: string;
102
+ /**
103
+ * Parent operation ID (creates a child operation if provided)
104
+ */
105
+ parentOperationId?: string;
122
106
  inSearchWorkflow?: boolean;
123
107
  /**
124
108
  * the RAG query content, should be embedding and used in the semantic search
@@ -219,37 +203,74 @@ export const streamingExecutor: StateCreator<
219
203
  return { state, context };
220
204
  },
221
205
 
222
- internal_fetchAIChatMessage: async ({ messages, messageId, params, provider, model }) => {
206
+ internal_fetchAIChatMessage: async ({
207
+ messageId,
208
+ messages,
209
+ model,
210
+ provider,
211
+ operationId,
212
+ agentConfig,
213
+ traceId: traceIdParam,
214
+ }) => {
223
215
  const {
224
- internal_toggleChatLoading,
225
- refreshMessages,
226
216
  optimisticUpdateMessageContent,
227
217
  internal_dispatchMessage,
228
218
  internal_toggleToolCallingStreaming,
229
- internal_toggleChatReasoning,
230
219
  } = get();
231
220
 
232
- const abortController = internal_toggleChatLoading(
233
- true,
234
- messageId,
235
- n('generateMessage(start)', { messageId, messages }),
236
- );
221
+ // Get sessionId, topicId, and abortController from operation
222
+ let sessionId: string;
223
+ let topicId: string | null | undefined;
224
+ let traceId: string | undefined = traceIdParam;
225
+ let abortController: AbortController;
226
+
227
+ if (operationId) {
228
+ const operation = get().operations[operationId];
229
+ if (!operation) {
230
+ log('[internal_fetchAIChatMessage] ERROR: Operation not found: %s', operationId);
231
+ throw new Error(`Operation not found: ${operationId}`);
232
+ }
233
+ sessionId = operation.context.sessionId!;
234
+ topicId = operation.context.topicId;
235
+ abortController = operation.abortController; // 👈 Use operation's abortController
236
+ log(
237
+ '[internal_fetchAIChatMessage] get context from operation %s: sessionId=%s, topicId=%s, aborted=%s',
238
+ operationId,
239
+ sessionId,
240
+ topicId,
241
+ abortController.signal.aborted,
242
+ );
243
+ // Get traceId from operation metadata if not explicitly provided
244
+ if (!traceId) {
245
+ traceId = operation.metadata?.traceId;
246
+ }
247
+ } else {
248
+ // Fallback to global state (for legacy code paths without operation)
249
+ sessionId = get().activeId;
250
+ topicId = get().activeTopicId;
251
+ abortController = new AbortController();
252
+ log(
253
+ '[internal_fetchAIChatMessage] use global context: sessionId=%s, topicId=%s',
254
+ sessionId,
255
+ topicId,
256
+ );
257
+ }
237
258
 
238
- const agentConfig =
239
- params?.agentConfig || agentSelectors.currentAgentConfig(getAgentStoreState());
259
+ // Get agent config from params or use current
260
+ const finalAgentConfig = agentConfig || agentSelectors.currentAgentConfig(getAgentStoreState());
240
261
  const chatConfig = agentChatConfigSelectors.currentChatConfig(getAgentStoreState());
241
262
 
242
263
  // ================================== //
243
264
  // messages uniformly preprocess //
244
265
  // ================================== //
245
266
  // 4. handle max_tokens
246
- agentConfig.params.max_tokens = chatConfig.enableMaxTokens
247
- ? agentConfig.params.max_tokens
267
+ finalAgentConfig.params.max_tokens = chatConfig.enableMaxTokens
268
+ ? finalAgentConfig.params.max_tokens
248
269
  : undefined;
249
270
 
250
271
  // 5. handle reasoning_effort
251
- agentConfig.params.reasoning_effort = chatConfig.enableReasoningEffort
252
- ? agentConfig.params.reasoning_effort
272
+ finalAgentConfig.params.reasoning_effort = chatConfig.enableReasoningEffort
273
+ ? finalAgentConfig.params.reasoning_effort
253
274
  : undefined;
254
275
 
255
276
  let isFunctionCall = false;
@@ -261,13 +282,11 @@ export const streamingExecutor: StateCreator<
261
282
  let thinking = '';
262
283
  let thinkingStartAt: number;
263
284
  let duration: number | undefined;
285
+ let reasoningOperationId: string | undefined;
286
+ let finishType: 'done' | 'error' | 'abort' | undefined;
264
287
  // to upload image
265
288
  const uploadTasks: Map<string, Promise<{ id?: string; url?: string }>> = new Map();
266
289
 
267
- const context: { sessionId: string; topicId?: string | null } = {
268
- sessionId: params?.sessionId || get().activeId,
269
- topicId: params?.topicId,
270
- };
271
290
  // Throttle tool_calls updates to prevent excessive re-renders (max once per 300ms)
272
291
  const throttledUpdateToolCalls = throttle(
273
292
  (toolCalls: any[]) => {
@@ -277,7 +296,7 @@ export const streamingExecutor: StateCreator<
277
296
  type: 'updateMessage',
278
297
  value: { tools: get().internal_transformToolCalls(toolCalls) },
279
298
  },
280
- context,
299
+ { operationId },
281
300
  );
282
301
  },
283
302
  300,
@@ -293,24 +312,28 @@ export const streamingExecutor: StateCreator<
293
312
  messages,
294
313
  model,
295
314
  provider,
296
- ...agentConfig.params,
297
- plugins: agentConfig.plugins,
315
+ ...finalAgentConfig.params,
316
+ plugins: finalAgentConfig.plugins,
298
317
  },
299
318
  historySummary: historySummary?.content,
300
319
  trace: {
301
- traceId: params?.traceId,
302
- sessionId: params?.sessionId ?? get().activeId,
303
- topicId:
304
- (params?.topicId !== undefined ? params.topicId : get().activeTopicId) ?? undefined,
320
+ traceId,
321
+ sessionId,
322
+ topicId: topicId ?? undefined,
305
323
  traceName: TraceNameMap.Conversation,
306
324
  },
307
325
  onErrorHandle: async (error) => {
308
- await messageService.updateMessageError(messageId, error, context);
309
- await refreshMessages(params?.sessionId, params?.topicId);
326
+ log(
327
+ '[internal_fetchAIChatMessage] onError: messageId=%s, error=%s, operationId=%s',
328
+ messageId,
329
+ error.message,
330
+ operationId,
331
+ );
332
+ await get().optimisticUpdateMessageError(messageId, error, { operationId });
310
333
  },
311
334
  onFinish: async (
312
335
  content,
313
- { traceId, observationId, toolCalls, reasoning, grounding, usage, speed },
336
+ { traceId, observationId, toolCalls, reasoning, grounding, usage, speed, type },
314
337
  ) => {
315
338
  // if there is traceId, update it
316
339
  if (traceId) {
@@ -318,7 +341,7 @@ export const streamingExecutor: StateCreator<
318
341
  messageService.updateMessage(
319
342
  messageId,
320
343
  { traceId, observationId: observationId ?? undefined },
321
- context,
344
+ { sessionId, topicId },
322
345
  );
323
346
  }
324
347
 
@@ -358,7 +381,14 @@ export const streamingExecutor: StateCreator<
358
381
  }
359
382
 
360
383
  finalUsage = usage;
361
- internal_toggleChatReasoning(false, messageId, n('toggleChatReasoning/false') as string);
384
+ finishType = type;
385
+
386
+ log(
387
+ '[internal_fetchAIChatMessage] onFinish: messageId=%s, finishType=%s, operationId=%s',
388
+ messageId,
389
+ type,
390
+ operationId,
391
+ );
362
392
 
363
393
  // update the content after fetch result
364
394
  await optimisticUpdateMessageContent(
@@ -371,9 +401,9 @@ export const streamingExecutor: StateCreator<
371
401
  : undefined,
372
402
  search: !!grounding?.citations ? grounding : undefined,
373
403
  imageList: finalImages.length > 0 ? finalImages : undefined,
374
- metadata: speed ? { ...usage, ...speed } : usage,
404
+ metadata: { ...usage, ...speed, performance: speed, usage, finishType: type },
375
405
  },
376
- context,
406
+ { operationId },
377
407
  );
378
408
  },
379
409
  onMessageHandle: async (chunk) => {
@@ -398,7 +428,7 @@ export const streamingExecutor: StateCreator<
398
428
  },
399
429
  },
400
430
  },
401
- context,
431
+ { operationId },
402
432
  );
403
433
  break;
404
434
  }
@@ -412,7 +442,7 @@ export const streamingExecutor: StateCreator<
412
442
  imageList: chunk.images.map((i) => ({ id: i.id, url: i.data, alt: i.id })),
413
443
  },
414
444
  },
415
- context,
445
+ { operationId },
416
446
  );
417
447
  const image = chunk.image;
418
448
 
@@ -436,16 +466,20 @@ export const streamingExecutor: StateCreator<
436
466
  if (!duration) {
437
467
  duration = Date.now() - thinkingStartAt;
438
468
 
439
- const isInChatReasoning = get().reasoningLoadingIds.includes(messageId);
440
- if (isInChatReasoning) {
441
- internal_toggleChatReasoning(
442
- false,
443
- messageId,
444
- n('toggleChatReasoning/false') as string,
445
- );
469
+ // Complete reasoning operation if it exists
470
+ if (reasoningOperationId) {
471
+ get().completeOperation(reasoningOperationId);
472
+ reasoningOperationId = undefined;
446
473
  }
447
474
  }
448
475
 
476
+ log(
477
+ '[text stream] messageId=%s, output length=%d, operationId=%s',
478
+ messageId,
479
+ output.length,
480
+ operationId,
481
+ );
482
+
449
483
  internal_dispatchMessage(
450
484
  {
451
485
  id: messageId,
@@ -455,7 +489,7 @@ export const streamingExecutor: StateCreator<
455
489
  reasoning: !!thinking ? { content: thinking, duration } : undefined,
456
490
  },
457
491
  },
458
- context,
492
+ { operationId },
459
493
  );
460
494
  break;
461
495
  }
@@ -464,11 +498,17 @@ export const streamingExecutor: StateCreator<
464
498
  // if there is no thinkingStartAt, it means the start of reasoning
465
499
  if (!thinkingStartAt) {
466
500
  thinkingStartAt = Date.now();
467
- internal_toggleChatReasoning(
468
- true,
469
- messageId,
470
- n('toggleChatReasoning/true') as string,
471
- );
501
+
502
+ // Create reasoning operation
503
+ const { operationId: reasoningOpId } = get().startOperation({
504
+ type: 'reasoning',
505
+ context: { sessionId, topicId, messageId },
506
+ parentOperationId: operationId,
507
+ });
508
+ reasoningOperationId = reasoningOpId;
509
+
510
+ // Associate message with reasoning operation
511
+ get().associateMessageWithOperation(messageId, reasoningOperationId);
472
512
  }
473
513
 
474
514
  thinking += chunk.text;
@@ -479,7 +519,7 @@ export const streamingExecutor: StateCreator<
479
519
  type: 'updateMessage',
480
520
  value: { reasoning: { content: thinking } },
481
521
  },
482
- context,
522
+ { operationId },
483
523
  );
484
524
  break;
485
525
  }
@@ -489,24 +529,25 @@ export const streamingExecutor: StateCreator<
489
529
  internal_toggleToolCallingStreaming(messageId, chunk.isAnimationActives);
490
530
  throttledUpdateToolCalls(chunk.tool_calls);
491
531
  isFunctionCall = true;
492
- const isInChatReasoning = get().reasoningLoadingIds.includes(messageId);
493
- if (isInChatReasoning) {
494
- if (!duration) {
495
- duration = Date.now() - thinkingStartAt;
496
- }
497
532
 
498
- internal_toggleChatReasoning(
499
- false,
500
- messageId,
501
- n('toggleChatReasoning/false') as string,
502
- );
533
+ // Complete reasoning operation if it exists
534
+ if (!duration && reasoningOperationId) {
535
+ duration = Date.now() - thinkingStartAt;
536
+ get().completeOperation(reasoningOperationId);
537
+ reasoningOperationId = undefined;
503
538
  }
504
539
  }
505
540
  }
506
541
  },
507
542
  });
508
543
 
509
- internal_toggleChatLoading(false, messageId, n('generateMessage(end)') as string);
544
+ log(
545
+ '[internal_fetchAIChatMessage] completed: messageId=%s, finishType=%s, isFunctionCall=%s, operationId=%s',
546
+ messageId,
547
+ finishType,
548
+ isFunctionCall,
549
+ operationId,
550
+ );
510
551
 
511
552
  return {
512
553
  isFunctionCall,
@@ -515,6 +556,7 @@ export const streamingExecutor: StateCreator<
515
556
  tools,
516
557
  usage: finalUsage,
517
558
  tool_calls,
559
+ finishType,
518
560
  };
519
561
  },
520
562
 
@@ -533,8 +575,34 @@ export const streamingExecutor: StateCreator<
533
575
  const topicId = paramTopicId !== undefined ? paramTopicId : activeTopicId;
534
576
  const messageKey = messageMapKey(sessionId, topicId);
535
577
 
578
+ // Create or use provided operation
579
+ let operationId = params.operationId;
580
+ if (!operationId) {
581
+ const { operationId: newOperationId } = get().startOperation({
582
+ type: 'execAgentRuntime',
583
+ context: {
584
+ sessionId,
585
+ topicId,
586
+ messageId: parentMessageId,
587
+ threadId: params.threadId,
588
+ },
589
+ parentOperationId: params.parentOperationId, // Pass parent operation ID
590
+ label: 'AI Generation',
591
+ metadata: {
592
+ // Mark if this operation is in thread context
593
+ // Thread operations should not affect main window UI state
594
+ inThread: params.inPortalThread || false,
595
+ },
596
+ });
597
+ operationId = newOperationId;
598
+
599
+ // Associate message with operation
600
+ get().associateMessageWithOperation(parentMessageId, operationId);
601
+ }
602
+
536
603
  log(
537
- '[internal_execAgentRuntime] start, sessionId: %s, topicId: %s, messageKey: %s, parentMessageId: %s, parentMessageType: %s, messages count: %d',
604
+ '[internal_execAgentRuntime] start, operationId: %s, sessionId: %s, topicId: %s, messageKey: %s, parentMessageId: %s, parentMessageType: %s, messages count: %d',
605
+ operationId,
538
606
  sessionId,
539
607
  topicId,
540
608
  messageKey,
@@ -624,14 +692,19 @@ export const streamingExecutor: StateCreator<
624
692
  executors: createAgentExecutors({
625
693
  get,
626
694
  messageKey,
695
+ operationId,
627
696
  parentId: params.parentMessageId,
628
- params: {
629
- ...params,
630
- sessionId,
631
- topicId,
632
- },
633
697
  skipCreateFirstMessage: params.skipCreateFirstMessage,
634
698
  }),
699
+ getOperation: (opId: string) => {
700
+ const op = get().operations[opId];
701
+ if (!op) throw new Error(`Operation not found: ${opId}`);
702
+ return {
703
+ abortController: op.abortController,
704
+ context: op.context,
705
+ };
706
+ },
707
+ operationId,
635
708
  });
636
709
 
637
710
  // Create agent state and context with user intervention config
@@ -657,6 +730,22 @@ export const streamingExecutor: StateCreator<
657
730
  // Execute the agent runtime loop
658
731
  let stepCount = 0;
659
732
  while (state.status !== 'done' && state.status !== 'error') {
733
+ // Check if operation has been cancelled
734
+ const currentOperation = get().operations[operationId];
735
+ if (currentOperation?.status === 'cancelled') {
736
+ log('[internal_execAgentRuntime] Operation cancelled, marking state as interrupted');
737
+
738
+ // Update state status to 'interrupted' so agent can handle abort
739
+ state = { ...state, status: 'interrupted' };
740
+
741
+ // Let agent handle the abort (will clean up pending tools if needed)
742
+ const result = await runtime.step(state, nextContext);
743
+ state = result.newState;
744
+
745
+ log('[internal_execAgentRuntime] Operation cancelled, stopping loop');
746
+ break;
747
+ }
748
+
660
749
  stepCount++;
661
750
  log(
662
751
  '[internal_execAgentRuntime][step-%d]: phase=%s, status=%s',
@@ -702,6 +791,28 @@ export const streamingExecutor: StateCreator<
702
791
 
703
792
  state = result.newState;
704
793
 
794
+ // Check if operation was cancelled after step completion
795
+ const operationAfterStep = get().operations[operationId];
796
+ if (operationAfterStep?.status === 'cancelled') {
797
+ log(
798
+ '[internal_execAgentRuntime] Operation cancelled after step %d, marking state as interrupted',
799
+ stepCount,
800
+ );
801
+
802
+ // Set state.status to 'interrupted' to trigger agent abort handling
803
+ state = { ...state, status: 'interrupted' };
804
+
805
+ // Let agent handle the abort (will clean up pending tools if needed)
806
+ // Use result.nextContext if available (e.g., llm_result with tool calls)
807
+ // otherwise fallback to current nextContext
808
+ const contextForAbort = result.nextContext || nextContext;
809
+ const abortResult = await runtime.step(state, contextForAbort);
810
+ state = abortResult.newState;
811
+
812
+ log('[internal_execAgentRuntime] Operation cancelled, stopping loop');
813
+ break;
814
+ }
815
+
705
816
  // If no nextContext, stop execution
706
817
  if (!result.nextContext) {
707
818
  log('[internal_execAgentRuntime] No next context, stopping loop');
@@ -723,13 +834,24 @@ export const streamingExecutor: StateCreator<
723
834
  const assistantMessage = finalMessages.findLast((m) => m.role === 'assistant');
724
835
  if (assistantMessage) {
725
836
  await get().optimisticUpdateMessageRAG(assistantMessage.id, params.ragMetadata, {
726
- sessionId,
727
- topicId,
837
+ operationId,
728
838
  });
729
839
  log('[internal_execAgentRuntime] RAG metadata updated for assistant message');
730
840
  }
731
841
  }
732
842
 
843
+ // Complete operation
844
+ if (state.status === 'done') {
845
+ get().completeOperation(operationId);
846
+ log('[internal_execAgentRuntime] Operation completed successfully');
847
+ } else if (state.status === 'error') {
848
+ get().failOperation(operationId, {
849
+ type: 'runtime_error',
850
+ message: 'Agent runtime execution failed',
851
+ });
852
+ log('[internal_execAgentRuntime] Operation failed');
853
+ }
854
+
733
855
  log('[internal_execAgentRuntime] completed');
734
856
 
735
857
  // Desktop notification (if not in tools calling mode)
@@ -3,36 +3,11 @@ import { produce } from 'immer';
3
3
  import { StateCreator } from 'zustand/vanilla';
4
4
 
5
5
  import { ChatStore } from '@/store/chat/store';
6
- import { Action } from '@/utils/storeDebug';
7
6
 
8
7
  /**
9
8
  * Manages loading states during streaming operations
10
9
  */
11
10
  export interface StreamingStatesAction {
12
- /**
13
- * Toggles the loading state for AI message generation, managing the UI feedback
14
- */
15
- internal_toggleChatLoading: (
16
- loading: boolean,
17
- id?: string,
18
- action?: Action,
19
- ) => AbortController | undefined;
20
- /**
21
- * Toggles the loading state for AI message reasoning, managing the UI feedback
22
- */
23
- internal_toggleChatReasoning: (
24
- loading: boolean,
25
- id?: string,
26
- action?: string,
27
- ) => AbortController | undefined;
28
- /**
29
- * Toggles the loading state for messages in tools calling
30
- */
31
- internal_toggleMessageInToolsCalling: (
32
- loading: boolean,
33
- id?: string,
34
- action?: Action,
35
- ) => AbortController | undefined;
36
11
  /**
37
12
  * Toggles the loading state for search workflow
38
13
  */
@@ -49,15 +24,6 @@ export const streamingStates: StateCreator<
49
24
  [],
50
25
  StreamingStatesAction
51
26
  > = (set, get) => ({
52
- internal_toggleChatLoading: (loading, id, action) => {
53
- return get().internal_toggleLoadingArrays('chatLoadingIds', loading, id, action);
54
- },
55
- internal_toggleChatReasoning: (loading, id, action) => {
56
- return get().internal_toggleLoadingArrays('reasoningLoadingIds', loading, id, action);
57
- },
58
- internal_toggleMessageInToolsCalling: (loading, id) => {
59
- return get().internal_toggleLoadingArrays('messageInToolsCallingIds', loading, id);
60
- },
61
27
  internal_toggleSearchWorkflow: (loading, id) => {
62
28
  return get().internal_toggleLoadingArrays('searchWorkflowLoadingIds', loading, id);
63
29
  },
@@ -1,36 +1,13 @@
1
1
  import type { ChatInputEditor } from '@/features/ChatInput';
2
2
 
3
- export interface MainSendMessageOperation {
4
- abortController?: AbortController | null;
5
- inputEditorTempState?: any | null;
6
- inputSendErrorMsg?: string;
7
- isLoading: boolean;
8
- }
9
-
10
3
  export interface ChatAIChatState {
11
- /**
12
- * is the AI message is generating
13
- */
14
- chatLoadingIds: string[];
15
- chatLoadingIdsAbortController?: AbortController;
16
4
  inputFiles: File[];
17
5
  inputMessage: string;
18
6
  mainInputEditor: ChatInputEditor | null;
19
- /**
20
- * sendMessageInServer operations map, keyed by sessionId|topicId
21
- * Contains both loading state and AbortController
22
- */
23
- mainSendMessageOperations: Record<string, MainSendMessageOperation>;
24
- messageInToolsCallingIds: string[];
25
7
  /**
26
8
  * is the message is in RAG flow
27
9
  */
28
10
  messageRAGLoadingIds: string[];
29
- pluginApiLoadingIds: string[];
30
- /**
31
- * is the AI message is reasoning
32
- */
33
- reasoningLoadingIds: string[];
34
11
  searchWorkflowLoadingIds: string[];
35
12
  threadInputEditor: ChatInputEditor | null;
36
13
  /**
@@ -40,15 +17,10 @@ export interface ChatAIChatState {
40
17
  }
41
18
 
42
19
  export const initialAiChatState: ChatAIChatState = {
43
- chatLoadingIds: [],
44
20
  inputFiles: [],
45
21
  inputMessage: '',
46
22
  mainInputEditor: null,
47
- mainSendMessageOperations: {},
48
- messageInToolsCallingIds: [],
49
23
  messageRAGLoadingIds: [],
50
- pluginApiLoadingIds: [],
51
- reasoningLoadingIds: [],
52
24
  searchWorkflowLoadingIds: [],
53
25
  threadInputEditor: null,
54
26
  toolCallingStreamIds: {},