feat: 添加后台 Agent 执行和模型选择功能
- 新增 AgentManager 管理后台 Agent 生命周期 - Task 工具支持 run_in_background 参数实现后台执行 - Task 工具支持 model 参数选择 sonnet/opus/haiku 模型 - 新增 agent_output 工具查询后台 Agent 执行状态和结果 - 添加 AgentManager 和 AgentOutput 工具单元测试
This commit is contained in:
@@ -21,6 +21,15 @@ export { AgentRegistry, agentRegistry } from './registry.js';
|
||||
// Executor
|
||||
export { AgentExecutor } from './executor.js';
|
||||
|
||||
// Manager
|
||||
export {
|
||||
AgentManager,
|
||||
getAgentManager,
|
||||
resetAgentManager,
|
||||
type BackgroundAgent,
|
||||
type BackgroundAgentStatus,
|
||||
} from './manager.js';
|
||||
|
||||
// Permission Merger
|
||||
export {
|
||||
SYSTEM_DEFAULT_PERMISSION,
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import type { AgentConfig } from '../types/index.js';
|
||||
import type { AgentExecutionContext, AgentInfo } from './types.js';
|
||||
import { AgentExecutor } from './executor.js';
|
||||
import { ToolRegistry } from '../tools/registry.js';
|
||||
|
||||
/**
|
||||
* 后台 Agent 状态
|
||||
*/
|
||||
export type BackgroundAgentStatus = 'running' | 'completed' | 'failed';
|
||||
|
||||
/**
|
||||
* 后台 Agent 信息
|
||||
*/
|
||||
export interface BackgroundAgent {
|
||||
/** Agent 唯一 ID */
|
||||
id: string;
|
||||
/** Agent 类型名称 */
|
||||
agentName: string;
|
||||
/** 任务描述 */
|
||||
description: string;
|
||||
/** 执行状态 */
|
||||
status: BackgroundAgentStatus;
|
||||
/** 任务提示词 */
|
||||
prompt: string;
|
||||
/** 开始时间 */
|
||||
startedAt: Date;
|
||||
/** 完成时间 */
|
||||
completedAt?: Date;
|
||||
/** 执行结果 */
|
||||
result?: string;
|
||||
/** 错误信息 */
|
||||
error?: string;
|
||||
/** 执行步数 */
|
||||
steps?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Agent 管理器
|
||||
* 负责管理后台 Agent 的生命周期
|
||||
*/
|
||||
export class AgentManager {
|
||||
private backgroundAgents = new Map<string, BackgroundAgent>();
|
||||
private completionCallbacks = new Map<string, Array<() => void>>();
|
||||
|
||||
/**
|
||||
* 启动后台 Agent
|
||||
*/
|
||||
async runInBackground(
|
||||
agentInfo: AgentInfo,
|
||||
description: string,
|
||||
prompt: string,
|
||||
baseConfig: AgentConfig,
|
||||
toolRegistry: ToolRegistry,
|
||||
context: AgentExecutionContext
|
||||
): Promise<string> {
|
||||
const agentId = uuidv4().substring(0, 8); // 短 ID
|
||||
|
||||
// 创建后台 Agent 记录
|
||||
this.backgroundAgents.set(agentId, {
|
||||
id: agentId,
|
||||
agentName: agentInfo.name,
|
||||
description,
|
||||
status: 'running',
|
||||
prompt,
|
||||
startedAt: new Date(),
|
||||
});
|
||||
|
||||
// 异步执行,不等待结果
|
||||
this.executeAsync(agentId, agentInfo, prompt, baseConfig, toolRegistry, context);
|
||||
|
||||
return agentId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步执行 Agent 任务
|
||||
*/
|
||||
private async executeAsync(
|
||||
agentId: string,
|
||||
agentInfo: AgentInfo,
|
||||
prompt: string,
|
||||
baseConfig: AgentConfig,
|
||||
toolRegistry: ToolRegistry,
|
||||
context: AgentExecutionContext
|
||||
): Promise<void> {
|
||||
try {
|
||||
const executor = new AgentExecutor(agentInfo, baseConfig, toolRegistry);
|
||||
const result = await executor.execute(prompt, {
|
||||
...context,
|
||||
onStream: undefined, // 后台运行不使用流式输出
|
||||
});
|
||||
|
||||
// 更新状态为完成
|
||||
const agent = this.backgroundAgents.get(agentId);
|
||||
if (agent) {
|
||||
agent.status = result.success ? 'completed' : 'failed';
|
||||
agent.completedAt = new Date();
|
||||
agent.result = result.text;
|
||||
agent.steps = result.steps;
|
||||
if (!result.success) {
|
||||
agent.error = result.error;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// 更新状态为失败
|
||||
const agent = this.backgroundAgents.get(agentId);
|
||||
if (agent) {
|
||||
agent.status = 'failed';
|
||||
agent.completedAt = new Date();
|
||||
agent.error = error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
}
|
||||
|
||||
// 触发等待回调
|
||||
this.notifyCompletion(agentId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取后台 Agent 状态
|
||||
*/
|
||||
getAgent(agentId: string): BackgroundAgent | null {
|
||||
return this.backgroundAgents.get(agentId) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Agent 输出(支持阻塞等待)
|
||||
*/
|
||||
async getAgentOutput(
|
||||
agentId: string,
|
||||
block: boolean = true,
|
||||
timeoutSeconds: number = 150
|
||||
): Promise<BackgroundAgent | null> {
|
||||
const agent = this.backgroundAgents.get(agentId);
|
||||
if (!agent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 如果已完成或不需要阻塞,直接返回
|
||||
if (agent.status !== 'running' || !block) {
|
||||
return agent;
|
||||
}
|
||||
|
||||
// 阻塞等待完成
|
||||
return this.waitForCompletion(agentId, timeoutSeconds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待 Agent 完成
|
||||
*/
|
||||
private waitForCompletion(
|
||||
agentId: string,
|
||||
timeoutSeconds: number
|
||||
): Promise<BackgroundAgent | null> {
|
||||
return new Promise((resolve) => {
|
||||
const agent = this.backgroundAgents.get(agentId);
|
||||
if (!agent || agent.status !== 'running') {
|
||||
resolve(agent || null);
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置超时
|
||||
const timeoutId = setTimeout(() => {
|
||||
// 移除回调
|
||||
const callbacks = this.completionCallbacks.get(agentId);
|
||||
if (callbacks) {
|
||||
const index = callbacks.indexOf(callback);
|
||||
if (index > -1) {
|
||||
callbacks.splice(index, 1);
|
||||
}
|
||||
}
|
||||
resolve(this.backgroundAgents.get(agentId) || null);
|
||||
}, timeoutSeconds * 1000);
|
||||
|
||||
// 注册完成回调
|
||||
const callback = () => {
|
||||
clearTimeout(timeoutId);
|
||||
resolve(this.backgroundAgents.get(agentId) || null);
|
||||
};
|
||||
|
||||
if (!this.completionCallbacks.has(agentId)) {
|
||||
this.completionCallbacks.set(agentId, []);
|
||||
}
|
||||
this.completionCallbacks.get(agentId)!.push(callback);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知等待者任务已完成
|
||||
*/
|
||||
private notifyCompletion(agentId: string): void {
|
||||
const callbacks = this.completionCallbacks.get(agentId);
|
||||
if (callbacks) {
|
||||
for (const callback of callbacks) {
|
||||
callback();
|
||||
}
|
||||
this.completionCallbacks.delete(agentId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出所有后台 Agent
|
||||
*/
|
||||
listAgents(): BackgroundAgent[] {
|
||||
return Array.from(this.backgroundAgents.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出运行中的 Agent
|
||||
*/
|
||||
listRunningAgents(): BackgroundAgent[] {
|
||||
return this.listAgents().filter((a) => a.status === 'running');
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理已完成的 Agent 记录
|
||||
*/
|
||||
cleanup(maxAge: number = 3600000): void {
|
||||
const now = Date.now();
|
||||
for (const [id, agent] of this.backgroundAgents) {
|
||||
if (
|
||||
agent.status !== 'running' &&
|
||||
agent.completedAt &&
|
||||
now - agent.completedAt.getTime() > maxAge
|
||||
) {
|
||||
this.backgroundAgents.delete(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 单例实例
|
||||
let agentManager: AgentManager | null = null;
|
||||
|
||||
/**
|
||||
* 获取 AgentManager 单例
|
||||
*/
|
||||
export function getAgentManager(): AgentManager {
|
||||
if (!agentManager) {
|
||||
agentManager = new AgentManager();
|
||||
}
|
||||
return agentManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置 AgentManager(测试用)
|
||||
*/
|
||||
export function resetAgentManager(): void {
|
||||
agentManager = null;
|
||||
}
|
||||
+2
-1
@@ -9,7 +9,7 @@ import { toolSearchTool } from './tool-search.js';
|
||||
import { todoReadTool, todoWriteTool } from './todo/index.js';
|
||||
|
||||
// Task 工具(Agent 子任务)
|
||||
import { taskTool } from './task/index.js';
|
||||
import { taskTool, agentOutputTool } from './task/index.js';
|
||||
|
||||
// 文件系统工具
|
||||
import {
|
||||
@@ -51,6 +51,7 @@ const allToolsWithMetadata: ToolWithMetadata[] = [
|
||||
todoReadTool,
|
||||
todoWriteTool,
|
||||
taskTool,
|
||||
agentOutputTool,
|
||||
|
||||
// 文件系统工具 (deferLoading: true)
|
||||
readFileTool,
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import type { ToolWithMetadata } from '../types.js';
|
||||
import { getAgentManager } from '../../agent/manager.js';
|
||||
|
||||
/**
|
||||
* Agent Output 工具
|
||||
* 用于获取后台 Agent 的执行结果
|
||||
*/
|
||||
export const agentOutputTool: ToolWithMetadata = {
|
||||
name: 'agent_output',
|
||||
description: `获取后台运行的 Agent 执行结果。
|
||||
|
||||
当使用 task 工具的 run_in_background 参数启动后台 Agent 后,
|
||||
使用此工具查询执行状态和结果。
|
||||
|
||||
使用示例:
|
||||
- 查询结果(阻塞等待): agent_output({ agent_id: "abc123" })
|
||||
- 检查状态(不阻塞): agent_output({ agent_id: "abc123", block: false })
|
||||
- 设置超时: agent_output({ agent_id: "abc123", timeout: 60 })`,
|
||||
parameters: {
|
||||
agent_id: {
|
||||
type: 'string',
|
||||
description: 'Agent ID(由 task 工具返回)',
|
||||
required: true,
|
||||
},
|
||||
block: {
|
||||
type: 'boolean',
|
||||
description: '是否阻塞等待结果完成(默认 true)。设为 false 可立即获取当前状态',
|
||||
required: false,
|
||||
},
|
||||
timeout: {
|
||||
type: 'number',
|
||||
description: '等待超时时间(秒),默认 150,最大 300',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
name: 'agent_output',
|
||||
category: 'agent',
|
||||
description: '获取后台 Agent 执行结果',
|
||||
keywords: ['agent', 'output', 'result', 'background', '结果', '后台', '查询'],
|
||||
deferLoading: false, // 核心工具,始终加载
|
||||
},
|
||||
async execute(params) {
|
||||
const {
|
||||
agent_id,
|
||||
block = true,
|
||||
timeout = 150,
|
||||
} = params as {
|
||||
agent_id: string;
|
||||
block?: boolean;
|
||||
timeout?: number;
|
||||
};
|
||||
|
||||
const agentManager = getAgentManager();
|
||||
|
||||
// 验证超时参数
|
||||
const effectiveTimeout = Math.min(Math.max(timeout, 1), 300);
|
||||
|
||||
// 获取 Agent 输出
|
||||
const agent = await agentManager.getAgentOutput(agent_id, block, effectiveTimeout);
|
||||
|
||||
if (!agent) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `Agent ${agent_id} 不存在。请检查 agent_id 是否正确。`,
|
||||
};
|
||||
}
|
||||
|
||||
// 根据状态返回不同结果
|
||||
if (agent.status === 'running') {
|
||||
const runningTime = Math.round((Date.now() - agent.startedAt.getTime()) / 1000);
|
||||
return {
|
||||
success: true,
|
||||
output: `Agent ${agent_id} 仍在运行中...\n` +
|
||||
`- 类型: ${agent.agentName}\n` +
|
||||
`- 任务: ${agent.description}\n` +
|
||||
`- 已运行: ${runningTime} 秒\n\n` +
|
||||
`稍后再次调用 agent_output 查询结果,或使用 block: true 等待完成。`,
|
||||
metadata: {
|
||||
agentId: agent.id,
|
||||
status: 'running',
|
||||
agentName: agent.agentName,
|
||||
runningTime,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (agent.status === 'failed') {
|
||||
const duration = agent.completedAt
|
||||
? Math.round((agent.completedAt.getTime() - agent.startedAt.getTime()) / 1000)
|
||||
: 0;
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `Agent ${agent_id} 执行失败:\n` +
|
||||
`- 类型: ${agent.agentName}\n` +
|
||||
`- 任务: ${agent.description}\n` +
|
||||
`- 耗时: ${duration} 秒\n` +
|
||||
`- 错误: ${agent.error || '未知错误'}`,
|
||||
metadata: {
|
||||
agentId: agent.id,
|
||||
status: 'failed',
|
||||
agentName: agent.agentName,
|
||||
duration,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// completed
|
||||
const duration = agent.completedAt
|
||||
? Math.round((agent.completedAt.getTime() - agent.startedAt.getTime()) / 1000)
|
||||
: 0;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: `## Agent ${agent_id} 执行完成\n\n` +
|
||||
`- 类型: ${agent.agentName}\n` +
|
||||
`- 任务: ${agent.description}\n` +
|
||||
`- 耗时: ${duration} 秒\n` +
|
||||
`- 步数: ${agent.steps || 0}\n\n` +
|
||||
`### 结果\n\n${agent.result || '(无输出)'}`,
|
||||
metadata: {
|
||||
agentId: agent.id,
|
||||
status: 'completed',
|
||||
agentName: agent.agentName,
|
||||
duration,
|
||||
steps: agent.steps,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -1 +1,2 @@
|
||||
export { taskTool, initTaskContext, updateTaskDescription } from './task.js';
|
||||
export { taskTool, initTaskContext, updateTaskDescription, getTaskContext } from './task.js';
|
||||
export { agentOutputTool } from './agent_output.js';
|
||||
|
||||
+94
-11
@@ -3,6 +3,16 @@ import type { AgentConfig } from '../../types/index.js';
|
||||
import { agentRegistry, AgentExecutor } from '../../agent/index.js';
|
||||
import { toolRegistry } from '../registry.js';
|
||||
import { SessionManager } from '../../session/index.js';
|
||||
import { getAgentManager } from '../../agent/manager.js';
|
||||
|
||||
/**
|
||||
* 模型预设映射
|
||||
*/
|
||||
const MODEL_PRESETS: Record<string, string> = {
|
||||
sonnet: 'claude-sonnet-4-20250514',
|
||||
opus: 'claude-opus-4-0-20250514',
|
||||
haiku: 'claude-3-5-haiku-20241022',
|
||||
};
|
||||
|
||||
// Task 工具上下文(运行时注入)
|
||||
let taskContext: {
|
||||
@@ -20,6 +30,13 @@ export function initTaskContext(
|
||||
taskContext = { baseConfig, sessionManager };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Task 工具上下文
|
||||
*/
|
||||
export function getTaskContext(): typeof taskContext {
|
||||
return taskContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Task 工具动态描述
|
||||
*/
|
||||
@@ -33,20 +50,21 @@ function getTaskDescription(): string {
|
||||
.map((a) => `- ${a.name}: ${a.description}`)
|
||||
.join('\n');
|
||||
|
||||
return `执行子任务,委派给专门的 Agent 处理。
|
||||
return `启动子 Agent 执行复杂任务,支持后台运行和模型选择。
|
||||
|
||||
可用的 Agent:
|
||||
${agentList}
|
||||
|
||||
使用示例:
|
||||
- 使用 explore Agent 搜索代码: task({ subagent_type: "explore", prompt: "找到所有 API 路由定义" })
|
||||
- 使用 code-reviewer Agent 审查: task({ subagent_type: "code-reviewer", prompt: "审查 src/auth 目录的代码" })
|
||||
- 使用 general Agent 执行复杂任务: task({ subagent_type: "general", prompt: "分析并重构这个函数" })`;
|
||||
- 同步执行: task({ subagent_type: "explore", prompt: "找到所有 API 路由" })
|
||||
- 后台运行: task({ subagent_type: "code-reviewer", prompt: "审查代码", run_in_background: true })
|
||||
- 指定模型: task({ subagent_type: "general", prompt: "复杂分析", model: "opus" })`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Task 工具
|
||||
* 用于创建子任务,委派给指定的 Agent 处理
|
||||
* 支持后台运行和模型选择
|
||||
*/
|
||||
export const taskTool: ToolWithMetadata = {
|
||||
name: 'task',
|
||||
@@ -67,19 +85,37 @@ export const taskTool: ToolWithMetadata = {
|
||||
description: '子 Agent 类型,可选: general, explore, code-reviewer',
|
||||
required: true,
|
||||
},
|
||||
model: {
|
||||
type: 'string',
|
||||
description: '模型选择: sonnet, opus, haiku(默认继承主 Agent 配置)',
|
||||
required: false,
|
||||
},
|
||||
run_in_background: {
|
||||
type: 'boolean',
|
||||
description: '是否后台运行。后台运行时立即返回 agentId,使用 agent_output 工具获取结果',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
name: 'task',
|
||||
category: 'agent',
|
||||
description: '执行子任务,委派给专门的 Agent',
|
||||
keywords: ['task', 'agent', 'subagent', '子任务', '委派', '探索', '审查'],
|
||||
keywords: ['task', 'agent', 'subagent', '子任务', '委派', '探索', '审查', '后台'],
|
||||
deferLoading: false, // 核心工具,始终加载
|
||||
},
|
||||
async execute(params) {
|
||||
const { description, prompt, subagent_type } = params as {
|
||||
const {
|
||||
description,
|
||||
prompt,
|
||||
subagent_type,
|
||||
model,
|
||||
run_in_background,
|
||||
} = params as {
|
||||
description: string;
|
||||
prompt: string;
|
||||
subagent_type: string;
|
||||
model?: string;
|
||||
run_in_background?: boolean;
|
||||
};
|
||||
|
||||
// 检查上下文是否已初始化
|
||||
@@ -113,7 +149,52 @@ export const taskTool: ToolWithMetadata = {
|
||||
};
|
||||
}
|
||||
|
||||
// 2. 创建子会话
|
||||
// 2. 处理模型选择
|
||||
let effectiveConfig = baseConfig;
|
||||
if (model) {
|
||||
const modelName = MODEL_PRESETS[model];
|
||||
if (!modelName) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `无效的模型选择: ${model}。可选: sonnet, opus, haiku`,
|
||||
};
|
||||
}
|
||||
effectiveConfig = {
|
||||
...baseConfig,
|
||||
model: modelName,
|
||||
};
|
||||
}
|
||||
|
||||
// 3. 后台运行模式
|
||||
if (run_in_background) {
|
||||
const agentManager = getAgentManager();
|
||||
const parentSessionId = sessionManager.getSessionId() || 'standalone';
|
||||
|
||||
const agentId = await agentManager.runInBackground(
|
||||
agent,
|
||||
description,
|
||||
prompt,
|
||||
effectiveConfig,
|
||||
toolRegistry,
|
||||
{
|
||||
parentSessionId,
|
||||
workdir: process.cwd(),
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: `Agent ${agentId} 已在后台启动 (@${agent.name})。\n使用 agent_output 工具查询结果: agent_output({ agent_id: "${agentId}" })`,
|
||||
metadata: {
|
||||
agentId,
|
||||
agent: agent.name,
|
||||
mode: 'background',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// 4. 同步执行模式
|
||||
const parentSessionId = sessionManager.getSessionId() || 'standalone';
|
||||
const childSession = sessionManager.createChildSession(
|
||||
parentSessionId,
|
||||
@@ -121,17 +202,17 @@ export const taskTool: ToolWithMetadata = {
|
||||
`${description} (@${agent.name})`
|
||||
);
|
||||
|
||||
// 3. 创建执行器
|
||||
const executor = new AgentExecutor(agent, baseConfig, toolRegistry);
|
||||
// 创建执行器
|
||||
const executor = new AgentExecutor(agent, effectiveConfig, toolRegistry);
|
||||
|
||||
// 4. 执行任务
|
||||
// 执行任务
|
||||
const result = await executor.execute(prompt, {
|
||||
parentSessionId,
|
||||
workdir: process.cwd(),
|
||||
onStream: undefined, // 子任务不使用流式输出
|
||||
});
|
||||
|
||||
// 5. 保存子会话
|
||||
// 保存子会话
|
||||
childSession.messages = [
|
||||
{ role: 'user', content: prompt },
|
||||
{ role: 'assistant', content: result.text },
|
||||
@@ -146,6 +227,7 @@ export const taskTool: ToolWithMetadata = {
|
||||
agent: agent.name,
|
||||
sessionId: childSession.id,
|
||||
steps: result.steps,
|
||||
mode: 'sync',
|
||||
},
|
||||
};
|
||||
} else {
|
||||
@@ -157,6 +239,7 @@ export const taskTool: ToolWithMetadata = {
|
||||
agent: agent.name,
|
||||
sessionId: childSession.id,
|
||||
steps: result.steps,
|
||||
mode: 'sync',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user