@lobehub/lobehub 2.0.0-next.326 → 2.0.0-next.328

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 (56) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v1.json +14 -0
  3. package/locales/en-US/chat.json +6 -1
  4. package/locales/zh-CN/chat.json +5 -0
  5. package/package.json +1 -1
  6. package/packages/agent-runtime/src/agents/GeneralChatAgent.ts +24 -0
  7. package/packages/agent-runtime/src/agents/__tests__/GeneralChatAgent.test.ts +210 -0
  8. package/packages/agent-runtime/src/types/instruction.ts +46 -2
  9. package/packages/builtin-tool-gtd/src/const.ts +1 -0
  10. package/packages/builtin-tool-gtd/src/executor/index.ts +38 -21
  11. package/packages/builtin-tool-gtd/src/manifest.ts +15 -0
  12. package/packages/builtin-tool-gtd/src/systemRole.ts +33 -1
  13. package/packages/builtin-tool-gtd/src/types.ts +55 -33
  14. package/packages/builtin-tool-local-system/src/client/Inspector/ReadLocalFile/index.tsx +1 -0
  15. package/packages/builtin-tool-local-system/src/client/Inspector/RunCommand/index.tsx +1 -1
  16. package/packages/builtin-tool-local-system/src/client/Render/WriteFile/index.tsx +1 -1
  17. package/packages/builtin-tool-local-system/src/client/Streaming/WriteFile/index.tsx +5 -1
  18. package/packages/builtin-tool-notebook/src/systemRole.ts +27 -7
  19. package/packages/conversation-flow/src/transformation/FlatListBuilder.ts +13 -1
  20. package/packages/conversation-flow/src/transformation/__tests__/FlatListBuilder.test.ts +40 -0
  21. package/packages/database/src/models/__tests__/messages/message.thread-query.test.ts +134 -1
  22. package/packages/database/src/models/message.ts +8 -1
  23. package/packages/database/src/models/thread.ts +1 -1
  24. package/packages/types/src/message/ui/chat.ts +2 -0
  25. package/packages/types/src/topic/thread.ts +20 -0
  26. package/src/components/StreamingMarkdown/index.tsx +10 -43
  27. package/src/features/ChatInput/ActionBar/Search/FunctionCallingModelSelect/index.tsx +6 -6
  28. package/src/features/Conversation/Messages/AssistantGroup/components/MessageContent.tsx +0 -2
  29. package/src/features/Conversation/Messages/Task/ClientTaskDetail/CompletedState.tsx +108 -0
  30. package/src/features/Conversation/Messages/Task/ClientTaskDetail/InitializingState.tsx +66 -0
  31. package/src/features/Conversation/Messages/Task/ClientTaskDetail/InstructionAccordion.tsx +63 -0
  32. package/src/features/Conversation/Messages/Task/ClientTaskDetail/ProcessingState.tsx +123 -0
  33. package/src/features/Conversation/Messages/Task/ClientTaskDetail/index.tsx +106 -0
  34. package/src/features/Conversation/Messages/Task/TaskDetailPanel/index.tsx +1 -0
  35. package/src/features/Conversation/Messages/Task/index.tsx +11 -6
  36. package/src/features/Conversation/Messages/Tasks/TaskItem/TaskTitle.tsx +3 -2
  37. package/src/features/Conversation/Messages/Tasks/shared/InitializingState.tsx +0 -4
  38. package/src/features/Conversation/Messages/Tasks/shared/utils.ts +22 -1
  39. package/src/features/Conversation/Messages/components/ContentLoading.tsx +1 -1
  40. package/src/features/Conversation/components/Thinking/index.tsx +9 -30
  41. package/src/features/Conversation/store/slices/data/action.ts +2 -3
  42. package/src/features/NavPanel/components/BackButton.tsx +10 -13
  43. package/src/features/NavPanel/components/NavPanelDraggable.tsx +4 -0
  44. package/src/hooks/useAutoScroll.ts +117 -0
  45. package/src/locales/default/chat.ts +6 -1
  46. package/src/server/routers/lambda/aiAgent.ts +239 -1
  47. package/src/server/routers/lambda/thread.ts +2 -0
  48. package/src/server/services/message/__tests__/index.test.ts +37 -0
  49. package/src/server/services/message/index.ts +6 -1
  50. package/src/services/aiAgent.ts +51 -0
  51. package/src/store/chat/agents/createAgentExecutors.ts +714 -12
  52. package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +6 -1
  53. package/src/store/chat/slices/message/actions/query.ts +33 -1
  54. package/src/store/chat/slices/message/selectors/displayMessage.test.ts +10 -0
  55. package/src/store/chat/slices/message/selectors/displayMessage.ts +1 -0
  56. package/src/store/chat/slices/operation/types.ts +4 -0
