feat(ui): 添加子 Agent 进度显示功能

当 build agent 调用 guide/explore 等子 agent 时,
用户可以在 web 页面实时看到子 agent 的执行进度

实现方案:
- Core: 使用 EventEmitter 模式发射子 agent 事件
- Server: 订阅事件并转发到 WebSocket
- UI: 处理事件并渲染 SubagentProgress 组件

新增文件:
- packages/core/src/agent/events.ts
- packages/ui/src/components/SubagentProgress.tsx

修改文件:
- core: executor.ts, manager.ts, types.ts, task.ts
- server: adapter.ts, types.ts
- ui: useChat.ts, types.ts
- web: Chat.tsx
This commit is contained in:
2025-12-16 19:38:36 +08:00
parent f0ff887129
commit 08d481483c
13 changed files with 921 additions and 19 deletions
+77 -11
View File
@@ -18,6 +18,7 @@ import type {
import { checkBashPermission, isPathInAllowedWritePaths } from './permission-merger.js';
import { getProviderRegistry } from '../provider/index.js';
import { renderPromptTemplate, createPlanContext } from '../template/index.js';
import { agentEventEmitter } from './events.js';
/**
* Agent 执行器
@@ -54,7 +55,10 @@ export class AgentExecutor {
prompt: string,
context: AgentExecutionContext
): Promise<AgentExecutionResult> {
const { onStream, onToolCall, onToolResult, images } = context;
const { onStream, onToolCall, onToolResult, images, sessionId, agentId, emitEvents } = context;
// 是否发射子 Agent 事件
const shouldEmitEvents = emitEvents && sessionId && agentId;
// 获取过滤后的工具
const tools = this.getFilteredTools();
@@ -82,9 +86,12 @@ export class AgentExecutor {
let fullResponse = '';
let steps = 0;
// 工具调用时间追踪(用于计算持续时间)
const toolStartTimes = new Map<string, number>();
try {
if (onStream) {
// 流式模式
if (onStream || shouldEmitEvents) {
// 流式模式(或需要发射事件时使用流式模式)
const result = streamText({
model: this.getModel(modelName),
system: systemPrompt,
@@ -96,23 +103,72 @@ export class AgentExecutor {
if (chunk.type === 'tool-call') {
steps++;
const toolArgs = 'input' in chunk ? chunk.input : {};
const toolCallId = `tool-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
// 记录工具开始时间
toolStartTimes.set(chunk.toolName, Date.now());
onToolCall?.(chunk.toolName, toolArgs as Record<string, unknown>);
onStream(`\n[调用工具: ${chunk.toolName}]\n`);
onStream?.(`\n[调用工具: ${chunk.toolName}]\n`);
// 发射子 Agent 工具开始事件
if (shouldEmitEvents) {
agentEventEmitter.emit({
type: 'subagent:tool_start',
sessionId: sessionId!,
agentId: agentId!,
toolCallId,
toolName: chunk.toolName,
args: toolArgs as Record<string, unknown>,
});
}
} else if (chunk.type === 'tool-result') {
const output = (chunk as { output?: ToolResult }).output;
onToolResult?.(
(chunk as { toolName?: string }).toolName ?? 'unknown',
output
);
const toolName = (chunk as { toolName?: string }).toolName ?? 'unknown';
const toolCallId = `tool-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
// 计算工具执行时间
const startTime = toolStartTimes.get(toolName);
const duration = startTime ? Date.now() - startTime : undefined;
toolStartTimes.delete(toolName);
onToolResult?.(toolName, output);
if (output && typeof output === 'object') {
if (output.success) {
const displayOutput =
output.output.length > 500
? output.output.substring(0, 500) + '...(截断)'
: output.output;
onStream(`[结果: ${displayOutput}]\n`);
onStream?.(`[结果: ${displayOutput}]\n`);
// 发射子 Agent 工具完成事件
if (shouldEmitEvents) {
agentEventEmitter.emit({
type: 'subagent:tool_end',
sessionId: sessionId!,
agentId: agentId!,
toolCallId,
status: 'completed',
result: output.output,
duration,
});
}
} else {
onStream(`[错误: ${output.error}]\n`);
onStream?.(`[错误: ${output.error}]\n`);
// 发射子 Agent 工具错误事件
if (shouldEmitEvents) {
agentEventEmitter.emit({
type: 'subagent:tool_end',
sessionId: sessionId!,
agentId: agentId!,
toolCallId,
status: 'error',
error: output.error,
duration,
});
}
}
}
}
@@ -121,7 +177,17 @@ export class AgentExecutor {
for await (const chunk of result.textStream) {
fullResponse += chunk;
onStream(chunk);
onStream?.(chunk);
// 发射子 Agent 流式输出事件
if (shouldEmitEvents && chunk) {
agentEventEmitter.emit({
type: 'subagent:stream',
sessionId: sessionId!,
agentId: agentId!,
content: chunk,
});
}
}
await result.response;