mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-26 13:19:34 +07:00
🐛 fix: fix multi agent tasks issue (#11672)
* improve run multi tasks ui * improve group mode * 🐛 fix: remove unused isCompleted variable in TaskTitle 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -118,10 +118,11 @@ export interface ExecutorResultSupervisorDecided {
|
||||
* - 'speak': Call a single agent
|
||||
* - 'broadcast': Call multiple agents in parallel
|
||||
* - 'delegate': Delegate to another agent
|
||||
* - 'execute_task': Execute an async task
|
||||
* - 'execute_task': Execute a single async task
|
||||
* - 'execute_tasks': Execute multiple async tasks in parallel
|
||||
* - 'finish': End the orchestration
|
||||
*/
|
||||
decision: 'speak' | 'broadcast' | 'delegate' | 'execute_task' | 'finish';
|
||||
decision: 'speak' | 'broadcast' | 'delegate' | 'execute_task' | 'execute_tasks' | 'finish';
|
||||
/**
|
||||
* Parameters for the decision
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
'use client';
|
||||
|
||||
import { DEFAULT_AVATAR } from '@lobechat/const';
|
||||
import type { AgentGroupMember, BuiltinRenderProps } from '@lobechat/types';
|
||||
import { Avatar, Flexbox, Text } from '@lobehub/ui';
|
||||
import { createStaticStyles, useTheme } from 'antd-style';
|
||||
import { Clock } from 'lucide-react';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useAgentGroupStore } from '@/store/agentGroup';
|
||||
import { agentGroupSelectors } from '@/store/agentGroup/selectors';
|
||||
|
||||
import type { ExecuteTasksParams } from '../../../types';
|
||||
|
||||
const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
container: css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding-block: 12px;
|
||||
`,
|
||||
taskCard: css`
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
background: ${cssVar.colorFillQuaternary};
|
||||
`,
|
||||
taskContent: css`
|
||||
padding-block: 8px;
|
||||
padding-inline: 12px;
|
||||
border-radius: ${cssVar.borderRadius};
|
||||
background: ${cssVar.colorFillTertiary};
|
||||
`,
|
||||
taskHeader: css`
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: ${cssVar.colorText};
|
||||
`,
|
||||
timeout: css`
|
||||
font-size: 12px;
|
||||
color: ${cssVar.colorTextTertiary};
|
||||
`,
|
||||
}));
|
||||
|
||||
/**
|
||||
* ExecuteTasks Render component for Group Management tool
|
||||
* Read-only display of multiple task execution requests
|
||||
*/
|
||||
const ExecuteTasksRender = memo<BuiltinRenderProps<ExecuteTasksParams>>(({ args }) => {
|
||||
const { t } = useTranslation('tool');
|
||||
const theme = useTheme();
|
||||
const { tasks } = args || {};
|
||||
|
||||
// Get active group ID and agents from store
|
||||
const activeGroupId = useAgentGroupStore(agentGroupSelectors.activeGroupId);
|
||||
const groupAgents = useAgentGroupStore((s) =>
|
||||
activeGroupId ? agentGroupSelectors.getGroupAgents(activeGroupId)(s) : [],
|
||||
);
|
||||
|
||||
// Get agent details for each task
|
||||
const tasksWithAgents = useMemo(() => {
|
||||
if (!tasks?.length || !groupAgents.length) return [];
|
||||
return tasks.map((task) => ({
|
||||
...task,
|
||||
agent: groupAgents.find((agent) => agent.id === task.agentId) as AgentGroupMember | undefined,
|
||||
}));
|
||||
}, [tasks, groupAgents]);
|
||||
|
||||
if (!tasksWithAgents.length) return null;
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{tasksWithAgents.map((task, index) => {
|
||||
const timeoutMinutes = task.timeout ? Math.round(task.timeout / 60_000) : 30;
|
||||
|
||||
return (
|
||||
<div className={styles.taskCard} key={task.agentId || index}>
|
||||
<Flexbox gap={12}>
|
||||
{/* Header: Agent info + Timeout */}
|
||||
<Flexbox align={'center'} gap={12} horizontal justify={'space-between'}>
|
||||
<Flexbox align={'center'} flex={1} gap={8} horizontal style={{ minWidth: 0 }}>
|
||||
<Avatar
|
||||
avatar={task.agent?.avatar || DEFAULT_AVATAR}
|
||||
background={task.agent?.backgroundColor || theme.colorBgContainer}
|
||||
shape={'square'}
|
||||
size={20}
|
||||
/>
|
||||
<span className={styles.taskHeader}>
|
||||
{task.title || task.agent?.title || 'Task'}
|
||||
</span>
|
||||
</Flexbox>
|
||||
<Flexbox align="center" className={styles.timeout} gap={4} horizontal>
|
||||
<Clock size={14} />
|
||||
<span>
|
||||
{timeoutMinutes}{' '}
|
||||
{t('agentGroupManagement.executeTask.intervention.timeoutUnit')}
|
||||
</span>
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
|
||||
{/* Task content (read-only) */}
|
||||
{task.instruction && (
|
||||
<Text className={styles.taskContent} style={{ margin: 0 }}>
|
||||
{task.instruction}
|
||||
</Text>
|
||||
)}
|
||||
</Flexbox>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
ExecuteTasksRender.displayName = 'ExecuteTasksRender';
|
||||
|
||||
export default ExecuteTasksRender;
|
||||
@@ -1,6 +1,7 @@
|
||||
import { GroupManagementApiName } from '../../types';
|
||||
import BroadcastRender from './Broadcast';
|
||||
import ExecuteTaskRender from './ExecuteTask';
|
||||
import ExecuteTasksRender from './ExecuteTasks';
|
||||
import SpeakRender from './Speak';
|
||||
|
||||
/**
|
||||
@@ -9,9 +10,11 @@ import SpeakRender from './Speak';
|
||||
export const GroupManagementRenders = {
|
||||
[GroupManagementApiName.broadcast]: BroadcastRender,
|
||||
[GroupManagementApiName.executeAgentTask]: ExecuteTaskRender,
|
||||
[GroupManagementApiName.executeAgentTasks]: ExecuteTasksRender,
|
||||
[GroupManagementApiName.speak]: SpeakRender,
|
||||
};
|
||||
|
||||
export { default as BroadcastRender } from './Broadcast';
|
||||
export { default as ExecuteTaskRender } from './ExecuteTask';
|
||||
export { default as ExecuteTasksRender } from './ExecuteTasks';
|
||||
export { default as SpeakRender } from './Speak';
|
||||
|
||||
@@ -75,6 +75,7 @@ describe('GroupManagementExecutor', () => {
|
||||
triggerBroadcast: vi.fn(),
|
||||
triggerDelegate: vi.fn(),
|
||||
triggerExecuteTask: vi.fn(),
|
||||
triggerExecuteTasks: vi.fn(),
|
||||
triggerSpeak,
|
||||
},
|
||||
'supervisor-agent',
|
||||
@@ -122,6 +123,7 @@ describe('GroupManagementExecutor', () => {
|
||||
triggerBroadcast: vi.fn(),
|
||||
triggerDelegate: vi.fn(),
|
||||
triggerExecuteTask: vi.fn(),
|
||||
triggerExecuteTasks: vi.fn(),
|
||||
triggerSpeak,
|
||||
},
|
||||
'supervisor-agent',
|
||||
@@ -171,6 +173,7 @@ describe('GroupManagementExecutor', () => {
|
||||
triggerBroadcast,
|
||||
triggerDelegate: vi.fn(),
|
||||
triggerExecuteTask: vi.fn(),
|
||||
triggerExecuteTasks: vi.fn(),
|
||||
triggerSpeak: vi.fn(),
|
||||
},
|
||||
'supervisor-agent',
|
||||
@@ -238,6 +241,7 @@ describe('GroupManagementExecutor', () => {
|
||||
triggerBroadcast: vi.fn(),
|
||||
triggerDelegate,
|
||||
triggerExecuteTask: vi.fn(),
|
||||
triggerExecuteTasks: vi.fn(),
|
||||
triggerSpeak: vi.fn(),
|
||||
},
|
||||
'supervisor-agent',
|
||||
@@ -308,6 +312,7 @@ describe('GroupManagementExecutor', () => {
|
||||
triggerBroadcast: vi.fn(),
|
||||
triggerDelegate: vi.fn(),
|
||||
triggerExecuteTask,
|
||||
triggerExecuteTasks: vi.fn(),
|
||||
triggerSpeak: vi.fn(),
|
||||
},
|
||||
'supervisor-agent',
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
CreateWorkflowParams,
|
||||
DelegateParams,
|
||||
ExecuteTaskParams,
|
||||
ExecuteTasksParams,
|
||||
GroupManagementApiName,
|
||||
GroupManagementIdentifier,
|
||||
InterruptParams,
|
||||
@@ -153,6 +154,38 @@ class GroupManagementExecutor extends BaseExecutor<typeof GroupManagementApiName
|
||||
};
|
||||
};
|
||||
|
||||
executeAgentTasks = async (
|
||||
params: ExecuteTasksParams,
|
||||
ctx: BuiltinToolContext,
|
||||
): Promise<BuiltinToolResult> => {
|
||||
// Register afterCompletion callback to trigger parallel task execution after AgentRuntime completes
|
||||
// This follows the same pattern as executeAgentTask - trigger mode, not blocking
|
||||
if (ctx.groupOrchestration && ctx.agentId && ctx.registerAfterCompletion) {
|
||||
ctx.registerAfterCompletion(() =>
|
||||
ctx.groupOrchestration!.triggerExecuteTasks({
|
||||
skipCallSupervisor: params.skipCallSupervisor,
|
||||
supervisorAgentId: ctx.agentId!,
|
||||
tasks: params.tasks,
|
||||
toolMessageId: ctx.messageId,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const agentIds = params.tasks.map((t) => t.agentId).join(', ');
|
||||
|
||||
// Returns stop: true to indicate the supervisor should stop and let the tasks execute
|
||||
return {
|
||||
content: `Triggered ${params.tasks.length} parallel tasks for agents: ${agentIds}.`,
|
||||
state: {
|
||||
skipCallSupervisor: params.skipCallSupervisor,
|
||||
tasks: params.tasks,
|
||||
type: 'executeAgentTasks',
|
||||
},
|
||||
stop: true,
|
||||
success: true,
|
||||
};
|
||||
};
|
||||
|
||||
interrupt = async (
|
||||
params: InterruptParams,
|
||||
_ctx: BuiltinToolContext,
|
||||
|
||||
@@ -121,52 +121,53 @@ export const GroupManagementManifest: BuiltinToolManifest = {
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
{
|
||||
description:
|
||||
'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.',
|
||||
name: GroupManagementApiName.executeAgentTasks,
|
||||
humanIntervention: 'required',
|
||||
parameters: {
|
||||
properties: {
|
||||
tasks: {
|
||||
description: 'Array of tasks, each assigned to a specific agent.',
|
||||
items: {
|
||||
properties: {
|
||||
agentId: {
|
||||
description: 'The ID of the agent to execute this task.',
|
||||
type: 'string',
|
||||
},
|
||||
title: {
|
||||
description: 'Brief title describing what this task does (shown in UI).',
|
||||
type: 'string',
|
||||
},
|
||||
instruction: {
|
||||
description:
|
||||
'Detailed instruction/prompt for the task execution. Be specific about expected deliverables.',
|
||||
type: 'string',
|
||||
},
|
||||
timeout: {
|
||||
description:
|
||||
'Optional timeout in milliseconds for this task (default: 1800000, 30 minutes).',
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
required: ['agentId', 'title', 'instruction'],
|
||||
type: 'object',
|
||||
},
|
||||
type: 'array',
|
||||
},
|
||||
skipCallSupervisor: {
|
||||
default: false,
|
||||
description:
|
||||
'If true, the orchestration will end after all tasks complete, without calling the supervisor again.',
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
required: ['tasks'],
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
// TODO: Enable executeAgentTasks when ready
|
||||
// {
|
||||
// description:
|
||||
// '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.',
|
||||
// name: GroupManagementApiName.executeAgentTasks,
|
||||
// humanIntervention: 'required',
|
||||
// parameters: {
|
||||
// properties: {
|
||||
// tasks: {
|
||||
// description: 'Array of tasks, each assigned to a specific agent.',
|
||||
// items: {
|
||||
// properties: {
|
||||
// agentId: {
|
||||
// description: 'The ID of the agent to execute this task.',
|
||||
// type: 'string',
|
||||
// },
|
||||
// title: {
|
||||
// description: 'Brief title describing what this task does (shown in UI).',
|
||||
// type: 'string',
|
||||
// },
|
||||
// instruction: {
|
||||
// description:
|
||||
// 'Detailed instruction/prompt for the task execution. Be specific about expected deliverables.',
|
||||
// type: 'string',
|
||||
// },
|
||||
// timeout: {
|
||||
// description:
|
||||
// 'Optional timeout in milliseconds for this task (default: 1800000, 30 minutes).',
|
||||
// type: 'number',
|
||||
// },
|
||||
// },
|
||||
// required: ['agentId', 'title', 'instruction'],
|
||||
// type: 'object',
|
||||
// },
|
||||
// type: 'array',
|
||||
// },
|
||||
// skipCallSupervisor: {
|
||||
// default: false,
|
||||
// description:
|
||||
// 'If true, the orchestration will end after all tasks complete, without calling the supervisor again.',
|
||||
// type: 'boolean',
|
||||
// },
|
||||
// },
|
||||
// required: ['tasks'],
|
||||
// type: 'object',
|
||||
// },
|
||||
// },
|
||||
{
|
||||
description:
|
||||
'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
|
||||
- **broadcast**: Multiple agents respond in parallel in group context
|
||||
|
||||
**Task Execution (Independent Context, With Tools):**
|
||||
- **executeAgentTask**: Assign a single task to one agent in isolated context
|
||||
- **executeAgentTasks**: Assign multiple tasks to different agents in parallel (each with isolated context)
|
||||
- **executeAgentTask**: Assign a task to one agent in isolated context
|
||||
- **interrupt**: Stop a running task
|
||||
|
||||
**Flow Control:**
|
||||
@@ -176,19 +175,17 @@ Analysis: Opinion-based, no tools needed
|
||||
Action: broadcast to [Architect, DevOps, Backend] - share perspectives
|
||||
\`\`\`
|
||||
|
||||
### Pattern 2: Independent Research (Parallel Tasks)
|
||||
When multiple agents need to research/work independently using their tools.
|
||||
### Pattern 2: Independent Research (Task)
|
||||
When an agent needs to research/work independently using their tools.
|
||||
|
||||
\`\`\`
|
||||
User: "Research the pros and cons of React vs Vue vs Svelte"
|
||||
Analysis: Requires web search, agents work independently
|
||||
Action: executeAgentTasks with parallel assignments
|
||||
executeAgentTasks({
|
||||
tasks: [
|
||||
{ agentId: "frontend-expert", title: "Research React", instruction: "Research React ecosystem, performance benchmarks, community size, and typical use cases. Provide pros and cons." },
|
||||
{ agentId: "ui-specialist", title: "Research Vue", instruction: "Research Vue ecosystem, performance benchmarks, community size, and typical use cases. Provide pros and cons." },
|
||||
{ agentId: "tech-analyst", title: "Research Svelte", instruction: "Research Svelte ecosystem, performance benchmarks, community size, and typical use cases. Provide pros and cons." }
|
||||
]
|
||||
User: "Research the pros and cons of React"
|
||||
Analysis: Requires web search, agent works independently
|
||||
Action: executeAgentTask to frontend expert
|
||||
executeAgentTask({
|
||||
agentId: "frontend-expert",
|
||||
title: "Research React",
|
||||
task: "Research React ecosystem, performance benchmarks, community size, and typical use cases. Provide pros and cons."
|
||||
})
|
||||
\`\`\`
|
||||
|
||||
@@ -211,28 +208,25 @@ When you need facts first, then discussion.
|
||||
User: "Should we migrate to Kubernetes? Research and discuss."
|
||||
Analysis: First gather facts (tools), then discuss (no tools)
|
||||
Action:
|
||||
1. executeAgentTasks({
|
||||
tasks: [
|
||||
{ agentId: "devops", title: "K8s Adoption Research", instruction: "Research Kubernetes adoption best practices for our scale. Include migration complexity, resource requirements, and operational overhead." },
|
||||
{ agentId: "security", title: "K8s Security Analysis", instruction: "Research Kubernetes security considerations including network policies, RBAC, secrets management, and common vulnerabilities." }
|
||||
]
|
||||
1. executeAgentTask({
|
||||
agentId: "devops",
|
||||
title: "K8s Adoption Research",
|
||||
task: "Research Kubernetes adoption best practices for our scale. Include migration complexity, resource requirements, operational overhead, and security considerations."
|
||||
})
|
||||
2. [Wait for results]
|
||||
3. broadcast: "Based on the research, share your recommendations"
|
||||
\`\`\`
|
||||
|
||||
### Pattern 5: Collaborative Implementation (Parallel Tasks)
|
||||
When multiple agents create deliverables using their tools.
|
||||
### Pattern 5: Implementation Task
|
||||
When an agent needs to create deliverables using their tools.
|
||||
|
||||
\`\`\`
|
||||
User: "Create a landing page - need copy, design specs, and code"
|
||||
Analysis: Each agent produces artifacts using their tools
|
||||
Action: executeAgentTasks({
|
||||
tasks: [
|
||||
{ agentId: "copywriter", title: "Write Copy", instruction: "Write compelling landing page copy for [product]. Include headline, subheadline, feature descriptions, and CTA text." },
|
||||
{ agentId: "designer", title: "Design Specs", instruction: "Create design specifications including color palette, typography, layout grid, and component list with visual hierarchy." },
|
||||
{ agentId: "frontend-dev", title: "Implement Page", instruction: "Implement the landing page using React. Include responsive design, animations, and SEO-friendly markup." }
|
||||
]
|
||||
User: "Write the landing page copy"
|
||||
Analysis: Agent produces artifacts using their tools
|
||||
Action: executeAgentTask({
|
||||
agentId: "copywriter",
|
||||
title: "Write Copy",
|
||||
task: "Write compelling landing page copy for [product]. Include headline, subheadline, feature descriptions, and CTA text."
|
||||
})
|
||||
\`\`\`
|
||||
</workflow_patterns>
|
||||
@@ -243,8 +237,7 @@ Action: executeAgentTasks({
|
||||
- broadcast: \`agentIds\` (array), \`instruction\` (optional shared guidance)
|
||||
|
||||
**Task Execution:**
|
||||
- executeAgentTask: \`agentId\`, \`task\` (clear deliverable description), \`timeout\` (optional, default 30min)
|
||||
- executeAgentTasks: \`tasks\` (array of {agentId, title, instruction, timeout?}) - **Use this for parallel task execution across multiple agents**
|
||||
- executeAgentTask: \`agentId\`, \`title\`, \`task\` (clear deliverable description), \`timeout\` (optional, default 30min)
|
||||
- interrupt: \`taskId\`
|
||||
|
||||
**Flow Control:**
|
||||
|
||||
@@ -452,6 +452,47 @@ export interface TriggerExecuteTaskParams extends GroupOrchestrationBaseParams {
|
||||
toolMessageId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Task item for triggerExecuteTasks callback
|
||||
*/
|
||||
export interface TriggerExecuteTaskItem {
|
||||
/**
|
||||
* The agent ID to execute this task
|
||||
*/
|
||||
agentId: string;
|
||||
/**
|
||||
* Detailed instruction/prompt for the task execution
|
||||
*/
|
||||
instruction: string;
|
||||
/**
|
||||
* Optional timeout in milliseconds for this specific task
|
||||
*/
|
||||
timeout?: number;
|
||||
/**
|
||||
* Brief title describing what this task does (shown in UI)
|
||||
*/
|
||||
title: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Params for triggerExecuteTasks callback (multiple tasks)
|
||||
*/
|
||||
export interface TriggerExecuteTasksParams extends GroupOrchestrationBaseParams {
|
||||
/**
|
||||
* If true, the orchestration will end after all tasks complete,
|
||||
* without calling the supervisor again.
|
||||
*/
|
||||
skipCallSupervisor?: boolean;
|
||||
/**
|
||||
* Array of tasks to execute, each assigned to a specific agent
|
||||
*/
|
||||
tasks: TriggerExecuteTaskItem[];
|
||||
/**
|
||||
* The tool message ID that triggered the tasks
|
||||
*/
|
||||
toolMessageId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Group Orchestration callbacks for group management tools
|
||||
* These callbacks are used to trigger the next phase in multi-agent orchestration
|
||||
@@ -472,6 +513,11 @@ export interface GroupOrchestrationCallbacks {
|
||||
*/
|
||||
triggerExecuteTask: (params: TriggerExecuteTaskParams) => Promise<void>;
|
||||
|
||||
/**
|
||||
* Trigger async execution of multiple tasks in parallel
|
||||
*/
|
||||
triggerExecuteTasks: (params: TriggerExecuteTasksParams) => Promise<void>;
|
||||
|
||||
/**
|
||||
* Trigger speak to a specific agent
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
'use client';
|
||||
|
||||
import { AccordionItem, Block, Text } from '@lobehub/ui';
|
||||
import { memo, useMemo, useState } from 'react';
|
||||
|
||||
import { ThreadStatus } from '@/types/index';
|
||||
import type { UIChatMessage } from '@/types/index';
|
||||
|
||||
import {
|
||||
CompletedState,
|
||||
ErrorState,
|
||||
InitializingState,
|
||||
ProcessingState,
|
||||
isProcessingStatus,
|
||||
} from '../shared';
|
||||
import TaskTitle, { type TaskMetrics } from './TaskTitle';
|
||||
import { useClientTaskStats } from './useClientTaskStats';
|
||||
|
||||
interface ClientTaskItemProps {
|
||||
item: UIChatMessage;
|
||||
}
|
||||
|
||||
const ClientTaskItem = memo<ClientTaskItemProps>(({ item }) => {
|
||||
const { id, content, metadata, taskDetail } = item;
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
const title = taskDetail?.title || metadata?.taskTitle;
|
||||
const instruction = metadata?.instruction;
|
||||
const status = taskDetail?.status;
|
||||
|
||||
const isProcessing = isProcessingStatus(status);
|
||||
const isCompleted = status === ThreadStatus.Completed;
|
||||
const isError = status === ThreadStatus.Failed || status === ThreadStatus.Cancel;
|
||||
const isInitializing = !taskDetail || !status;
|
||||
|
||||
// Fetch client task stats when processing
|
||||
const clientStats = useClientTaskStats({
|
||||
enabled: isProcessing,
|
||||
threadId: taskDetail?.threadId,
|
||||
});
|
||||
|
||||
// Build metrics for TaskTitle
|
||||
const metrics: TaskMetrics | undefined = useMemo(() => {
|
||||
if (isProcessing) {
|
||||
return {
|
||||
isLoading: clientStats.isLoading,
|
||||
startTime: clientStats.startTime,
|
||||
steps: clientStats.steps,
|
||||
toolCalls: clientStats.toolCalls,
|
||||
};
|
||||
}
|
||||
if (isCompleted || isError) {
|
||||
return {
|
||||
duration: taskDetail?.duration,
|
||||
steps: taskDetail?.totalSteps,
|
||||
toolCalls: taskDetail?.totalToolCalls,
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}, [
|
||||
isProcessing,
|
||||
isCompleted,
|
||||
isError,
|
||||
clientStats,
|
||||
taskDetail?.duration,
|
||||
taskDetail?.totalSteps,
|
||||
taskDetail?.totalToolCalls,
|
||||
]);
|
||||
|
||||
return (
|
||||
<AccordionItem
|
||||
expand={expanded}
|
||||
itemKey={id}
|
||||
onExpandChange={setExpanded}
|
||||
paddingBlock={4}
|
||||
paddingInline={4}
|
||||
title={<TaskTitle metrics={metrics} status={status} title={title} />}
|
||||
>
|
||||
<Block gap={16} padding={12} style={{ marginBlock: 8 }} variant={'outlined'}>
|
||||
{instruction && (
|
||||
<Block padding={12}>
|
||||
<Text fontSize={13} type={'secondary'}>
|
||||
{instruction}
|
||||
</Text>
|
||||
</Block>
|
||||
)}
|
||||
|
||||
{/* Initializing State - no taskDetail yet */}
|
||||
{isInitializing && <InitializingState />}
|
||||
|
||||
{/* Processing State */}
|
||||
{!isInitializing && isProcessing && taskDetail && (
|
||||
<ProcessingState messageId={id} taskDetail={taskDetail} variant="compact" />
|
||||
)}
|
||||
|
||||
{/* Error State */}
|
||||
{!isInitializing && isError && taskDetail && <ErrorState taskDetail={taskDetail} />}
|
||||
|
||||
{/* Completed State */}
|
||||
{!isInitializing && isCompleted && taskDetail && (
|
||||
<CompletedState
|
||||
content={content}
|
||||
expanded={expanded}
|
||||
taskDetail={taskDetail}
|
||||
variant="compact"
|
||||
/>
|
||||
)}
|
||||
</Block>
|
||||
</AccordionItem>
|
||||
);
|
||||
}, Object.is);
|
||||
|
||||
ClientTaskItem.displayName = 'ClientTaskItem';
|
||||
|
||||
export default ClientTaskItem;
|
||||
@@ -0,0 +1,92 @@
|
||||
'use client';
|
||||
|
||||
import { AccordionItem, Block, Text } from '@lobehub/ui';
|
||||
import { memo, useMemo, useState } from 'react';
|
||||
|
||||
import { ThreadStatus } from '@/types/index';
|
||||
import type { UIChatMessage } from '@/types/index';
|
||||
|
||||
import {
|
||||
CompletedState,
|
||||
ErrorState,
|
||||
InitializingState,
|
||||
ProcessingState,
|
||||
isProcessingStatus,
|
||||
} from '../shared';
|
||||
import TaskTitle, { type TaskMetrics } from './TaskTitle';
|
||||
|
||||
interface ServerTaskItemProps {
|
||||
item: UIChatMessage;
|
||||
}
|
||||
|
||||
const ServerTaskItem = memo<ServerTaskItemProps>(({ item }) => {
|
||||
const { id, content, metadata, taskDetail } = item;
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
const title = taskDetail?.title || metadata?.taskTitle;
|
||||
const instruction = metadata?.instruction;
|
||||
const status = taskDetail?.status;
|
||||
|
||||
const isProcessing = isProcessingStatus(status);
|
||||
const isCompleted = status === ThreadStatus.Completed;
|
||||
const isError = status === ThreadStatus.Failed || status === ThreadStatus.Cancel;
|
||||
const isInitializing = !taskDetail || !status;
|
||||
|
||||
// Build metrics for TaskTitle (only for completed/error states)
|
||||
const metrics: TaskMetrics | undefined = useMemo(() => {
|
||||
if (isCompleted || isError) {
|
||||
return {
|
||||
duration: taskDetail?.duration,
|
||||
steps: taskDetail?.totalSteps,
|
||||
toolCalls: taskDetail?.totalToolCalls,
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}, [isCompleted, isError, taskDetail?.duration, taskDetail?.totalSteps, taskDetail?.totalToolCalls]);
|
||||
|
||||
return (
|
||||
<AccordionItem
|
||||
expand={expanded}
|
||||
itemKey={id}
|
||||
onExpandChange={setExpanded}
|
||||
paddingBlock={4}
|
||||
paddingInline={4}
|
||||
title={<TaskTitle metrics={metrics} status={status} title={title} />}
|
||||
>
|
||||
<Block gap={16} padding={12} style={{ marginBlock: 8 }} variant={'outlined'}>
|
||||
{instruction && (
|
||||
<Block padding={12}>
|
||||
<Text fontSize={13} type={'secondary'}>
|
||||
{instruction}
|
||||
</Text>
|
||||
</Block>
|
||||
)}
|
||||
|
||||
{/* Initializing State - no taskDetail yet */}
|
||||
{isInitializing && <InitializingState />}
|
||||
|
||||
{/* Processing State */}
|
||||
{!isInitializing && isProcessing && taskDetail && (
|
||||
<ProcessingState messageId={id} taskDetail={taskDetail} variant="compact" />
|
||||
)}
|
||||
|
||||
{/* Error State */}
|
||||
{!isInitializing && isError && taskDetail && <ErrorState taskDetail={taskDetail} />}
|
||||
|
||||
{/* Completed State */}
|
||||
{!isInitializing && isCompleted && taskDetail && (
|
||||
<CompletedState
|
||||
content={content}
|
||||
expanded={expanded}
|
||||
taskDetail={taskDetail}
|
||||
variant="compact"
|
||||
/>
|
||||
)}
|
||||
</Block>
|
||||
</AccordionItem>
|
||||
);
|
||||
}, Object.is);
|
||||
|
||||
ServerTaskItem.displayName = 'ServerTaskItem';
|
||||
|
||||
export default ServerTaskItem;
|
||||
@@ -2,15 +2,31 @@
|
||||
|
||||
import { Block, Flexbox, Icon, Text } from '@lobehub/ui';
|
||||
import { cssVar } from 'antd-style';
|
||||
import { ListChecksIcon, XIcon } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
import { Footprints, ListChecksIcon, Wrench, XIcon } from 'lucide-react';
|
||||
import { memo, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import NeuralNetworkLoading from '@/components/NeuralNetworkLoading';
|
||||
import { ThreadStatus } from '@/types/index';
|
||||
|
||||
import { isProcessingStatus } from '../shared';
|
||||
import { formatDuration, formatElapsedTime, isProcessingStatus } from '../shared';
|
||||
|
||||
export interface TaskMetrics {
|
||||
/** Task duration in milliseconds (for completed tasks) */
|
||||
duration?: number;
|
||||
/** Whether metrics are still loading */
|
||||
isLoading?: boolean;
|
||||
/** Start time timestamp for elapsed time calculation */
|
||||
startTime?: number;
|
||||
/** Number of execution steps/blocks */
|
||||
steps?: number;
|
||||
/** Total tool calls count */
|
||||
toolCalls?: number;
|
||||
}
|
||||
|
||||
interface TaskTitleProps {
|
||||
/** Metrics to display (steps, tool calls, elapsed time) */
|
||||
metrics?: TaskMetrics;
|
||||
status?: ThreadStatus;
|
||||
title?: string;
|
||||
}
|
||||
@@ -54,13 +70,90 @@ const TaskStatusIndicator = memo<{ status?: ThreadStatus }>(({ status }) => {
|
||||
|
||||
TaskStatusIndicator.displayName = 'TaskStatusIndicator';
|
||||
|
||||
const TaskTitle = memo<TaskTitleProps>(({ title, status }) => {
|
||||
interface MetricsDisplayProps {
|
||||
metrics: TaskMetrics;
|
||||
status?: ThreadStatus;
|
||||
}
|
||||
|
||||
const MetricsDisplay = memo<MetricsDisplayProps>(({ metrics, status }) => {
|
||||
const { t } = useTranslation('chat');
|
||||
const { steps, toolCalls, startTime, duration, isLoading } = metrics;
|
||||
const [elapsedTime, setElapsedTime] = useState(0);
|
||||
|
||||
const isProcessing = status ? isProcessingStatus(status) : false;
|
||||
|
||||
// Calculate initial elapsed time
|
||||
useEffect(() => {
|
||||
if (startTime && isProcessing) {
|
||||
setElapsedTime(Math.max(0, Date.now() - startTime));
|
||||
}
|
||||
}, [startTime, isProcessing]);
|
||||
|
||||
// Timer for updating elapsed time every second (only when processing)
|
||||
useEffect(() => {
|
||||
if (!startTime || !isProcessing) return;
|
||||
|
||||
const timer = setInterval(() => {
|
||||
setElapsedTime(Math.max(0, Date.now() - startTime));
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, [startTime, isProcessing]);
|
||||
|
||||
// Don't show metrics if loading or no data
|
||||
if (isLoading) return null;
|
||||
|
||||
const hasSteps = steps !== undefined && steps > 0;
|
||||
const hasToolCalls = toolCalls !== undefined && toolCalls > 0;
|
||||
const hasTime = isProcessing ? startTime !== undefined : duration !== undefined;
|
||||
|
||||
// Don't render if no metrics to show
|
||||
if (!hasSteps && !hasToolCalls && !hasTime) return null;
|
||||
|
||||
return (
|
||||
<Flexbox align={'center'} gap={6} horizontal>
|
||||
<Flexbox align="center" gap={8} horizontal>
|
||||
{/* Steps */}
|
||||
{hasSteps && (
|
||||
<Flexbox align="center" gap={2} horizontal>
|
||||
<Icon color={cssVar.colorTextTertiary} icon={Footprints} size={12} />
|
||||
<Text fontSize={12} type="secondary">
|
||||
{steps}
|
||||
</Text>
|
||||
</Flexbox>
|
||||
)}
|
||||
{/* Tool calls */}
|
||||
{hasToolCalls && (
|
||||
<Flexbox align="center" gap={2} horizontal>
|
||||
<Icon color={cssVar.colorTextTertiary} icon={Wrench} size={12} />
|
||||
<Text fontSize={12} type="secondary">
|
||||
{toolCalls}
|
||||
</Text>
|
||||
</Flexbox>
|
||||
)}
|
||||
{/* Time */}
|
||||
{hasTime && (
|
||||
<Text fontSize={12} type="secondary">
|
||||
{isProcessing
|
||||
? formatElapsedTime(elapsedTime)
|
||||
: duration
|
||||
? t('task.metrics.duration', { duration: formatDuration(duration) })
|
||||
: null}
|
||||
</Text>
|
||||
)}
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
MetricsDisplay.displayName = 'MetricsDisplay';
|
||||
|
||||
const TaskTitle = memo<TaskTitleProps>(({ title, status, metrics }) => {
|
||||
return (
|
||||
<Flexbox align="center" gap={6} horizontal>
|
||||
<TaskStatusIndicator status={status} />
|
||||
<Text ellipsis fontSize={14}>
|
||||
{title}
|
||||
</Text>
|
||||
{metrics && <MetricsDisplay metrics={metrics} status={status} />}
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,81 +1,26 @@
|
||||
'use client';
|
||||
|
||||
import { AccordionItem, Block, Text } from '@lobehub/ui';
|
||||
import { memo, useState } from 'react';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { ThreadStatus } from '@/types/index';
|
||||
import type { UIChatMessage } from '@/types/index';
|
||||
|
||||
import {
|
||||
CompletedState,
|
||||
ErrorState,
|
||||
InitializingState,
|
||||
ProcessingState,
|
||||
isProcessingStatus,
|
||||
} from '../shared';
|
||||
import TaskTitle from './TaskTitle';
|
||||
import ClientTaskItem from './ClientTaskItem';
|
||||
import ServerTaskItem from './ServerTaskItem';
|
||||
|
||||
interface TaskItemProps {
|
||||
item: UIChatMessage;
|
||||
}
|
||||
|
||||
const TaskItem = memo<TaskItemProps>(({ item }) => {
|
||||
const { id, content, metadata, taskDetail } = item;
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const isClientMode = item.taskDetail?.clientMode;
|
||||
|
||||
const title = taskDetail?.title || metadata?.taskTitle;
|
||||
const instruction = metadata?.instruction;
|
||||
const status = taskDetail?.status;
|
||||
if (isClientMode) {
|
||||
return <ClientTaskItem item={item} />;
|
||||
}
|
||||
|
||||
// Check if task is processing using shared utility
|
||||
const isProcessing = isProcessingStatus(status);
|
||||
|
||||
const isCompleted = status === ThreadStatus.Completed;
|
||||
const isError = status === ThreadStatus.Failed || status === ThreadStatus.Cancel;
|
||||
const isInitializing = !taskDetail || !status;
|
||||
|
||||
return (
|
||||
<AccordionItem
|
||||
expand={expanded}
|
||||
itemKey={id}
|
||||
onExpandChange={setExpanded}
|
||||
paddingBlock={4}
|
||||
paddingInline={4}
|
||||
title={<TaskTitle status={status} title={title} />}
|
||||
>
|
||||
<Block gap={16} padding={12} style={{ marginBlock: 8 }} variant={'outlined'}>
|
||||
{instruction && (
|
||||
<Block padding={12}>
|
||||
<Text fontSize={13} type={'secondary'}>
|
||||
{instruction}
|
||||
</Text>
|
||||
</Block>
|
||||
)}
|
||||
|
||||
{/* Initializing State - no taskDetail yet */}
|
||||
{isInitializing && <InitializingState />}
|
||||
|
||||
{/* Processing State */}
|
||||
{!isInitializing && isProcessing && taskDetail && (
|
||||
<ProcessingState messageId={id} taskDetail={taskDetail} variant="compact" />
|
||||
)}
|
||||
|
||||
{/* Error State */}
|
||||
{!isInitializing && isError && taskDetail && <ErrorState taskDetail={taskDetail} />}
|
||||
|
||||
{/* Completed State */}
|
||||
{!isInitializing && isCompleted && taskDetail && (
|
||||
<CompletedState
|
||||
content={content}
|
||||
expanded={expanded}
|
||||
taskDetail={taskDetail}
|
||||
variant="compact"
|
||||
/>
|
||||
)}
|
||||
</Block>
|
||||
</AccordionItem>
|
||||
);
|
||||
}, Object.is);
|
||||
return <ServerTaskItem item={item} />;
|
||||
}, isEqual);
|
||||
|
||||
TaskItem.displayName = 'TaskItem';
|
||||
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { useChatStore } from '@/store/chat';
|
||||
import { displayMessageSelectors } from '@/store/chat/selectors';
|
||||
import { messageMapKey } from '@/store/chat/utils/messageMapKey';
|
||||
|
||||
export interface ClientTaskStats {
|
||||
isLoading: boolean;
|
||||
startTime?: number;
|
||||
steps: number;
|
||||
toolCalls: number;
|
||||
}
|
||||
|
||||
interface UseClientTaskStatsOptions {
|
||||
enabled?: boolean;
|
||||
threadId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch thread messages and compute task statistics for client mode tasks.
|
||||
* Used in TaskItem to display progress metrics (steps, tool calls, elapsed time).
|
||||
*/
|
||||
export const useClientTaskStats = ({
|
||||
threadId,
|
||||
enabled = true,
|
||||
}: UseClientTaskStatsOptions): ClientTaskStats => {
|
||||
const [activeAgentId, activeTopicId, useFetchMessages] = useChatStore((s) => [
|
||||
s.activeAgentId,
|
||||
s.activeTopicId,
|
||||
s.useFetchMessages,
|
||||
]);
|
||||
|
||||
const threadContext = useMemo(
|
||||
() => ({
|
||||
agentId: activeAgentId,
|
||||
scope: 'thread' as const,
|
||||
threadId,
|
||||
topicId: activeTopicId,
|
||||
}),
|
||||
[activeAgentId, activeTopicId, threadId],
|
||||
);
|
||||
|
||||
const threadMessageKey = useMemo(
|
||||
() => (threadId ? messageMapKey(threadContext) : null),
|
||||
[threadId, threadContext],
|
||||
);
|
||||
|
||||
// Fetch thread messages (skip when disabled or no threadId)
|
||||
useFetchMessages(threadContext, !enabled || !threadId);
|
||||
|
||||
// Get thread messages from store using selector
|
||||
const threadMessages = useChatStore((s) =>
|
||||
threadMessageKey
|
||||
? displayMessageSelectors.getDisplayMessagesByKey(threadMessageKey)(s)
|
||||
: undefined,
|
||||
);
|
||||
|
||||
// Compute stats from thread messages
|
||||
return useMemo(() => {
|
||||
if (!threadMessages || !enabled) {
|
||||
return { isLoading: true, steps: 0, toolCalls: 0 };
|
||||
}
|
||||
|
||||
// Find the assistantGroup message which contains the children blocks
|
||||
const assistantGroupMessage = threadMessages.find((item) => item.role === 'assistantGroup');
|
||||
const blocks = assistantGroupMessage?.children ?? [];
|
||||
|
||||
// Calculate stats
|
||||
const steps = blocks.length;
|
||||
const toolCalls = blocks.reduce((sum, block) => sum + (block.tools?.length || 0), 0);
|
||||
const startTime = assistantGroupMessage?.createdAt;
|
||||
|
||||
return {
|
||||
isLoading: false,
|
||||
startTime,
|
||||
steps,
|
||||
toolCalls,
|
||||
};
|
||||
}, [threadMessages, enabled]);
|
||||
};
|
||||
18
src/features/Conversation/Messages/Tool/preload.ts
Normal file
18
src/features/Conversation/Messages/Tool/preload.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Preload Tool Render components to avoid Suspense flash on first expand
|
||||
*
|
||||
* These components are dynamically imported in Tool/Tool/index.tsx.
|
||||
* By preloading them when tool calls are detected, we can avoid
|
||||
* the loading skeleton flash when user first expands the tool result.
|
||||
*/
|
||||
|
||||
let preloaded = false;
|
||||
|
||||
export const preloadToolRenderComponents = () => {
|
||||
if (preloaded) return;
|
||||
preloaded = true;
|
||||
|
||||
// Preload Detail and Debug components (dynamic imports in Tool/Tool/index.tsx)
|
||||
import('../AssistantGroup/Tool/Detail');
|
||||
import('../AssistantGroup/Tool/Debug');
|
||||
};
|
||||
@@ -73,6 +73,12 @@ export interface GroupOrchestrationAction {
|
||||
*/
|
||||
triggerExecuteTask: GroupOrchestrationCallbacks['triggerExecuteTask'];
|
||||
|
||||
/**
|
||||
* Trigger execute tasks - called by executeTasks tool when supervisor decides to execute multiple async tasks in parallel
|
||||
* This starts the group orchestration loop with supervisor_decided result
|
||||
*/
|
||||
triggerExecuteTasks: GroupOrchestrationCallbacks['triggerExecuteTasks'];
|
||||
|
||||
/**
|
||||
* Enable polling for task status
|
||||
* Used by ProcessingState component to poll for real-time task updates
|
||||
@@ -240,6 +246,42 @@ export const groupOrchestrationSlice: StateCreator<
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Trigger execute tasks - Entry point when supervisor calls executeTasks tool
|
||||
* Creates a supervisor_decided result with decision='execute_tasks' and starts orchestration
|
||||
*/
|
||||
triggerExecuteTasks: async (params) => {
|
||||
const { supervisorAgentId, tasks, toolMessageId, skipCallSupervisor } = params;
|
||||
log(
|
||||
'[triggerExecuteTasks] Starting orchestration with execute_tasks: supervisorAgentId=%s, tasks=%d, toolMessageId=%s, skipCallSupervisor=%s',
|
||||
supervisorAgentId,
|
||||
tasks.length,
|
||||
toolMessageId,
|
||||
skipCallSupervisor,
|
||||
);
|
||||
|
||||
const groupId = get().activeGroupId;
|
||||
if (!groupId) {
|
||||
log('[triggerExecuteTasks] No active group, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
// Start orchestration loop with supervisor_decided result (decision=execute_tasks)
|
||||
await get().internal_execGroupOrchestration({
|
||||
groupId,
|
||||
supervisorAgentId,
|
||||
topicId: get().activeTopicId,
|
||||
initialResult: {
|
||||
type: 'supervisor_decided',
|
||||
payload: {
|
||||
decision: 'execute_tasks',
|
||||
params: { tasks, toolMessageId },
|
||||
skipCallSupervisor: skipCallSupervisor ?? false,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Get group orchestration callbacks
|
||||
* These are the action methods that tools can call to trigger orchestration
|
||||
@@ -250,6 +292,7 @@ export const groupOrchestrationSlice: StateCreator<
|
||||
triggerBroadcast: get().triggerBroadcast,
|
||||
triggerDelegate: get().triggerDelegate,
|
||||
triggerExecuteTask: get().triggerExecuteTask,
|
||||
triggerExecuteTasks: get().triggerExecuteTasks,
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
@@ -736,8 +736,13 @@ export const streamingExecutor: StateCreator<
|
||||
// After parallel tool batch completes, refresh messages to ensure all tool results are synced
|
||||
// This fixes the race condition where each tool's replaceMessages may overwrite others
|
||||
// REMEMBER: There is no test for it (too hard to add), if you want to change it , ask @arvinxx first
|
||||
if (result.nextContext?.phase === 'tools_batch_result') {
|
||||
log('[internal_execAgentRuntime] Tools batch completed, refreshing messages to sync state');
|
||||
if (
|
||||
result.nextContext?.phase &&
|
||||
['tasks_batch_result', 'tools_batch_result'].includes(result.nextContext?.phase)
|
||||
) {
|
||||
log(
|
||||
`[internal_execAgentRuntime] ${result.nextContext?.phase} completed, refreshing messages to sync state`,
|
||||
);
|
||||
await get().refreshMessages(context);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ export type {
|
||||
IBuiltinToolExecutor,
|
||||
TriggerBroadcastParams,
|
||||
TriggerDelegateParams,
|
||||
TriggerExecuteTaskItem,
|
||||
TriggerExecuteTasksParams,
|
||||
TriggerSpeakParams,
|
||||
} from '@lobechat/types';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user