@@ -3,6 +3,8 @@ import {
3
3
  type AgentInstruction,
4
4
  type AgentInstructionCallLlm,
5
5
  type AgentInstructionCallTool,
6
+ type AgentInstructionExecClientTask,
7
+ type AgentInstructionExecClientTasks,
6
8
  type AgentInstructionExecTask,
7
9
  type AgentInstructionExecTasks,
8
10
  type AgentRuntimeContext,
@@ -15,13 +17,17 @@ import {
15
17
  type TasksBatchResultPayload,
16
18
  UsageCounter,
17
19
  } from '@lobechat/agent-runtime';
18
- import type { ChatToolPayload, CreateMessageParams } from '@lobechat/types';
20
+ import { isDesktop } from '@lobechat/const';
21
+ import type { ChatToolPayload, ConversationContext, CreateMessageParams } from '@lobechat/types';
19
22
  import debug from 'debug';
20
23
  import pMap from 'p-map';
21
24
 
22
25
  import { LOADING_FLAT } from '@/const/message';
23
26
  import { aiAgentService } from '@/services/aiAgent';
27
+ import { agentByIdSelectors } from '@/store/agent/selectors';
28
+ import { getAgentStoreState } from '@/store/agent/store';
24
29
  import type { ChatStore } from '@/store/chat/store';
30
+ import { messageMapKey } from '@/store/chat/utils/messageMapKey';
25
31
  import { sleep } from '@/utils/sleep';
26
32
 
27
33
  const log = debug('lobe-store:agent-executors');
@@ -571,7 +577,9 @@ export const createAgentExecutors = (context: {
571
577
  const stateType = result.state?.type;
572
578
 
573
579
  // GTD async tasks need to be passed to Agent for exec_task/exec_tasks instruction
574
- if (stateType === 'execTask' || stateType === 'execTasks') {
580
+ // Includes both server-side (execTask/execTasks) and client-side (execClientTask/execClientTasks)
581
+ const execTaskStateTypes = ['execTask', 'execTasks', 'execClientTask', 'execClientTasks'];
582
+ if (execTaskStateTypes.includes(stateType)) {
575
583
  log(
576
584
  '[%s][call_tool] Detected %s state, passing to Agent for decision',
577
585
  sessionLogId,
@@ -955,7 +963,8 @@ export const createAgentExecutors = (context: {
955
963
  const taskMessageId = taskMessageResult.id;
956
964
  log('[%s] Created task message: %s', taskLogId, taskMessageId);
957
965
 
958
- // 2. Create task via backend API
966
+ // 2. Create and execute task on server
967
+ log('[%s] Using server-side execution', taskLogId);
959
968
  const createResult = await aiAgentService.execSubAgentTask({
960
969
  agentId,
961
970
  instruction: task.instruction,
@@ -1006,10 +1015,29 @@ export const createAgentExecutors = (context: {
1006
1015
  const startTime = Date.now();
1007
1016
 
1008
1017
  while (Date.now() - startTime < maxWait) {
1009
- // Check if operation has been cancelled
1018
+ // Check if parent operation has been cancelled
1010
1019
  const currentOperation = context.get().operations[state.operationId];
1011
1020
  if (currentOperation?.status === 'cancelled') {
1012
1021
  log('[%s] Operation cancelled, stopping polling', taskLogId);
1022
+
1023
+ // Send interrupt request to stop the server-side task
1024
+ try {
1025
+ await aiAgentService.interruptTask({ threadId: createResult.threadId });
1026
+ log('[%s] Sent interrupt request for cancelled task', taskLogId);
1027
+ } catch (err) {
1028
+ log('[%s] Failed to interrupt cancelled task: %O', taskLogId, err);
1029
+ }
1030
+
1031
+ // Update task message to cancelled state
1032
+ await context
1033
+ .get()
1034
+ .optimisticUpdateMessageContent(
1035
+ taskMessageId,
1036
+ 'Task was cancelled by user.',
1037
+ undefined,
1038
+ { operationId: state.operationId },
1039
+ );
1040
+
1013
1041
  const updatedMessages = context.get().dbMessagesMap[context.messageKey] || [];
1014
1042
  return {
1015
1043
  events,
@@ -1125,6 +1153,8 @@ export const createAgentExecutors = (context: {
1125
1153
 
1126
1154
  if (status.status === 'cancel') {
1127
1155
  log('[%s] Task was cancelled', taskLogId);
1156
+ // Note: Don't fail the operation here - it was cancelled intentionally
1157
+ // The cancel handler already updated the message
1128
1158
  await context
1129
1159
  .get()
1130
1160
  .optimisticUpdateMessageContent(taskMessageId, 'Task was cancelled', undefined, {
@@ -1161,6 +1191,15 @@ export const createAgentExecutors = (context: {
1161
1191
 
1162
1192
  // Timeout reached
1163
1193
  log('[%s] Task timeout after %dms', taskLogId, maxWait);
1194
+
1195
+ // Try to interrupt the task that timed out
1196
+ try {
1197
+ await aiAgentService.interruptTask({ threadId: createResult.threadId });
1198
+ log('[%s] Sent interrupt request for timed out task', taskLogId);
1199
+ } catch (err) {
1200
+ log('[%s] Failed to interrupt timed out task: %O', taskLogId, err);
1201
+ }
1202
+
1164
1203
  await context
1165
1204
  .get()
1166
1205
  .optimisticUpdateMessageContent(
@@ -1303,7 +1342,8 @@ export const createAgentExecutors = (context: {
1303
1342
  const taskMessageId = taskMessageResult.id;
1304
1343
  log('[%s] Created task message: %s', taskLogId, taskMessageId);
1305
1344
 
1306
- // 2. Create task via backend API (no groupId for single agent mode)
1345
+ // 2. Create and execute task on server
1346
+ log('[%s] Using server-side execution', taskLogId);
1307
1347
  const createResult = await aiAgentService.execSubAgentTask({
1308
1348
  agentId,
1309
1349
  instruction: task.instruction,
@@ -1333,16 +1373,35 @@ export const createAgentExecutors = (context: {
1333
1373
 
1334
1374
  log('[%s] Task created with threadId: %s', taskLogId, createResult.threadId);
1335
1375
 
1336
- // 3. Poll for task completion
1376
+ // 4. Poll for task completion
1337
1377
  const pollInterval = 3000; // 3 seconds
1338
1378
  const maxWait = task.timeout || 1_800_000; // Default 30 minutes
1339
1379
  const startTime = Date.now();
1340
1380
 
1341
1381
  while (Date.now() - startTime < maxWait) {
1342
- // Check if operation has been cancelled
1382
+ // Check if parent operation has been cancelled
1343
1383
  const currentOperation = context.get().operations[state.operationId];
1344
1384
  if (currentOperation?.status === 'cancelled') {
1345
1385
  log('[%s] Operation cancelled, stopping polling', taskLogId);
1386
+
1387
+ // Send interrupt request to stop the server-side task
1388
+ try {
1389
+ await aiAgentService.interruptTask({ threadId: createResult.threadId });
1390
+ log('[%s] Sent interrupt request for cancelled task', taskLogId);
1391
+ } catch (err) {
1392
+ log('[%s] Failed to interrupt cancelled task: %O', taskLogId, err);
1393
+ }
1394
+
1395
+ // Update task message to cancelled state
1396
+ await context
1397
+ .get()
1398
+ .optimisticUpdateMessageContent(
1399
+ taskMessageId,
1400
+ 'Task was cancelled by user.',
1401
+ undefined,
1402
+ { operationId: state.operationId },
1403
+ );
1404
+
1346
1405
  return {
1347
1406
  error: 'Operation cancelled',
1348
1407
  success: false,
@@ -1370,7 +1429,7 @@ export const createAgentExecutors = (context: {
1370
1429
 
1371
1430
  if (status.status === 'completed') {
1372
1431
  log('[%s] Task completed successfully', taskLogId);
1373
- // 4. Update task message with result
1432
+ // 5. Update task message with result
1374
1433
  if (status.result) {
1375
1434
  await context
1376
1435
  .get()
@@ -1387,13 +1446,14 @@ export const createAgentExecutors = (context: {
1387
1446
  }
1388
1447
 
1389
1448
  if (status.status === 'failed') {
1390
- log('[%s] Task failed: %s', taskLogId, status.error);
1449
+ const errorMessage = status.error || 'Unknown error';
1450
+ log('[%s] Task failed: %s', taskLogId, errorMessage);
1391
1451
  // Update task message with error
1392
1452
  await context
1393
1453
  .get()
1394
1454
  .optimisticUpdateMessageContent(
1395
1455
  taskMessageId,
1396
- `Task failed: ${status.error}`,
1456
+ `Task failed: ${errorMessage}`,
1397
1457
  undefined,
1398
1458
  { operationId: state.operationId },
1399
1459
  );
@@ -1407,7 +1467,8 @@ export const createAgentExecutors = (context: {
1407
1467
 
1408
1468
  if (status.status === 'cancel') {
1409
1469
  log('[%s] Task was cancelled', taskLogId);
1410
- // Update task message with cancelled status
1470
+ // Note: Don't fail the operation here - it was cancelled intentionally
1471
+ // The cancel handler already updated the message
1411
1472
  await context
1412
1473
  .get()
1413
1474
  .optimisticUpdateMessageContent(taskMessageId, 'Task was cancelled', undefined, {
@@ -1427,6 +1488,15 @@ export const createAgentExecutors = (context: {
1427
1488
 
1428
1489
  // Timeout reached
1429
1490
  log('[%s] Task timeout after %dms', taskLogId, maxWait);
1491
+
1492
+ // Try to interrupt the task that timed out
1493
+ try {
1494
+ await aiAgentService.interruptTask({ threadId: createResult.threadId });
1495
+ log('[%s] Sent interrupt request for timed out task', taskLogId);
1496
+ } catch (err) {
1497
+ log('[%s] Failed to interrupt timed out task: %O', taskLogId, err);
1498
+ }
1499
+
1430
1500
  // Update task message with timeout error
1431
1501
  await context
1432
1502
  .get()
@@ -1453,7 +1523,7 @@ export const createAgentExecutors = (context: {
1453
1523
  };
1454
1524
  }
1455
1525
  },
1456
- { concurrency: 5 }, // Limit concurrent tasks
1526
+ { concurrency: 15 }, // Limit concurrent tasks
1457
1527
  );
1458
1528
 
1459
1529
  log('[%s][exec_tasks] All tasks completed, results: %O', sessionLogId, results);
@@ -1481,6 +1551,638 @@ export const createAgentExecutors = (context: {
1481
1551
  } as AgentRuntimeContext,
1482
1552
  };
1483
1553
  },
1554
+
1555
+ /**
1556
+ * exec_client_task executor
1557
+ * Executes a single async task on the client side (desktop only)
1558
+ * Used when task requires local tools like file system or shell commands
1559
+ *
1560
+ * Flow:
1561
+ * 1. Create a task message (role: 'task') as placeholder
1562
+ * 2. Create Thread via API (for isolation)
1563
+ * 3. Execute using internal_execAgentRuntime (client-side)
1564
+ * 4. Update Thread status via API on completion
1565
+ * 5. Update task message content with result
1566
+ * 6. Return task_result phase with result
1567
+ */
1568
+ exec_client_task: async (instruction, state) => {
1569
+ const { parentMessageId, task } = (instruction as AgentInstructionExecClientTask).payload;
1570
+
1571
+ const events: AgentEvent[] = [];
1572
+ const sessionLogId = `${state.operationId}:${state.stepCount}`;
1573
+
1574
+ log(
1575
+ '[%s][exec_client_task] Starting client-side execution of task: %s',
1576
+ sessionLogId,
1577
+ task.description,
1578
+ );
1579
+
1580
+ // Check if we're on desktop - if not, this executor shouldn't have been called
1581
+ if (!isDesktop) {
1582
+ log(
1583
+ '[%s][exec_client_task] ERROR: Not on desktop, cannot execute client-side task',
1584
+ sessionLogId,
1585
+ );
1586
+ return {
1587
+ events,
1588
+ newState: state,
1589
+ nextContext: {
1590
+ payload: {
1591
+ parentMessageId,
1592
+ result: {
1593
+ error: 'Client-side task execution is only available on desktop',
1594
+ success: false,
1595
+ taskMessageId: '',
1596
+ threadId: '',
1597
+ },
1598
+ } as TaskResultPayload,
1599
+ phase: 'task_result',
1600
+ session: {
1601
+ messageCount: state.messages.length,
1602
+ sessionId: state.operationId,
1603
+ status: 'running',
1604
+ stepCount: state.stepCount + 1,
1605
+ },
1606
+ } as AgentRuntimeContext,
1607
+ };
1608
+ }
1609
+
1610
+ // Get context from operation
1611
+ const opContext = getOperationContext();
1612
+ const { agentId, topicId } = opContext;
1613
+
1614
+ if (!agentId || !topicId) {
1615
+ log('[%s][exec_client_task] No valid context, cannot execute task', sessionLogId);
1616
+ return {
1617
+ events,
1618
+ newState: state,
1619
+ nextContext: {
1620
+ payload: {
1621
+ parentMessageId,
1622
+ result: {
1623
+ error: 'No valid context available',
1624
+ success: false,
1625
+ taskMessageId: '',
1626
+ threadId: '',
1627
+ },
1628
+ } as TaskResultPayload,
1629
+ phase: 'task_result',
1630
+ session: {
1631
+ messageCount: state.messages.length,
1632
+ sessionId: state.operationId,
1633
+ status: 'running',
1634
+ stepCount: state.stepCount + 1,
1635
+ },
1636
+ } as AgentRuntimeContext,
1637
+ };
1638
+ }
1639
+
1640
+ const taskLogId = `${sessionLogId}:client-task`;
1641
+
1642
+ // Get agent's model and provider configuration
1643
+ const agentState = getAgentStoreState();
1644
+ const taskModel = agentByIdSelectors.getAgentModelById(agentId)(agentState);
1645
+ const taskProvider = agentByIdSelectors.getAgentModelProviderById(agentId)(agentState);
1646
+
1647
+ try {
1648
+ // 1. Create task message as placeholder with model/provider
1649
+ const taskMessageResult = await context.get().optimisticCreateMessage(
1650
+ {
1651
+ agentId,
1652
+ content: '',
1653
+ metadata: { instruction: task.instruction, taskTitle: task.description },
1654
+ model: taskModel,
1655
+ parentId: parentMessageId,
1656
+ provider: taskProvider,
1657
+ role: 'task',
1658
+ topicId,
1659
+ },
1660
+ { operationId: state.operationId },
1661
+ );
1662
+
1663
+ if (!taskMessageResult) {
1664
+ log('[%s] Failed to create task message', taskLogId);
1665
+ return {
1666
+ events,
1667
+ newState: state,
1668
+ nextContext: {
1669
+ payload: {
1670
+ parentMessageId,
1671
+ result: {
1672
+ error: 'Failed to create task message',
1673
+ success: false,
1674
+ taskMessageId: '',
1675
+ threadId: '',
1676
+ },
1677
+ } as TaskResultPayload,
1678
+ phase: 'task_result',
1679
+ session: {
1680
+ messageCount: state.messages.length,
1681
+ sessionId: state.operationId,
1682
+ status: 'running',
1683
+ stepCount: state.stepCount + 1,
1684
+ },
1685
+ } as AgentRuntimeContext,
1686
+ };
1687
+ }
1688
+
1689
+ const taskMessageId = taskMessageResult.id;
1690
+ log('[%s][exec_client_task] Created task message: %s', taskLogId, taskMessageId);
1691
+
1692
+ // 2. Create Thread via API first (to get threadId for operation context)
1693
+ const threadResult = await aiAgentService.createClientTaskThread({
1694
+ agentId,
1695
+ instruction: task.instruction,
1696
+ parentMessageId: taskMessageId,
1697
+ title: task.description,
1698
+ topicId,
1699
+ });
1700
+
1701
+ if (!threadResult.success) {
1702
+ log('[%s][exec_client_task] Failed to create client task thread', taskLogId);
1703
+ await context
1704
+ .get()
1705
+ .optimisticUpdateMessageContent(
1706
+ taskMessageId,
1707
+ 'Failed to create task thread',
1708
+ undefined,
1709
+ { operationId: state.operationId },
1710
+ );
1711
+ return {
1712
+ events,
1713
+ newState: state,
1714
+ nextContext: {
1715
+ payload: {
1716
+ parentMessageId,
1717
+ result: {
1718
+ error: 'Failed to create client task thread',
1719
+ success: false,
1720
+ taskMessageId,
1721
+ threadId: '',
1722
+ },
1723
+ } as TaskResultPayload,
1724
+ phase: 'task_result',
1725
+ session: {
1726
+ messageCount: state.messages.length,
1727
+ sessionId: state.operationId,
1728
+ status: 'running',
1729
+ stepCount: state.stepCount + 1,
1730
+ },
1731
+ } as AgentRuntimeContext,
1732
+ };
1733
+ }
1734
+
1735
+ const { threadId, userMessageId, threadMessages, messages } = threadResult;
1736
+
1737
+ // 3. Build sub-task ConversationContext (uses threadId for isolation)
1738
+ const subContext: ConversationContext = { agentId, topicId, threadId, scope: 'thread' };
1739
+
1740
+ // 4. Create a child operation for task execution (now with threadId)
1741
+ const { operationId: taskOperationId } = context.get().startOperation({
1742
+ type: 'execClientTask',
1743
+ context: subContext,
1744
+ parentOperationId: state.operationId,
1745
+ metadata: {
1746
+ startTime: Date.now(),
1747
+ taskDescription: task.description,
1748
+ taskMessageId,
1749
+ executionMode: 'client',
1750
+ },
1751
+ });
1752
+ log(
1753
+ '[%s][exec_client_task] Created thread: %s, userMessageId: %s, threadMessages: %d',
1754
+ taskLogId,
1755
+ threadId,
1756
+ userMessageId,
1757
+ threadMessages.length,
1758
+ );
1759
+
1760
+ // 5. Sync messages to store
1761
+ // Update main chat messages with latest taskDetail status
1762
+ context.get().replaceMessages(messages, { operationId: state.operationId });
1763
+ // Update thread messages
1764
+ context.get().replaceMessages(threadMessages, { context: subContext });
1765
+
1766
+ // 6. Use server-returned thread messages (already persisted)
1767
+ let subMessages = [...threadMessages];
1768
+
1769
+ // Optionally inherit messages from parent conversation
1770
+ if (task.inheritMessages) {
1771
+ const parentMessages = state.messages.filter((m) => m.role !== 'task');
1772
+ subMessages = [...parentMessages, ...subMessages];
1773
+ // Re-sync with inherited messages
1774
+ context.get().replaceMessages(subMessages, { context: subContext });
1775
+ }
1776
+
1777
+ // 7. Execute using internal_execAgentRuntime (client-side with local tools access)
1778
+ log('[%s][exec_client_task] Starting client-side AgentRuntime execution', taskLogId);
1779
+
1780
+ const runtimeResult = await context.get().internal_execAgentRuntime({
1781
+ context: subContext,
1782
+ messages: subMessages,
1783
+ parentMessageId: userMessageId, // Use server-returned userMessageId
1784
+ parentMessageType: 'user',
1785
+ operationId: taskOperationId,
1786
+ parentOperationId: state.operationId,
1787
+ });
1788
+
1789
+ log('[%s][exec_client_task] Client-side AgentRuntime execution completed', taskLogId);
1790
+
1791
+ // 8. Get execution result from sub-task messages
1792
+ const subMessageKey = messageMapKey(subContext);
1793
+ const subTaskMessages = context.get().dbMessagesMap[subMessageKey] || [];
1794
+ const lastAssistant = subTaskMessages.findLast((m) => m.role === 'assistant');
1795
+ const resultContent = lastAssistant?.content || 'Task completed';
1796
+
1797
+ log(
1798
+ '[%s][exec_client_task] Got result from sub-task: %d chars',
1799
+ taskLogId,
1800
+ resultContent.length,
1801
+ );
1802
+
1803
+ // Count tool calls
1804
+ const totalToolCalls = subTaskMessages.filter((m) => m.role === 'tool').length;
1805
+
1806
+ // Get usage data from runtime result
1807
+ const { usage, cost } = runtimeResult || {};
1808
+
1809
+ log(
1810
+ '[%s][exec_client_task] Runtime usage: tokens=%d, cost=%s, model=%s',
1811
+ taskLogId,
1812
+ usage?.llm?.tokens?.total,
1813
+ cost?.total,
1814
+ taskModel,
1815
+ );
1816
+
1817
+ // 9. Update task message with result and usage (model/provider already set at creation)
1818
+ await context.get().optimisticUpdateMessageContent(
1819
+ taskMessageId,
1820
+ resultContent,
1821
+ {
1822
+ metadata: {
1823
+ cost: cost?.total,
1824
+ duration: usage?.llm?.processingTimeMs,
1825
+ totalInputTokens: usage?.llm?.tokens?.input,
1826
+ totalOutputTokens: usage?.llm?.tokens?.output,
1827
+ totalTokens: usage?.llm?.tokens?.total,
1828
+ },
1829
+ },
1830
+ { operationId: state.operationId },
1831
+ );
1832
+
1833
+ // 10. Update Thread status via API with metadata
1834
+ await aiAgentService.updateClientTaskThreadStatus({
1835
+ threadId,
1836
+ completionReason: 'done',
1837
+ resultContent,
1838
+ metadata: {
1839
+ totalCost: cost?.total,
1840
+ totalMessages: subTaskMessages.length,
1841
+ totalTokens: usage?.llm?.tokens?.total,
1842
+ totalToolCalls,
1843
+ },
1844
+ });
1845
+
1846
+ // 11. Complete operation
1847
+ context.get().completeOperation(taskOperationId);
1848
+
1849
+ // 12. Return success result
1850
+ const updatedMessages = context.get().dbMessagesMap[context.messageKey] || [];
1851
+ return {
1852
+ events,
1853
+ newState: { ...state, messages: updatedMessages },
1854
+ nextContext: {
1855
+ payload: {
1856
+ // Use taskMessageId as parent so subsequent messages are created after the task
1857
+ parentMessageId: taskMessageId,
1858
+ result: {
1859
+ result: resultContent,
1860
+ success: true,
1861
+ taskMessageId,
1862
+ threadId,
1863
+ },
1864
+ } as TaskResultPayload,
1865
+ phase: 'task_result',
1866
+ session: {
1867
+ messageCount: updatedMessages.length,
1868
+ sessionId: state.operationId,
1869
+ status: 'running',
1870
+ stepCount: state.stepCount + 1,
1871
+ },
1872
+ } as AgentRuntimeContext,
1873
+ };
1874
+ } catch (error) {
1875
+ log('[%s][exec_client_task] Error executing client task: %O', taskLogId, error);
1876
+
1877
+ // Update task message with error
1878
+ // Note: taskMessageId may not exist if error occurred before message creation
1879
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
1880
+
1881
+ return {
1882
+ events,
1883
+ newState: state,
1884
+ nextContext: {
1885
+ payload: {
1886
+ parentMessageId,
1887
+ result: {
1888
+ error: errorMessage,
1889
+ success: false,
1890
+ taskMessageId: '',
1891
+ threadId: '',
1892
+ },
1893
+ } as TaskResultPayload,
1894
+ phase: 'task_result',
1895
+ session: {
1896
+ messageCount: state.messages.length,
1897
+ sessionId: state.operationId,
1898
+ status: 'running',
1899
+ stepCount: state.stepCount + 1,
1900
+ },
1901
+ } as AgentRuntimeContext,
1902
+ };
1903
+ }
1904
+ },
1905
+
1906
+ /**
1907
+ * exec_client_tasks executor
1908
+ * Executes multiple async tasks on the client side in parallel (desktop only)
1909
+ * Used when tasks require local tools like file system or shell commands
1910
+ *
1911
+ * Flow:
1912
+ * 1. For each task, create a task message (role: 'task') as placeholder
1913
+ * 2. Create Thread via API (for isolation)
1914
+ * 3. Execute using internal_execAgentRuntime (client-side)
1915
+ * 4. Update Thread status via API on completion
1916
+ * 5. Update task message content with result
1917
+ * 6. Return tasks_batch_result phase with all results
1918
+ */
1919
+ exec_client_tasks: async (instruction, state) => {
1920
+ const { parentMessageId, tasks } = (instruction as AgentInstructionExecClientTasks).payload;
1921
+
1922
+ const events: AgentEvent[] = [];
1923
+ const sessionLogId = `${state.operationId}:${state.stepCount}`;
1924
+
1925
+ log(
1926
+ '[%s][exec_client_tasks] Starting client-side execution of %d tasks',
1927
+ sessionLogId,
1928
+ tasks.length,
1929
+ );
1930
+
1931
+ // Check if we're on desktop - if not, this executor shouldn't have been called
1932
+ if (!isDesktop) {
1933
+ log(
1934
+ '[%s][exec_client_tasks] ERROR: Not on desktop, cannot execute client-side tasks',
1935
+ sessionLogId,
1936
+ );
1937
+ return {
1938
+ events,
1939
+ newState: state,
1940
+ nextContext: {
1941
+ payload: {
1942
+ parentMessageId,
1943
+ results: tasks.map(() => ({
1944
+ error: 'Client-side task execution is only available on desktop',
1945
+ success: false,
1946
+ taskMessageId: '',
1947
+ threadId: '',
1948
+ })),
1949
+ } as TasksBatchResultPayload,
1950
+ phase: 'tasks_batch_result',
1951
+ session: {
1952
+ messageCount: state.messages.length,
1953
+ sessionId: state.operationId,
1954
+ status: 'running',
1955
+ stepCount: state.stepCount + 1,
1956
+ },
1957
+ } as AgentRuntimeContext,
1958
+ };
1959
+ }
1960
+
1961
+ // Get context from operation
1962
+ const opContext = getOperationContext();
1963
+ const { agentId, topicId } = opContext;
1964
+
1965
+ if (!agentId || !topicId) {
1966
+ log('[%s][exec_client_tasks] No valid context, cannot execute tasks', sessionLogId);
1967
+ return {
1968
+ events,
1969
+ newState: state,
1970
+ nextContext: {
1971
+ payload: {
1972
+ parentMessageId,
1973
+ results: tasks.map(() => ({
1974
+ error: 'No valid context available',
1975
+ success: false,
1976
+ taskMessageId: '',
1977
+ threadId: '',
1978
+ })),
1979
+ } as TasksBatchResultPayload,
1980
+ phase: 'tasks_batch_result',
1981
+ session: {
1982
+ messageCount: state.messages.length,
1983
+ sessionId: state.operationId,
1984
+ status: 'running',
1985
+ stepCount: state.stepCount + 1,
1986
+ },
1987
+ } as AgentRuntimeContext,
1988
+ };
1989
+ }
1990
+
1991
+ // Execute all tasks in parallel
1992
+ const results = await pMap(
1993
+ tasks,
1994
+ async (task, taskIndex) => {
1995
+ const taskLogId = `${sessionLogId}:client-task-${taskIndex}`;
1996
+ log('[%s] Starting client task: %s', taskLogId, task.description);
1997
+
1998
+ try {
1999
+ // 1. Create task message as placeholder
2000
+ const taskMessageResult = await context.get().optimisticCreateMessage(
2001
+ {
2002
+ agentId,
2003
+ content: '',
2004
+ metadata: { instruction: task.instruction, taskTitle: task.description },
2005
+ parentId: parentMessageId,
2006
+ role: 'task',
2007
+ topicId,
2008
+ },
2009
+ { operationId: state.operationId },
2010
+ );
2011
+
2012
+ if (!taskMessageResult) {
2013
+ log('[%s] Failed to create task message', taskLogId);
2014
+ return {
2015
+ error: 'Failed to create task message',
2016
+ success: false,
2017
+ taskMessageId: '',
2018
+ threadId: '',
2019
+ };
2020
+ }
2021
+
2022
+ const taskMessageId = taskMessageResult.id;
2023
+ log('[%s] Created task message: %s', taskLogId, taskMessageId);
2024
+
2025
+ // 2. Create Thread via API first (to get threadId for operation context)
2026
+ const threadResult = await aiAgentService.createClientTaskThread({
2027
+ agentId,
2028
+ instruction: task.instruction,
2029
+ parentMessageId: taskMessageId,
2030
+ title: task.description,
2031
+ topicId,
2032
+ });
2033
+
2034
+ if (!threadResult.success) {
2035
+ log('[%s] Failed to create client task thread', taskLogId);
2036
+ await context
2037
+ .get()
2038
+ .optimisticUpdateMessageContent(
2039
+ taskMessageId,
2040
+ 'Failed to create task thread',
2041
+ undefined,
2042
+ { operationId: state.operationId },
2043
+ );
2044
+ return {
2045
+ error: 'Failed to create client task thread',
2046
+ success: false,
2047
+ taskMessageId,
2048
+ threadId: '',
2049
+ };
2050
+ }
2051
+
2052
+ const { threadId, userMessageId, threadMessages, messages } = threadResult;
2053
+ log(
2054
+ '[%s] Created thread: %s, userMessageId: %s, threadMessages: %d',
2055
+ taskLogId,
2056
+ threadId,
2057
+ userMessageId,
2058
+ threadMessages.length,
2059
+ );
2060
+
2061
+ // 3. Build sub-task ConversationContext (uses threadId for isolation)
2062
+ const subContext: ConversationContext = {
2063
+ agentId,
2064
+ topicId,
2065
+ threadId,
2066
+ scope: 'thread',
2067
+ };
2068
+
2069
+ // 4. Create a child operation for task execution (now with threadId)
2070
+ const { operationId: taskOperationId } = context.get().startOperation({
2071
+ type: 'execClientTask',
2072
+ context: subContext,
2073
+ parentOperationId: state.operationId,
2074
+ metadata: {
2075
+ startTime: Date.now(),
2076
+ taskDescription: task.description,
2077
+ taskIndex,
2078
+ taskMessageId,
2079
+ executionMode: 'client',
2080
+ },
2081
+ });
2082
+
2083
+ // 5. Sync messages to store
2084
+ // Update main chat messages with latest taskDetail status
2085
+ context.get().replaceMessages(messages, { operationId: state.operationId });
2086
+ // Update thread messages
2087
+ context.get().replaceMessages(threadMessages, { context: subContext });
2088
+
2089
+ // 6. Use server-returned thread messages (already persisted)
2090
+ let subMessages = [...threadMessages];
2091
+
2092
+ // Optionally inherit messages from parent conversation
2093
+ if (task.inheritMessages) {
2094
+ const parentMessages = state.messages.filter((m) => m.role !== 'task');
2095
+ subMessages = [...parentMessages, ...subMessages];
2096
+ // Re-sync with inherited messages
2097
+ context.get().replaceMessages(subMessages, { context: subContext });
2098
+ }
2099
+
2100
+ // 7. Execute using internal_execAgentRuntime (client-side with local tools access)
2101
+ log('[%s] Starting client-side AgentRuntime execution', taskLogId);
2102
+
2103
+ await context.get().internal_execAgentRuntime({
2104
+ context: subContext,
2105
+ messages: subMessages,
2106
+ parentMessageId: userMessageId, // Use server-returned userMessageId
2107
+ parentMessageType: 'user',
2108
+ operationId: taskOperationId,
2109
+ parentOperationId: state.operationId,
2110
+ });
2111
+
2112
+ log('[%s] Client-side AgentRuntime execution completed', taskLogId);
2113
+
2114
+ // 7. Get execution result from sub-task messages
2115
+ const subMessageKey = messageMapKey(subContext);
2116
+ const subTaskMessages = context.get().dbMessagesMap[subMessageKey] || [];
2117
+ const lastAssistant = subTaskMessages.findLast((m) => m.role === 'assistant');
2118
+ const resultContent = lastAssistant?.content || 'Task completed';
2119
+
2120
+ log('[%s] Got result from sub-task: %d chars', taskLogId, resultContent.length);
2121
+
2122
+ // 8. Update task message with result
2123
+ await context
2124
+ .get()
2125
+ .optimisticUpdateMessageContent(taskMessageId, resultContent, undefined, {
2126
+ operationId: state.operationId,
2127
+ });
2128
+
2129
+ // 9. Update Thread status via API
2130
+ await aiAgentService.updateClientTaskThreadStatus({
2131
+ threadId,
2132
+ completionReason: 'done',
2133
+ resultContent,
2134
+ });
2135
+
2136
+ // 10. Complete operation
2137
+ context.get().completeOperation(taskOperationId);
2138
+
2139
+ return {
2140
+ result: resultContent,
2141
+ success: true,
2142
+ taskMessageId,
2143
+ threadId,
2144
+ };
2145
+ } catch (error) {
2146
+ log('[%s] Error executing client task: %O', taskLogId, error);
2147
+ return {
2148
+ error: error instanceof Error ? error.message : 'Unknown error',
2149
+ success: false,
2150
+ taskMessageId: '',
2151
+ threadId: '',
2152
+ };
2153
+ }
2154
+ },
2155
+ { concurrency: 15 },
2156
+ );
2157
+
2158
+ log('[%s][exec_client_tasks] All tasks completed, results: %O', sessionLogId, results);
2159
+
2160
+ // Get latest messages from store
2161
+ const updatedMessages = context.get().dbMessagesMap[context.messageKey] || [];
2162
+ const newState = { ...state, messages: updatedMessages };
2163
+
2164
+ // Use the last successful task's message ID as parent for subsequent messages
2165
+ const lastSuccessfulTaskId = results.findLast((r) => r.success)?.taskMessageId;
2166
+
2167
+ return {
2168
+ events,
2169
+ newState,
2170
+ nextContext: {
2171
+ payload: {
2172
+ // Use last task message as parent so subsequent messages are created after the tasks
2173
+ parentMessageId: lastSuccessfulTaskId || parentMessageId,
2174
+ results,
2175
+ } as TasksBatchResultPayload,
2176
+ phase: 'tasks_batch_result',
2177
+ session: {
2178
+ messageCount: newState.messages.length,
2179
+ sessionId: state.operationId,
2180
+ status: 'running',
2181
+ stepCount: state.stepCount + 1,
2182
+ },
2183
+ } as AgentRuntimeContext,
2184
+ };
2185
+ },
1484
2186
  };
1485
2187
 
1486
2188
  return executors;