feat: 添加后台 Agent 执行和模型选择功能

- 新增 AgentManager 管理后台 Agent 生命周期
- Task 工具支持 run_in_background 参数实现后台执行
- Task 工具支持 model 参数选择 sonnet/opus/haiku 模型
- 新增 agent_output 工具查询后台 Agent 执行状态和结果
- 添加 AgentManager 和 AgentOutput 工具单元测试
This commit is contained in:
2025-12-11 15:46:30 +08:00
parent 729fb2d42a
commit ad5d30b262
10 changed files with 927 additions and 13 deletions
+255
View File
@@ -0,0 +1,255 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { AgentManager, resetAgentManager, getAgentManager } from '../../../src/agent/manager.js';
import type { AgentInfo } from '../../../src/agent/types.js';
import type { AgentConfig } from '../../../src/types/index.js';
// Mock AgentExecutor - 使用延迟执行
vi.mock('../../../src/agent/executor.js', () => ({
AgentExecutor: vi.fn().mockImplementation(() => ({
execute: vi.fn().mockImplementation(async () => {
// 延迟 200ms,让测试能在执行完成前检查状态
await new Promise((r) => setTimeout(r, 200));
return {
success: true,
text: '任务完成',
steps: 3,
sessionId: 'test-session',
};
}),
})),
}));
describe('AgentManager - Agent 管理器', () => {
let manager: AgentManager;
const mockAgentInfo: AgentInfo = {
name: 'test-agent',
description: '测试 Agent',
mode: 'subagent',
prompt: '你是一个测试助手',
};
const mockConfig: AgentConfig = {
provider: 'anthropic',
apiKey: 'test-key',
model: 'claude-3-5-sonnet-20241022',
maxTokens: 4096,
systemPrompt: '测试系统提示词',
};
const mockContext = {
parentSessionId: 'parent-123',
workdir: '/test',
};
beforeEach(() => {
vi.clearAllMocks();
resetAgentManager();
manager = new AgentManager();
});
describe('getAgentManager - 单例获取', () => {
it('返回相同的实例', () => {
resetAgentManager();
const instance1 = getAgentManager();
const instance2 = getAgentManager();
expect(instance1).toBe(instance2);
});
it('resetAgentManager 重置实例', () => {
const instance1 = getAgentManager();
resetAgentManager();
const instance2 = getAgentManager();
expect(instance1).not.toBe(instance2);
});
});
describe('runInBackground - 后台运行', () => {
it('返回 agent ID', async () => {
const agentId = await manager.runInBackground(
mockAgentInfo,
'测试任务',
'执行测试',
mockConfig,
{} as any,
mockContext
);
expect(agentId).toBeDefined();
expect(typeof agentId).toBe('string');
expect(agentId.length).toBe(8); // 短 ID
});
it('创建初始状态的记录', async () => {
const agentId = await manager.runInBackground(
mockAgentInfo,
'测试任务',
'执行测试',
mockConfig,
{} as any,
mockContext
);
const agent = manager.getAgent(agentId);
expect(agent).not.toBeNull();
expect(agent!.agentName).toBe('test-agent');
expect(agent!.description).toBe('测试任务');
expect(agent!.prompt).toBe('执行测试');
expect(agent!.startedAt).toBeInstanceOf(Date);
// 由于是异步执行,初始状态应该是 running
// 但由于测试环境的原因,可能已经变成其他状态
expect(['running', 'completed', 'failed']).toContain(agent!.status);
});
});
describe('getAgent - 获取 Agent', () => {
it('返回存在的 Agent', async () => {
const agentId = await manager.runInBackground(
mockAgentInfo,
'测试任务',
'执行测试',
mockConfig,
{} as any,
mockContext
);
const agent = manager.getAgent(agentId);
expect(agent).not.toBeNull();
expect(agent!.id).toBe(agentId);
});
it('返回 null 当 Agent 不存在', () => {
const agent = manager.getAgent('non-existent');
expect(agent).toBeNull();
});
});
describe('getAgentOutput - 获取输出', () => {
it('返回 null 当 Agent 不存在', async () => {
const result = await manager.getAgentOutput('non-existent', false);
expect(result).toBeNull();
});
it('非阻塞模式立即返回状态', async () => {
const agentId = await manager.runInBackground(
mockAgentInfo,
'测试任务',
'执行测试',
mockConfig,
{} as any,
mockContext
);
const result = await manager.getAgentOutput(agentId, false);
expect(result).not.toBeNull();
expect(result!.id).toBe(agentId);
});
it('阻塞模式等待完成', async () => {
const agentId = await manager.runInBackground(
mockAgentInfo,
'测试任务',
'执行测试',
mockConfig,
{} as any,
mockContext
);
// 使用阻塞模式等待
const result = await manager.getAgentOutput(agentId, true, 5);
expect(result).not.toBeNull();
// 等待后应该是完成状态
expect(['completed', 'failed']).toContain(result!.status);
});
});
describe('listAgents - 列出所有 Agent', () => {
it('返回所有 Agent', async () => {
await manager.runInBackground(
mockAgentInfo,
'任务1',
'执行1',
mockConfig,
{} as any,
mockContext
);
await manager.runInBackground(
mockAgentInfo,
'任务2',
'执行2',
mockConfig,
{} as any,
mockContext
);
const agents = manager.listAgents();
expect(agents.length).toBe(2);
});
it('空管理器返回空数组', () => {
const agents = manager.listAgents();
expect(agents).toEqual([]);
});
});
describe('listRunningAgents - 列出运行中的 Agent', () => {
it('返回数组', async () => {
await manager.runInBackground(
mockAgentInfo,
'任务1',
'执行1',
mockConfig,
{} as any,
mockContext
);
const running = manager.listRunningAgents();
expect(Array.isArray(running)).toBe(true);
});
});
describe('cleanup - 清理', () => {
it('清理已完成的 Agent', async () => {
const agentId = await manager.runInBackground(
mockAgentInfo,
'任务',
'执行',
mockConfig,
{} as any,
mockContext
);
// 等待一段时间让 Agent 完成(无论成功或失败)
await new Promise((r) => setTimeout(r, 100));
// 确认 Agent 不再是 running 状态
const beforeCleanup = manager.getAgent(agentId);
expect(beforeCleanup).not.toBeNull();
expect(['completed', 'failed']).toContain(beforeCleanup?.status);
// 设置 maxAge 为 0,立即清理
manager.cleanup(0);
const agent = manager.getAgent(agentId);
expect(agent).toBeNull();
});
it('不清理运行中的 Agent', async () => {
const agentId = await manager.runInBackground(
mockAgentInfo,
'任务',
'执行',
mockConfig,
{} as any,
mockContext
);
// 立即清理(不等待)
manager.cleanup(0);
// cleanup 应该不会报错
// Agent 可能还在运行(取决于执行速度)
// 这里只验证 cleanup 不会崩溃
expect(true).toBe(true);
});
});
});
+160
View File
@@ -0,0 +1,160 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { agentOutputTool } from '../../../../src/tools/task/agent_output.js';
import { getAgentManager, resetAgentManager } from '../../../../src/agent/manager.js';
// Mock AgentExecutor - 使用延迟执行让测试更可控
vi.mock('../../../../src/agent/executor.js', () => ({
AgentExecutor: vi.fn().mockImplementation(() => ({
execute: vi.fn().mockImplementation(async () => {
await new Promise((r) => setTimeout(r, 50));
return {
success: true,
text: '任务完成结果',
steps: 3,
sessionId: 'test-session',
};
}),
})),
}));
describe('agentOutputTool - Agent 输出工具', () => {
beforeEach(() => {
vi.clearAllMocks();
resetAgentManager();
});
describe('工具定义', () => {
it('有正确的名称', () => {
expect(agentOutputTool.name).toBe('agent_output');
});
it('有正确的元数据', () => {
expect(agentOutputTool.metadata.category).toBe('agent');
expect(agentOutputTool.metadata.keywords).toContain('agent');
expect(agentOutputTool.metadata.keywords).toContain('output');
expect(agentOutputTool.metadata.keywords).toContain('background');
});
it('agent_id 参数是必须的', () => {
expect(agentOutputTool.parameters.agent_id.required).toBe(true);
});
it('block 和 timeout 参数是可选的', () => {
expect(agentOutputTool.parameters.block.required).toBe(false);
expect(agentOutputTool.parameters.timeout.required).toBe(false);
});
});
describe('execute - 执行', () => {
it('Agent 不存在时返回错误', async () => {
const result = await agentOutputTool.execute({
agent_id: 'non-existent-id',
});
expect(result.success).toBe(false);
expect(result.error).toContain('不存在');
});
it('返回 Agent 的状态', async () => {
const manager = getAgentManager();
const agentId = await manager.runInBackground(
{
name: 'test-agent',
description: '测试',
mode: 'subagent',
},
'测试任务',
'执行测试',
{
provider: 'anthropic',
apiKey: 'test',
model: 'test',
maxTokens: 1000,
systemPrompt: 'test',
},
{} as any,
{ parentSessionId: 'parent', workdir: '/test' }
);
// 立即查询(不阻塞)
const result = await agentOutputTool.execute({
agent_id: agentId,
block: false,
});
// 应该成功返回状态(可能是 running 或 completed
expect(result.output).toBeDefined();
expect(result.metadata?.agentId).toBe(agentId);
});
it('阻塞等待后返回结果', async () => {
const manager = getAgentManager();
const agentId = await manager.runInBackground(
{
name: 'test-agent',
description: '测试',
mode: 'subagent',
},
'测试任务',
'执行测试',
{
provider: 'anthropic',
apiKey: 'test',
model: 'test',
maxTokens: 1000,
systemPrompt: 'test',
},
{} as any,
{ parentSessionId: 'parent', workdir: '/test' }
);
// 使用阻塞模式等待
const result = await agentOutputTool.execute({
agent_id: agentId,
block: true,
timeout: 5,
});
// 等待后应该有确定的状态
expect(result.metadata?.status).toBeDefined();
expect(['running', 'completed', 'failed']).toContain(result.metadata?.status);
});
it('返回包含正确字段的输出', async () => {
const manager = getAgentManager();
const agentId = await manager.runInBackground(
{
name: 'test-agent',
description: '测试',
mode: 'subagent',
},
'测试任务',
'执行测试',
{
provider: 'anthropic',
apiKey: 'test',
model: 'test',
maxTokens: 1000,
systemPrompt: 'test',
},
{} as any,
{ parentSessionId: 'parent', workdir: '/test' }
);
// 等待完成
await new Promise((r) => setTimeout(r, 200));
const result = await agentOutputTool.execute({
agent_id: agentId,
block: false,
});
// 检查返回了有效结果
expect(result.output).toBeDefined();
expect(result.metadata?.agentId).toBe(agentId);
expect(result.metadata?.agentName).toBe('test-agent');
// 状态应该是完成或失败(由于 mock,可能会失败)
expect(['completed', 'failed']).toContain(result.metadata?.status);
});
});
});