@lobehub/lobehub 2.0.0-next.329 → 2.0.0-next.330

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 CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## [Version 2.0.0-next.330](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.329...v2.0.0-next.330)
6
+
7
+ <sup>Released on **2026-01-21**</sup>
8
+
9
+ #### 🐛 Bug Fixes
10
+
11
+ - **misc**: Fix multi agent tasks issue.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's fixed
19
+
20
+ - **misc**: Fix multi agent tasks issue, closes [#11672](https://github.com/lobehub/lobe-chat/issues/11672) ([9de773b](https://github.com/lobehub/lobe-chat/commit/9de773b))
21
+
22
+ </details>
23
+
24
+ <div align="right">
25
+
26
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
5
30
  ## [Version 2.0.0-next.329](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.328...v2.0.0-next.329)
6
31
 
7
32
  <sup>Released on **2026-01-21**</sup>
package/changelog/v1.json CHANGED
@@ -1,4 +1,13 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "fixes": [
5
+ "Fix multi agent tasks issue."
6
+ ]
7
+ },
8
+ "date": "2026-01-21",
9
+ "version": "2.0.0-next.330"
10
+ },
2
11
  {
3
12
  "children": {
4
13
  "fixes": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/lobehub",
3
- "version": "2.0.0-next.329",
3
+ "version": "2.0.0-next.330",
4
4
  "description": "LobeHub - an open-source,comprehensive AI Agent framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -118,10 +118,11 @@ export interface ExecutorResultSupervisorDecided {
118
118
  * - 'speak': Call a single agent
119
119
  * - 'broadcast': Call multiple agents in parallel
120
120
  * - 'delegate': Delegate to another agent
121
- * - 'execute_task': Execute an async task
121
+ * - 'execute_task': Execute a single async task
122
+ * - 'execute_tasks': Execute multiple async tasks in parallel
122
123
  * - 'finish': End the orchestration
123
124
  */
124
- decision: 'speak' | 'broadcast' | 'delegate' | 'execute_task' | 'finish';
125
+ decision: 'speak' | 'broadcast' | 'delegate' | 'execute_task' | 'execute_tasks' | 'finish';
125
126
  /**
126
127
  * Parameters for the decision
127
128
  */
@@ -0,0 +1,117 @@
1
+ 'use client';
2
+
3
+ import { DEFAULT_AVATAR } from '@lobechat/const';
4
+ import type { AgentGroupMember, BuiltinRenderProps } from '@lobechat/types';
5
+ import { Avatar, Flexbox, Text } from '@lobehub/ui';
6
+ import { createStaticStyles, useTheme } from 'antd-style';
7
+ import { Clock } from 'lucide-react';
8
+ import { memo, useMemo } from 'react';
9
+ import { useTranslation } from 'react-i18next';
10
+
11
+ import { useAgentGroupStore } from '@/store/agentGroup';
12
+ import { agentGroupSelectors } from '@/store/agentGroup/selectors';
13
+
14
+ import type { ExecuteTasksParams } from '../../../types';
15
+
16
+ const styles = createStaticStyles(({ css, cssVar }) => ({
17
+ container: css`
18
+ display: flex;
19
+ flex-direction: column;
20
+ gap: 12px;
21
+ padding-block: 12px;
22
+ `,
23
+ taskCard: css`
24
+ padding: 12px;
25
+ border-radius: 8px;
26
+ background: ${cssVar.colorFillQuaternary};
27
+ `,
28
+ taskContent: css`
29
+ padding-block: 8px;
30
+ padding-inline: 12px;
31
+ border-radius: ${cssVar.borderRadius};
32
+ background: ${cssVar.colorFillTertiary};
33
+ `,
34
+ taskHeader: css`
35
+ font-size: 13px;
36
+ font-weight: 500;
37
+ color: ${cssVar.colorText};
38
+ `,
39
+ timeout: css`
40
+ font-size: 12px;
41
+ color: ${cssVar.colorTextTertiary};
42
+ `,
43
+ }));
44
+
45
+ /**
46
+ * ExecuteTasks Render component for Group Management tool
47
+ * Read-only display of multiple task execution requests
48
+ */
49
+ const ExecuteTasksRender = memo<BuiltinRenderProps<ExecuteTasksParams>>(({ args }) => {
50
+ const { t } = useTranslation('tool');
51
+ const theme = useTheme();
52
+ const { tasks } = args || {};
53
+
54
+ // Get active group ID and agents from store
55
+ const activeGroupId = useAgentGroupStore(agentGroupSelectors.activeGroupId);
56
+ const groupAgents = useAgentGroupStore((s) =>
57
+ activeGroupId ? agentGroupSelectors.getGroupAgents(activeGroupId)(s) : [],
58
+ );
59
+
60
+ // Get agent details for each task
61
+ const tasksWithAgents = useMemo(() => {
62
+ if (!tasks?.length || !groupAgents.length) return [];
63
+ return tasks.map((task) => ({
64
+ ...task,
65
+ agent: groupAgents.find((agent) => agent.id === task.agentId) as AgentGroupMember | undefined,
66
+ }));
67
+ }, [tasks, groupAgents]);
68
+
69
+ if (!tasksWithAgents.length) return null;
70
+
71
+ return (
72
+ <div className={styles.container}>
73
+ {tasksWithAgents.map((task, index) => {
74
+ const timeoutMinutes = task.timeout ? Math.round(task.timeout / 60_000) : 30;
75
+
76
+ return (
77
+ <div className={styles.taskCard} key={task.agentId || index}>
78
+ <Flexbox gap={12}>
79
+ {/* Header: Agent info + Timeout */}
80
+ <Flexbox align={'center'} gap={12} horizontal justify={'space-between'}>
81
+ <Flexbox align={'center'} flex={1} gap={8} horizontal style={{ minWidth: 0 }}>
82
+ <Avatar
83
+ avatar={task.agent?.avatar || DEFAULT_AVATAR}
84
+ background={task.agent?.backgroundColor || theme.colorBgContainer}
85
+ shape={'square'}
86
+ size={20}
87
+ />
88
+ <span className={styles.taskHeader}>
89
+ {task.title || task.agent?.title || 'Task'}
90
+ </span>
91
+ </Flexbox>
92
+ <Flexbox align="center" className={styles.timeout} gap={4} horizontal>
93
+ <Clock size={14} />
94
+ <span>
95
+ {timeoutMinutes}{' '}
96
+ {t('agentGroupManagement.executeTask.intervention.timeoutUnit')}
97
+ </span>
98
+ </Flexbox>
99
+ </Flexbox>
100
+
101
+ {/* Task content (read-only) */}
102
+ {task.instruction && (
103
+ <Text className={styles.taskContent} style={{ margin: 0 }}>
104
+ {task.instruction}
105
+ </Text>
106
+ )}
107
+ </Flexbox>
108
+ </div>
109
+ );
110
+ })}
111
+ </div>
112
+ );
113
+ });
114
+
115
+ ExecuteTasksRender.displayName = 'ExecuteTasksRender';
116
+
117
+ export default ExecuteTasksRender;
@@ -1,6 +1,7 @@
1
1
  import { GroupManagementApiName } from '../../types';
2
2
  import BroadcastRender from './Broadcast';
3
3
  import ExecuteTaskRender from './ExecuteTask';
4
+ import ExecuteTasksRender from './ExecuteTasks';
4
5
  import SpeakRender from './Speak';
5
6
 
6
7
  /**
@@ -9,9 +10,11 @@ import SpeakRender from './Speak';
9
10
  export const GroupManagementRenders = {
10
11
  [GroupManagementApiName.broadcast]: BroadcastRender,
11
12
  [GroupManagementApiName.executeAgentTask]: ExecuteTaskRender,
13
+ [GroupManagementApiName.executeAgentTasks]: ExecuteTasksRender,
12
14
  [GroupManagementApiName.speak]: SpeakRender,
13
15
  };
14
16
 
15
17
  export { default as BroadcastRender } from './Broadcast';
16
18
  export { default as ExecuteTaskRender } from './ExecuteTask';
19
+ export { default as ExecuteTasksRender } from './ExecuteTasks';
17
20
  export { default as SpeakRender } from './Speak';
@@ -75,6 +75,7 @@ describe('GroupManagementExecutor', () => {
75
75
  triggerBroadcast: vi.fn(),
76
76
  triggerDelegate: vi.fn(),
77
77
  triggerExecuteTask: vi.fn(),
78
+ triggerExecuteTasks: vi.fn(),
78
79
  triggerSpeak,
79
80
  },
80
81
  'supervisor-agent',
@@ -122,6 +123,7 @@ describe('GroupManagementExecutor', () => {
122
123
  triggerBroadcast: vi.fn(),
123
124
  triggerDelegate: vi.fn(),
124
125
  triggerExecuteTask: vi.fn(),
126
+ triggerExecuteTasks: vi.fn(),
125
127
  triggerSpeak,
126
128
  },
127
129
  'supervisor-agent',
@@ -171,6 +173,7 @@ describe('GroupManagementExecutor', () => {
171
173
  triggerBroadcast,
172
174
  triggerDelegate: vi.fn(),
173
175
  triggerExecuteTask: vi.fn(),
176
+ triggerExecuteTasks: vi.fn(),
174
177
  triggerSpeak: vi.fn(),
175
178
  },
176
179
  'supervisor-agent',
@@ -238,6 +241,7 @@ describe('GroupManagementExecutor', () => {
238
241
  triggerBroadcast: vi.fn(),
239
242
  triggerDelegate,
240
243
  triggerExecuteTask: vi.fn(),
244
+ triggerExecuteTasks: vi.fn(),
241
245
  triggerSpeak: vi.fn(),
242
246
  },
243
247
  'supervisor-agent',
@@ -308,6 +312,7 @@ describe('GroupManagementExecutor', () => {
308
312
  triggerBroadcast: vi.fn(),
309
313
  triggerDelegate: vi.fn(),
310
314
  triggerExecuteTask,
315
+ triggerExecuteTasks: vi.fn(),
311
316
  triggerSpeak: vi.fn(),
312
317
  },
313
318
  'supervisor-agent',
@@ -11,6 +11,7 @@ import {
11
11
  CreateWorkflowParams,
12
12
  DelegateParams,
13
13
  ExecuteTaskParams,
14
+ ExecuteTasksParams,
14
15
  GroupManagementApiName,
15
16
  GroupManagementIdentifier,
16
17
  InterruptParams,
@@ -153,6 +154,38 @@ class GroupManagementExecutor extends BaseExecutor<typeof GroupManagementApiName
153
154
  };
154
155
  };
155
156
 
157
+ executeAgentTasks = async (
158
+ params: ExecuteTasksParams,
159
+ ctx: BuiltinToolContext,
160
+ ): Promise<BuiltinToolResult> => {
161
+ // Register afterCompletion callback to trigger parallel task execution after AgentRuntime completes
162
+ // This follows the same pattern as executeAgentTask - trigger mode, not blocking
163
+ if (ctx.groupOrchestration && ctx.agentId && ctx.registerAfterCompletion) {
164
+ ctx.registerAfterCompletion(() =>
165
+ ctx.groupOrchestration!.triggerExecuteTasks({
166
+ skipCallSupervisor: params.skipCallSupervisor,
167
+ supervisorAgentId: ctx.agentId!,
168
+ tasks: params.tasks,
169
+ toolMessageId: ctx.messageId,
170
+ }),
171
+ );
172
+ }
173
+
174
+ const agentIds = params.tasks.map((t) => t.agentId).join(', ');
175
+
176
+ // Returns stop: true to indicate the supervisor should stop and let the tasks execute
177
+ return {
178
+ content: `Triggered ${params.tasks.length} parallel tasks for agents: ${agentIds}.`,
179
+ state: {
180
+ skipCallSupervisor: params.skipCallSupervisor,
181
+ tasks: params.tasks,
182
+ type: 'executeAgentTasks',
183
+ },
184
+ stop: true,
185
+ success: true,
186
+ };
187
+ };
188
+
156
189
  interrupt = async (
157
190
  params: InterruptParams,
158
191
  _ctx: BuiltinToolContext,
@@ -121,52 +121,53 @@ export const GroupManagementManifest: BuiltinToolManifest = {
121
121
  type: 'object',
122
122
  },
123
123
  },
124
- {
125
- description:
126
- 'Assign multiple tasks to different agents to run in parallel. Each agent works independently in their own context. Use this when you need multiple agents to work on different parts of a problem simultaneously.',
127
- name: GroupManagementApiName.executeAgentTasks,
128
- humanIntervention: 'required',
129
- parameters: {
130
- properties: {
131
- tasks: {
132
- description: 'Array of tasks, each assigned to a specific agent.',
133
- items: {
134
- properties: {
135
- agentId: {
136
- description: 'The ID of the agent to execute this task.',
137
- type: 'string',
138
- },
139
- title: {
140
- description: 'Brief title describing what this task does (shown in UI).',
141
- type: 'string',
142
- },
143
- instruction: {
144
- description:
145
- 'Detailed instruction/prompt for the task execution. Be specific about expected deliverables.',
146
- type: 'string',
147
- },
148
- timeout: {
149
- description:
150
- 'Optional timeout in milliseconds for this task (default: 1800000, 30 minutes).',
151
- type: 'number',
152
- },
153
- },
154
- required: ['agentId', 'title', 'instruction'],
155
- type: 'object',
156
- },
157
- type: 'array',
158
- },
159
- skipCallSupervisor: {
160
- default: false,
161
- description:
162
- 'If true, the orchestration will end after all tasks complete, without calling the supervisor again.',
163
- type: 'boolean',
164
- },
165
- },
166
- required: ['tasks'],
167
- type: 'object',
168
- },
169
- },
124
+ // TODO: Enable executeAgentTasks when ready
125
+ // {
126
+ // description:
127
+ // 'Assign multiple tasks to different agents to run in parallel. Each agent works independently in their own context. Use this when you need multiple agents to work on different parts of a problem simultaneously.',
128
+ // name: GroupManagementApiName.executeAgentTasks,
129
+ // humanIntervention: 'required',
130
+ // parameters: {
131
+ // properties: {
132
+ // tasks: {
133
+ // description: 'Array of tasks, each assigned to a specific agent.',
134
+ // items: {
135
+ // properties: {
136
+ // agentId: {
137
+ // description: 'The ID of the agent to execute this task.',
138
+ // type: 'string',
139
+ // },
140
+ // title: {
141
+ // description: 'Brief title describing what this task does (shown in UI).',
142
+ // type: 'string',
143
+ // },
144
+ // instruction: {
145
+ // description:
146
+ // 'Detailed instruction/prompt for the task execution. Be specific about expected deliverables.',
147
+ // type: 'string',
148
+ // },
149
+ // timeout: {
150
+ // description:
151
+ // 'Optional timeout in milliseconds for this task (default: 1800000, 30 minutes).',
152
+ // type: 'number',
153
+ // },
154
+ // },
155
+ // required: ['agentId', 'title', 'instruction'],
156
+ // type: 'object',
157
+ // },
158
+ // type: 'array',
159
+ // },
160
+ // skipCallSupervisor: {
161
+ // default: false,
162
+ // description:
163
+ // 'If true, the orchestration will end after all tasks complete, without calling the supervisor again.',
164
+ // type: 'boolean',
165
+ // },
166
+ // },
167
+ // required: ['tasks'],
168
+ // type: 'object',
169
+ // },
170
+ // },
170
171
  {
171
172
  description:
172
173
  'Interrupt a running agent task. Use this to stop a task that is taking too long or is no longer needed.',
@@ -155,8 +155,7 @@ When a user's request is broad or unclear, ask 1-2 focused questions to understa
155
155
  - **broadcast**: Multiple agents respond in parallel in group context
156
156
 
157
157
  **Task Execution (Independent Context, With Tools):**
158
- - **executeAgentTask**: Assign a single task to one agent in isolated context
159
- - **executeAgentTasks**: Assign multiple tasks to different agents in parallel (each with isolated context)
158
+ - **executeAgentTask**: Assign a task to one agent in isolated context
160
159
  - **interrupt**: Stop a running task
161
160
 
162
161
  **Flow Control:**
@@ -176,19 +175,17 @@ Analysis: Opinion-based, no tools needed
176
175
  Action: broadcast to [Architect, DevOps, Backend] - share perspectives
177
176
  \`\`\`
178
177
 
179
- ### Pattern 2: Independent Research (Parallel Tasks)
180
- When multiple agents need to research/work independently using their tools.
178
+ ### Pattern 2: Independent Research (Task)
179
+ When an agent needs to research/work independently using their tools.
181
180
 
182
181
  \`\`\`
183
- User: "Research the pros and cons of React vs Vue vs Svelte"
184
- Analysis: Requires web search, agents work independently
185
- Action: executeAgentTasks with parallel assignments
186
- executeAgentTasks({
187
- tasks: [
188
- { agentId: "frontend-expert", title: "Research React", instruction: "Research React ecosystem, performance benchmarks, community size, and typical use cases. Provide pros and cons." },
189
- { agentId: "ui-specialist", title: "Research Vue", instruction: "Research Vue ecosystem, performance benchmarks, community size, and typical use cases. Provide pros and cons." },
190
- { agentId: "tech-analyst", title: "Research Svelte", instruction: "Research Svelte ecosystem, performance benchmarks, community size, and typical use cases. Provide pros and cons." }
191
- ]
182
+ User: "Research the pros and cons of React"
183
+ Analysis: Requires web search, agent works independently
184
+ Action: executeAgentTask to frontend expert
185
+ executeAgentTask({
186
+ agentId: "frontend-expert",
187
+ title: "Research React",
188
+ task: "Research React ecosystem, performance benchmarks, community size, and typical use cases. Provide pros and cons."
192
189
  })
193
190
  \`\`\`
194
191
 
@@ -211,28 +208,25 @@ When you need facts first, then discussion.
211
208
  User: "Should we migrate to Kubernetes? Research and discuss."
212
209
  Analysis: First gather facts (tools), then discuss (no tools)
213
210
  Action:
214
- 1. executeAgentTasks({
215
- tasks: [
216
- { agentId: "devops", title: "K8s Adoption Research", instruction: "Research Kubernetes adoption best practices for our scale. Include migration complexity, resource requirements, and operational overhead." },
217
- { agentId: "security", title: "K8s Security Analysis", instruction: "Research Kubernetes security considerations including network policies, RBAC, secrets management, and common vulnerabilities." }
218
- ]
211
+ 1. executeAgentTask({
212
+ agentId: "devops",
213
+ title: "K8s Adoption Research",
214
+ task: "Research Kubernetes adoption best practices for our scale. Include migration complexity, resource requirements, operational overhead, and security considerations."
219
215
  })
220
216
  2. [Wait for results]
221
217
  3. broadcast: "Based on the research, share your recommendations"
222
218
  \`\`\`
223
219
 
224
- ### Pattern 5: Collaborative Implementation (Parallel Tasks)
225
- When multiple agents create deliverables using their tools.
220
+ ### Pattern 5: Implementation Task
221
+ When an agent needs to create deliverables using their tools.
226
222
 
227
223
  \`\`\`
228
- User: "Create a landing page - need copy, design specs, and code"
229
- Analysis: Each agent produces artifacts using their tools
230
- Action: executeAgentTasks({
231
- tasks: [
232
- { agentId: "copywriter", title: "Write Copy", instruction: "Write compelling landing page copy for [product]. Include headline, subheadline, feature descriptions, and CTA text." },
233
- { agentId: "designer", title: "Design Specs", instruction: "Create design specifications including color palette, typography, layout grid, and component list with visual hierarchy." },
234
- { agentId: "frontend-dev", title: "Implement Page", instruction: "Implement the landing page using React. Include responsive design, animations, and SEO-friendly markup." }
235
- ]
224
+ User: "Write the landing page copy"
225
+ Analysis: Agent produces artifacts using their tools
226
+ Action: executeAgentTask({
227
+ agentId: "copywriter",
228
+ title: "Write Copy",
229
+ task: "Write compelling landing page copy for [product]. Include headline, subheadline, feature descriptions, and CTA text."
236
230
  })
237
231
  \`\`\`
238
232
  </workflow_patterns>
@@ -243,8 +237,7 @@ Action: executeAgentTasks({
243
237
  - broadcast: \`agentIds\` (array), \`instruction\` (optional shared guidance)
244
238
 
245
239
  **Task Execution:**
246
- - executeAgentTask: \`agentId\`, \`task\` (clear deliverable description), \`timeout\` (optional, default 30min)
247
- - executeAgentTasks: \`tasks\` (array of {agentId, title, instruction, timeout?}) - **Use this for parallel task execution across multiple agents**
240
+ - executeAgentTask: \`agentId\`, \`title\`, \`task\` (clear deliverable description), \`timeout\` (optional, default 30min)
248
241
  - interrupt: \`taskId\`
249
242
 
250
243
  **Flow Control:**
@@ -452,6 +452,47 @@ export interface TriggerExecuteTaskParams extends GroupOrchestrationBaseParams {
452
452
  toolMessageId: string;
453
453
  }
454
454
 
455
+ /**
456
+ * Task item for triggerExecuteTasks callback
457
+ */
458
+ export interface TriggerExecuteTaskItem {
459
+ /**
460
+ * The agent ID to execute this task
461
+ */
462
+ agentId: string;
463
+ /**
464
+ * Detailed instruction/prompt for the task execution
465
+ */
466
+ instruction: string;
467
+ /**
468
+ * Optional timeout in milliseconds for this specific task
469
+ */
470
+ timeout?: number;
471
+ /**
472
+ * Brief title describing what this task does (shown in UI)
473
+ */
474
+ title: string;
475
+ }
476
+
477
+ /**
478
+ * Params for triggerExecuteTasks callback (multiple tasks)
479
+ */
480
+ export interface TriggerExecuteTasksParams extends GroupOrchestrationBaseParams {
481
+ /**
482
+ * If true, the orchestration will end after all tasks complete,
483
+ * without calling the supervisor again.
484
+ */
485
+ skipCallSupervisor?: boolean;
486
+ /**
487
+ * Array of tasks to execute, each assigned to a specific agent
488
+ */
489
+ tasks: TriggerExecuteTaskItem[];
490
+ /**
491
+ * The tool message ID that triggered the tasks
492
+ */
493
+ toolMessageId: string;
494
+ }
495
+
455
496
  /**
456
497
  * Group Orchestration callbacks for group management tools
457
498
  * These callbacks are used to trigger the next phase in multi-agent orchestration
@@ -472,6 +513,11 @@ export interface GroupOrchestrationCallbacks {
472
513
  */
473
514
  triggerExecuteTask: (params: TriggerExecuteTaskParams) => Promise<void>;
474
515
 
516
+ /**
517
+ * Trigger async execution of multiple tasks in parallel
518
+ */
519
+ triggerExecuteTasks: (params: TriggerExecuteTasksParams) => Promise<void>;
520
+
475
521
  /**
476
522
  * Trigger speak to a specific agent
477
523
  */
@@ -0,0 +1,115 @@
1
+ 'use client';
2
+
3
+ import { AccordionItem, Block, Text } from '@lobehub/ui';
4
+ import { memo, useMemo, useState } from 'react';
5
+
6
+ import { ThreadStatus } from '@/types/index';
7
+ import type { UIChatMessage } from '@/types/index';
8
+
9
+ import {
10
+ CompletedState,
11
+ ErrorState,
12
+ InitializingState,
13
+ ProcessingState,
14
+ isProcessingStatus,
15
+ } from '../shared';
16
+ import TaskTitle, { type TaskMetrics } from './TaskTitle';
17
+ import { useClientTaskStats } from './useClientTaskStats';
18
+
19
+ interface ClientTaskItemProps {
20
+ item: UIChatMessage;
21
+ }
22
+
23
+ const ClientTaskItem = memo<ClientTaskItemProps>(({ item }) => {
24
+ const { id, content, metadata, taskDetail } = item;
25
+ const [expanded, setExpanded] = useState(false);
26
+
27
+ const title = taskDetail?.title || metadata?.taskTitle;
28
+ const instruction = metadata?.instruction;
29
+ const status = taskDetail?.status;
30
+
31
+ const isProcessing = isProcessingStatus(status);
32
+ const isCompleted = status === ThreadStatus.Completed;
33
+ const isError = status === ThreadStatus.Failed || status === ThreadStatus.Cancel;
34
+ const isInitializing = !taskDetail || !status;
35
+
36
+ // Fetch client task stats when processing
37
+ const clientStats = useClientTaskStats({
38
+ enabled: isProcessing,
39
+ threadId: taskDetail?.threadId,
40
+ });
41
+
42
+ // Build metrics for TaskTitle
43
+ const metrics: TaskMetrics | undefined = useMemo(() => {
44
+ if (isProcessing) {
45
+ return {
46
+ isLoading: clientStats.isLoading,
47
+ startTime: clientStats.startTime,
48
+ steps: clientStats.steps,
49
+ toolCalls: clientStats.toolCalls,
50
+ };
51
+ }
52
+ if (isCompleted || isError) {
53
+ return {
54
+ duration: taskDetail?.duration,
55
+ steps: taskDetail?.totalSteps,
56
+ toolCalls: taskDetail?.totalToolCalls,
57
+ };
58
+ }
59
+ return undefined;
60
+ }, [
61
+ isProcessing,
62
+ isCompleted,
63
+ isError,
64
+ clientStats,
65
+ taskDetail?.duration,
66
+ taskDetail?.totalSteps,
67
+ taskDetail?.totalToolCalls,
68
+ ]);
69
+
70
+ return (
71
+ <AccordionItem
72
+ expand={expanded}
73
+ itemKey={id}
74
+ onExpandChange={setExpanded}
75
+ paddingBlock={4}
76
+ paddingInline={4}
77
+ title={<TaskTitle metrics={metrics} status={status} title={title} />}
78
+ >
79
+ <Block gap={16} padding={12} style={{ marginBlock: 8 }} variant={'outlined'}>
80
+ {instruction && (
81
+ <Block padding={12}>
82
+ <Text fontSize={13} type={'secondary'}>
83
+ {instruction}
84
+ </Text>
85
+ </Block>
86
+ )}
87
+
88
+ {/* Initializing State - no taskDetail yet */}
89
+ {isInitializing && <InitializingState />}
90
+
91
+ {/* Processing State */}
92
+ {!isInitializing && isProcessing && taskDetail && (
93
+ <ProcessingState messageId={id} taskDetail={taskDetail} variant="compact" />
94
+ )}
95
+
96
+ {/* Error State */}
97
+ {!isInitializing && isError && taskDetail && <ErrorState taskDetail={taskDetail} />}
98
+
99
+ {/* Completed State */}
100
+ {!isInitializing && isCompleted && taskDetail && (
101
+ <CompletedState
102
+ content={content}
103
+ expanded={expanded}
104
+ taskDetail={taskDetail}
105
+ variant="compact"
106
+ />
107
+ )}
108
+ </Block>
109
+ </AccordionItem>
110
+ );
111
+ }, Object.is);
112
+
113
+ ClientTaskItem.displayName = 'ClientTaskItem';
114
+
115
+ export default ClientTaskItem;
@@ -0,0 +1,92 @@
1
+ 'use client';
2
+
3
+ import { AccordionItem, Block, Text } from '@lobehub/ui';
4
+ import { memo, useMemo, useState } from 'react';
5
+
6
+ import { ThreadStatus } from '@/types/index';
7
+ import type { UIChatMessage } from '@/types/index';
8
+
9
+ import {
10
+ CompletedState,
11
+ ErrorState,
12
+ InitializingState,
13
+ ProcessingState,
14
+ isProcessingStatus,
15
+ } from '../shared';
16
+ import TaskTitle, { type TaskMetrics } from './TaskTitle';
17
+
18
+ interface ServerTaskItemProps {
19
+ item: UIChatMessage;
20
+ }
21
+
22
+ const ServerTaskItem = memo<ServerTaskItemProps>(({ item }) => {
23
+ const { id, content, metadata, taskDetail } = item;
24
+ const [expanded, setExpanded] = useState(false);
25
+
26
+ const title = taskDetail?.title || metadata?.taskTitle;
27
+ const instruction = metadata?.instruction;
28
+ const status = taskDetail?.status;
29
+
30
+ const isProcessing = isProcessingStatus(status);
31
+ const isCompleted = status === ThreadStatus.Completed;
32
+ const isError = status === ThreadStatus.Failed || status === ThreadStatus.Cancel;
33
+ const isInitializing = !taskDetail || !status;
34
+
35
+ // Build metrics for TaskTitle (only for completed/error states)
36
+ const metrics: TaskMetrics | undefined = useMemo(() => {
37
+ if (isCompleted || isError) {
38
+ return {
39
+ duration: taskDetail?.duration,
40
+ steps: taskDetail?.totalSteps,
41
+ toolCalls: taskDetail?.totalToolCalls,
42
+ };
43
+ }
44
+ return undefined;
45
+ }, [isCompleted, isError, taskDetail?.duration, taskDetail?.totalSteps, taskDetail?.totalToolCalls]);
46
+
47
+ return (
48
+ <AccordionItem
49
+ expand={expanded}
50
+ itemKey={id}
51
+ onExpandChange={setExpanded}
52
+ paddingBlock={4}
53
+ paddingInline={4}
54
+ title={<TaskTitle metrics={metrics} status={status} title={title} />}
55
+ >
56
+ <Block gap={16} padding={12} style={{ marginBlock: 8 }} variant={'outlined'}>
57
+ {instruction && (
58
+ <Block padding={12}>
59
+ <Text fontSize={13} type={'secondary'}>
60
+ {instruction}
61
+ </Text>
62
+ </Block>
63
+ )}
64
+
65
+ {/* Initializing State - no taskDetail yet */}
66
+ {isInitializing && <InitializingState />}
67
+
68
+ {/* Processing State */}
69
+ {!isInitializing && isProcessing && taskDetail && (
70
+ <ProcessingState messageId={id} taskDetail={taskDetail} variant="compact" />
71
+ )}
72
+
73
+ {/* Error State */}
74
+ {!isInitializing && isError && taskDetail && <ErrorState taskDetail={taskDetail} />}
75
+
76
+ {/* Completed State */}
77
+ {!isInitializing && isCompleted && taskDetail && (
78
+ <CompletedState
79
+ content={content}
80
+ expanded={expanded}
81
+ taskDetail={taskDetail}
82
+ variant="compact"
83
+ />
84
+ )}
85
+ </Block>
86
+ </AccordionItem>
87
+ );
88
+ }, Object.is);
89
+
90
+ ServerTaskItem.displayName = 'ServerTaskItem';
91
+
92
+ export default ServerTaskItem;
@@ -2,15 +2,31 @@
2
2
 
3
3
  import { Block, Flexbox, Icon, Text } from '@lobehub/ui';
4
4
  import { cssVar } from 'antd-style';
5
- import { ListChecksIcon, XIcon } from 'lucide-react';
6
- import { memo } from 'react';
5
+ import { Footprints, ListChecksIcon, Wrench, XIcon } from 'lucide-react';
6
+ import { memo, useEffect, useState } from 'react';
7
+ import { useTranslation } from 'react-i18next';
7
8
 
8
9
  import NeuralNetworkLoading from '@/components/NeuralNetworkLoading';
9
10
  import { ThreadStatus } from '@/types/index';
10
11
 
11
- import { isProcessingStatus } from '../shared';
12
+ import { formatDuration, formatElapsedTime, isProcessingStatus } from '../shared';
13
+
14
+ export interface TaskMetrics {
15
+ /** Task duration in milliseconds (for completed tasks) */
16
+ duration?: number;
17
+ /** Whether metrics are still loading */
18
+ isLoading?: boolean;
19
+ /** Start time timestamp for elapsed time calculation */
20
+ startTime?: number;
21
+ /** Number of execution steps/blocks */
22
+ steps?: number;
23
+ /** Total tool calls count */
24
+ toolCalls?: number;
25
+ }
12
26
 
13
27
  interface TaskTitleProps {
28
+ /** Metrics to display (steps, tool calls, elapsed time) */
29
+ metrics?: TaskMetrics;
14
30
  status?: ThreadStatus;
15
31
  title?: string;
16
32
  }
@@ -54,13 +70,90 @@ const TaskStatusIndicator = memo<{ status?: ThreadStatus }>(({ status }) => {
54
70
 
55
71
  TaskStatusIndicator.displayName = 'TaskStatusIndicator';
56
72
 
57
- const TaskTitle = memo<TaskTitleProps>(({ title, status }) => {
73
+ interface MetricsDisplayProps {
74
+ metrics: TaskMetrics;
75
+ status?: ThreadStatus;
76
+ }
77
+
78
+ const MetricsDisplay = memo<MetricsDisplayProps>(({ metrics, status }) => {
79
+ const { t } = useTranslation('chat');
80
+ const { steps, toolCalls, startTime, duration, isLoading } = metrics;
81
+ const [elapsedTime, setElapsedTime] = useState(0);
82
+
83
+ const isProcessing = status ? isProcessingStatus(status) : false;
84
+
85
+ // Calculate initial elapsed time
86
+ useEffect(() => {
87
+ if (startTime && isProcessing) {
88
+ setElapsedTime(Math.max(0, Date.now() - startTime));
89
+ }
90
+ }, [startTime, isProcessing]);
91
+
92
+ // Timer for updating elapsed time every second (only when processing)
93
+ useEffect(() => {
94
+ if (!startTime || !isProcessing) return;
95
+
96
+ const timer = setInterval(() => {
97
+ setElapsedTime(Math.max(0, Date.now() - startTime));
98
+ }, 1000);
99
+
100
+ return () => clearInterval(timer);
101
+ }, [startTime, isProcessing]);
102
+
103
+ // Don't show metrics if loading or no data
104
+ if (isLoading) return null;
105
+
106
+ const hasSteps = steps !== undefined && steps > 0;
107
+ const hasToolCalls = toolCalls !== undefined && toolCalls > 0;
108
+ const hasTime = isProcessing ? startTime !== undefined : duration !== undefined;
109
+
110
+ // Don't render if no metrics to show
111
+ if (!hasSteps && !hasToolCalls && !hasTime) return null;
112
+
113
+ return (
114
+ <Flexbox align="center" gap={8} horizontal>
115
+ {/* Steps */}
116
+ {hasSteps && (
117
+ <Flexbox align="center" gap={2} horizontal>
118
+ <Icon color={cssVar.colorTextTertiary} icon={Footprints} size={12} />
119
+ <Text fontSize={12} type="secondary">
120
+ {steps}
121
+ </Text>
122
+ </Flexbox>
123
+ )}
124
+ {/* Tool calls */}
125
+ {hasToolCalls && (
126
+ <Flexbox align="center" gap={2} horizontal>
127
+ <Icon color={cssVar.colorTextTertiary} icon={Wrench} size={12} />
128
+ <Text fontSize={12} type="secondary">
129
+ {toolCalls}
130
+ </Text>
131
+ </Flexbox>
132
+ )}
133
+ {/* Time */}
134
+ {hasTime && (
135
+ <Text fontSize={12} type="secondary">
136
+ {isProcessing
137
+ ? formatElapsedTime(elapsedTime)
138
+ : duration
139
+ ? t('task.metrics.duration', { duration: formatDuration(duration) })
140
+ : null}
141
+ </Text>
142
+ )}
143
+ </Flexbox>
144
+ );
145
+ });
146
+
147
+ MetricsDisplay.displayName = 'MetricsDisplay';
148
+
149
+ const TaskTitle = memo<TaskTitleProps>(({ title, status, metrics }) => {
58
150
  return (
59
- <Flexbox align={'center'} gap={6} horizontal>
151
+ <Flexbox align="center" gap={6} horizontal>
60
152
  <TaskStatusIndicator status={status} />
61
153
  <Text ellipsis fontSize={14}>
62
154
  {title}
63
155
  </Text>
156
+ {metrics && <MetricsDisplay metrics={metrics} status={status} />}
64
157
  </Flexbox>
65
158
  );
66
159
  });
@@ -1,81 +1,26 @@
1
1
  'use client';
2
2
 
3
- import { AccordionItem, Block, Text } from '@lobehub/ui';
4
- import { memo, useState } from 'react';
3
+ import isEqual from 'fast-deep-equal';
4
+ import { memo } from 'react';
5
5
 
6
- import { ThreadStatus } from '@/types/index';
7
6
  import type { UIChatMessage } from '@/types/index';
8
7
 
9
- import {
10
- CompletedState,
11
- ErrorState,
12
- InitializingState,
13
- ProcessingState,
14
- isProcessingStatus,
15
- } from '../shared';
16
- import TaskTitle from './TaskTitle';
8
+ import ClientTaskItem from './ClientTaskItem';
9
+ import ServerTaskItem from './ServerTaskItem';
17
10
 
18
11
  interface TaskItemProps {
19
12
  item: UIChatMessage;
20
13
  }
21
14
 
22
15
  const TaskItem = memo<TaskItemProps>(({ item }) => {
23
- const { id, content, metadata, taskDetail } = item;
24
- const [expanded, setExpanded] = useState(false);
16
+ const isClientMode = item.taskDetail?.clientMode;
25
17
 
26
- const title = taskDetail?.title || metadata?.taskTitle;
27
- const instruction = metadata?.instruction;
28
- const status = taskDetail?.status;
18
+ if (isClientMode) {
19
+ return <ClientTaskItem item={item} />;
20
+ }
29
21
 
30
- // Check if task is processing using shared utility
31
- const isProcessing = isProcessingStatus(status);
32
-
33
- const isCompleted = status === ThreadStatus.Completed;
34
- const isError = status === ThreadStatus.Failed || status === ThreadStatus.Cancel;
35
- const isInitializing = !taskDetail || !status;
36
-
37
- return (
38
- <AccordionItem
39
- expand={expanded}
40
- itemKey={id}
41
- onExpandChange={setExpanded}
42
- paddingBlock={4}
43
- paddingInline={4}
44
- title={<TaskTitle status={status} title={title} />}
45
- >
46
- <Block gap={16} padding={12} style={{ marginBlock: 8 }} variant={'outlined'}>
47
- {instruction && (
48
- <Block padding={12}>
49
- <Text fontSize={13} type={'secondary'}>
50
- {instruction}
51
- </Text>
52
- </Block>
53
- )}
54
-
55
- {/* Initializing State - no taskDetail yet */}
56
- {isInitializing && <InitializingState />}
57
-
58
- {/* Processing State */}
59
- {!isInitializing && isProcessing && taskDetail && (
60
- <ProcessingState messageId={id} taskDetail={taskDetail} variant="compact" />
61
- )}
62
-
63
- {/* Error State */}
64
- {!isInitializing && isError && taskDetail && <ErrorState taskDetail={taskDetail} />}
65
-
66
- {/* Completed State */}
67
- {!isInitializing && isCompleted && taskDetail && (
68
- <CompletedState
69
- content={content}
70
- expanded={expanded}
71
- taskDetail={taskDetail}
72
- variant="compact"
73
- />
74
- )}
75
- </Block>
76
- </AccordionItem>
77
- );
78
- }, Object.is);
22
+ return <ServerTaskItem item={item} />;
23
+ }, isEqual);
79
24
 
80
25
  TaskItem.displayName = 'TaskItem';
81
26
 
@@ -0,0 +1,82 @@
1
+ 'use client';
2
+
3
+ import { useMemo } from 'react';
4
+
5
+ import { useChatStore } from '@/store/chat';
6
+ import { displayMessageSelectors } from '@/store/chat/selectors';
7
+ import { messageMapKey } from '@/store/chat/utils/messageMapKey';
8
+
9
+ export interface ClientTaskStats {
10
+ isLoading: boolean;
11
+ startTime?: number;
12
+ steps: number;
13
+ toolCalls: number;
14
+ }
15
+
16
+ interface UseClientTaskStatsOptions {
17
+ enabled?: boolean;
18
+ threadId?: string;
19
+ }
20
+
21
+ /**
22
+ * Hook to fetch thread messages and compute task statistics for client mode tasks.
23
+ * Used in TaskItem to display progress metrics (steps, tool calls, elapsed time).
24
+ */
25
+ export const useClientTaskStats = ({
26
+ threadId,
27
+ enabled = true,
28
+ }: UseClientTaskStatsOptions): ClientTaskStats => {
29
+ const [activeAgentId, activeTopicId, useFetchMessages] = useChatStore((s) => [
30
+ s.activeAgentId,
31
+ s.activeTopicId,
32
+ s.useFetchMessages,
33
+ ]);
34
+
35
+ const threadContext = useMemo(
36
+ () => ({
37
+ agentId: activeAgentId,
38
+ scope: 'thread' as const,
39
+ threadId,
40
+ topicId: activeTopicId,
41
+ }),
42
+ [activeAgentId, activeTopicId, threadId],
43
+ );
44
+
45
+ const threadMessageKey = useMemo(
46
+ () => (threadId ? messageMapKey(threadContext) : null),
47
+ [threadId, threadContext],
48
+ );
49
+
50
+ // Fetch thread messages (skip when disabled or no threadId)
51
+ useFetchMessages(threadContext, !enabled || !threadId);
52
+
53
+ // Get thread messages from store using selector
54
+ const threadMessages = useChatStore((s) =>
55
+ threadMessageKey
56
+ ? displayMessageSelectors.getDisplayMessagesByKey(threadMessageKey)(s)
57
+ : undefined,
58
+ );
59
+
60
+ // Compute stats from thread messages
61
+ return useMemo(() => {
62
+ if (!threadMessages || !enabled) {
63
+ return { isLoading: true, steps: 0, toolCalls: 0 };
64
+ }
65
+
66
+ // Find the assistantGroup message which contains the children blocks
67
+ const assistantGroupMessage = threadMessages.find((item) => item.role === 'assistantGroup');
68
+ const blocks = assistantGroupMessage?.children ?? [];
69
+
70
+ // Calculate stats
71
+ const steps = blocks.length;
72
+ const toolCalls = blocks.reduce((sum, block) => sum + (block.tools?.length || 0), 0);
73
+ const startTime = assistantGroupMessage?.createdAt;
74
+
75
+ return {
76
+ isLoading: false,
77
+ startTime,
78
+ steps,
79
+ toolCalls,
80
+ };
81
+ }, [threadMessages, enabled]);
82
+ };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Preload Tool Render components to avoid Suspense flash on first expand
3
+ *
4
+ * These components are dynamically imported in Tool/Tool/index.tsx.
5
+ * By preloading them when tool calls are detected, we can avoid
6
+ * the loading skeleton flash when user first expands the tool result.
7
+ */
8
+
9
+ let preloaded = false;
10
+
11
+ export const preloadToolRenderComponents = () => {
12
+ if (preloaded) return;
13
+ preloaded = true;
14
+
15
+ // Preload Detail and Debug components (dynamic imports in Tool/Tool/index.tsx)
16
+ import('../AssistantGroup/Tool/Detail');
17
+ import('../AssistantGroup/Tool/Debug');
18
+ };
@@ -73,6 +73,12 @@ export interface GroupOrchestrationAction {
73
73
  */
74
74
  triggerExecuteTask: GroupOrchestrationCallbacks['triggerExecuteTask'];
75
75
 
76
+ /**
77
+ * Trigger execute tasks - called by executeTasks tool when supervisor decides to execute multiple async tasks in parallel
78
+ * This starts the group orchestration loop with supervisor_decided result
79
+ */
80
+ triggerExecuteTasks: GroupOrchestrationCallbacks['triggerExecuteTasks'];
81
+
76
82
  /**
77
83
  * Enable polling for task status
78
84
  * Used by ProcessingState component to poll for real-time task updates
@@ -240,6 +246,42 @@ export const groupOrchestrationSlice: StateCreator<
240
246
  });
241
247
  },
242
248
 
249
+ /**
250
+ * Trigger execute tasks - Entry point when supervisor calls executeTasks tool
251
+ * Creates a supervisor_decided result with decision='execute_tasks' and starts orchestration
252
+ */
253
+ triggerExecuteTasks: async (params) => {
254
+ const { supervisorAgentId, tasks, toolMessageId, skipCallSupervisor } = params;
255
+ log(
256
+ '[triggerExecuteTasks] Starting orchestration with execute_tasks: supervisorAgentId=%s, tasks=%d, toolMessageId=%s, skipCallSupervisor=%s',
257
+ supervisorAgentId,
258
+ tasks.length,
259
+ toolMessageId,
260
+ skipCallSupervisor,
261
+ );
262
+
263
+ const groupId = get().activeGroupId;
264
+ if (!groupId) {
265
+ log('[triggerExecuteTasks] No active group, skipping');
266
+ return;
267
+ }
268
+
269
+ // Start orchestration loop with supervisor_decided result (decision=execute_tasks)
270
+ await get().internal_execGroupOrchestration({
271
+ groupId,
272
+ supervisorAgentId,
273
+ topicId: get().activeTopicId,
274
+ initialResult: {
275
+ type: 'supervisor_decided',
276
+ payload: {
277
+ decision: 'execute_tasks',
278
+ params: { tasks, toolMessageId },
279
+ skipCallSupervisor: skipCallSupervisor ?? false,
280
+ },
281
+ },
282
+ });
283
+ },
284
+
243
285
  /**
244
286
  * Get group orchestration callbacks
245
287
  * These are the action methods that tools can call to trigger orchestration
@@ -250,6 +292,7 @@ export const groupOrchestrationSlice: StateCreator<
250
292
  triggerBroadcast: get().triggerBroadcast,
251
293
  triggerDelegate: get().triggerDelegate,
252
294
  triggerExecuteTask: get().triggerExecuteTask,
295
+ triggerExecuteTasks: get().triggerExecuteTasks,
253
296
  };
254
297
  },
255
298
 
@@ -736,8 +736,13 @@ export const streamingExecutor: StateCreator<
736
736
  // After parallel tool batch completes, refresh messages to ensure all tool results are synced
737
737
  // This fixes the race condition where each tool's replaceMessages may overwrite others
738
738
  // REMEMBER: There is no test for it (too hard to add), if you want to change it , ask @arvinxx first
739
- if (result.nextContext?.phase === 'tools_batch_result') {
740
- log('[internal_execAgentRuntime] Tools batch completed, refreshing messages to sync state');
739
+ if (
740
+ result.nextContext?.phase &&
741
+ ['tasks_batch_result', 'tools_batch_result'].includes(result.nextContext?.phase)
742
+ ) {
743
+ log(
744
+ `[internal_execAgentRuntime] ${result.nextContext?.phase} completed, refreshing messages to sync state`,
745
+ );
741
746
  await get().refreshMessages(context);
742
747
  }
743
748
 
@@ -16,6 +16,8 @@ export type {
16
16
  IBuiltinToolExecutor,
17
17
  TriggerBroadcastParams,
18
18
  TriggerDelegateParams,
19
+ TriggerExecuteTaskItem,
20
+ TriggerExecuteTasksParams,
19
21
  TriggerSpeakParams,
20
22
  } from '@lobechat/types';
21
23