子代理增强:支持只读工具调用 + 可配置参数 + 10轮/5分钟
- 15 个只读工具可用:文件读取、搜索、浏览器查看、记忆/会话/技能查询 - 内置 mini Agent Loop,独立执行工具调用链 - 可配置 maxLoops / timeout / model - agent-engine 批次并行已天然支持多个 spawn_task 并行
This commit is contained in:
@@ -1,112 +1,152 @@
|
||||
/**
|
||||
* SubAgent - 子代理委派系统 (v4.3)
|
||||
* 允许主 Agent 将独立任务委派给子 Agent 执行
|
||||
* 子 Agent 独立运行,完成后将结果返回给主 Agent
|
||||
* SubAgent - 子代理委派系统 (v0.9.0 增强版)
|
||||
* 子代理拥有受限工具集(只读),可独立完成调研/搜索/分析类任务
|
||||
* 主 Agent 通过 spawn_task 工具并行委派多个子代理
|
||||
*/
|
||||
|
||||
import { state, KEYS } from '../state/state.js';
|
||||
import { OllamaAPI } from '../api/ollama.js';
|
||||
import { logInfo, logWarn, logError, logAgentLoop } from './log-service.js';
|
||||
import type { ToolResult } from '../types.js';
|
||||
import { TOOL_DEFINITIONS } from './tool-registry.js';
|
||||
import { getEnabledToolDefinitions } from './tool-registry.js';
|
||||
import { logInfo, logWarn, logError } from './log-service.js';
|
||||
import type { ToolResult, ToolCall, ToolDefinition } from '../types.js';
|
||||
|
||||
const SUB_AGENT_TIMEOUT = 120000; // 2 分钟超时
|
||||
const SUB_AGENT_MAX_LOOPS = 5; // 子 Agent 最多 5 轮
|
||||
const SUB_AGENT_MAX_LOOPS = 10; // 子代理最多 10 轮
|
||||
const SUB_AGENT_TIMEOUT = 300000; // 5 分钟超时
|
||||
|
||||
/** 子代理可用工具:只读类,不能修改文件系统 */
|
||||
const SUB_AGENT_TOOL_WHITELIST = new Set([
|
||||
'read_file', 'list_directory', 'search_files',
|
||||
'web_search', 'web_fetch',
|
||||
'browser_extract', 'browser_screenshot',
|
||||
'memory_search',
|
||||
'session_list', 'session_read',
|
||||
'skill_list', 'skill_view',
|
||||
]);
|
||||
|
||||
/** 从总工具列表中筛选子代理可用的 */
|
||||
function getSubAgentTools(): ToolDefinition[] {
|
||||
return TOOL_DEFINITIONS.filter(d => SUB_AGENT_TOOL_WHITELIST.has(d.function.name));
|
||||
}
|
||||
|
||||
export interface SubAgentOptions {
|
||||
maxLoops?: number;
|
||||
timeout?: number;
|
||||
model?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行子 Agent 任务
|
||||
* 执行子代理任务
|
||||
* @param task 子任务描述
|
||||
* @param context 附加上下文(主 Agent 提供的参考信息)
|
||||
* @returns 子 Agent 的执行结果
|
||||
* @param context 附加上下文
|
||||
* @param options 可选配置
|
||||
*/
|
||||
export async function executeSubAgent(task: string, context?: string): Promise<ToolResult> {
|
||||
export async function executeSubAgent(
|
||||
task: string,
|
||||
context?: string,
|
||||
options: SubAgentOptions = {}
|
||||
): Promise<ToolResult> {
|
||||
const api = state.get<OllamaAPI>(KEYS.API);
|
||||
const model = state.get<string>('_defaultModel', '');
|
||||
const model = options.model || state.get<string>('_defaultModel', '');
|
||||
const maxLoops = options.maxLoops ?? SUB_AGENT_MAX_LOOPS;
|
||||
const timeout = options.timeout ?? SUB_AGENT_TIMEOUT;
|
||||
|
||||
if (!api || !model) {
|
||||
return { success: false, error: '未选择模型,无法执行子任务' };
|
||||
}
|
||||
|
||||
logInfo(`子 Agent 启动`, `任务: ${task.slice(0, 80)}`);
|
||||
const tools = getSubAgentTools();
|
||||
const toolNames = [...SUB_AGENT_TOOL_WHITELIST].join(', ');
|
||||
logInfo(`子 Agent 启动`, `任务: ${task.slice(0, 80)} | 工具: ${toolNames}`);
|
||||
|
||||
const systemPrompt = `你是一个子任务执行 Agent。你的任务是高效完成指定的单一任务,然后给出简洁的结果报告。
|
||||
不要闲聊,不要说废话。直接执行任务,完成后给出结果。
|
||||
const systemPrompt = `你是一个子任务执行 Agent,拥有只读工具权限。请高效完成指定任务,给出结果报告。
|
||||
${context ? `\n附加上下文:\n${context}` : ''}`;
|
||||
|
||||
const messages: Array<{ role: string; content: string }> = [
|
||||
const messages: Array<{ role: string; content: string; tool_calls?: ToolCall[] }> = [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: task }
|
||||
];
|
||||
|
||||
let resultContent = '';
|
||||
let loopCount = 0;
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
while (loopCount < SUB_AGENT_MAX_LOOPS) {
|
||||
loopCount++;
|
||||
while (loopCount < maxLoops) {
|
||||
loopCount++;
|
||||
|
||||
if (Date.now() - startTime > SUB_AGENT_TIMEOUT) {
|
||||
logWarn('子 Agent 超时', `${loopCount} 轮`);
|
||||
break;
|
||||
}
|
||||
if (Date.now() - startTime > timeout) {
|
||||
logWarn('子 Agent 超时', `${loopCount} 轮`);
|
||||
return { success: true, content: '子任务执行超时', loops: loopCount, duration: Date.now() - startTime, partial: true };
|
||||
}
|
||||
|
||||
let content = '';
|
||||
let toolCalls: Array<{ name: string; arguments: Record<string, unknown> }> = [];
|
||||
let content = '';
|
||||
let toolCalls: Array<{ name: string; arguments: Record<string, unknown> }> = [];
|
||||
|
||||
try {
|
||||
await api.chatStream({
|
||||
model,
|
||||
messages,
|
||||
stream: true,
|
||||
think: false,
|
||||
tools: tools as any,
|
||||
options: { num_ctx: 8192, temperature: 0.3 }
|
||||
} as any, (chunk: any) => {
|
||||
if (chunk.message?.content) content += chunk.message.content;
|
||||
if (chunk.message?.tool_calls?.length) {
|
||||
for (const tc of chunk.message.tool_calls) {
|
||||
if (tc.function?.name) {
|
||||
if (tc.function?.name && SUB_AGENT_TOOL_WHITELIST.has(tc.function.name)) {
|
||||
toolCalls.push({ name: tc.function.name, arguments: tc.function.arguments || {} });
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
resultContent = content;
|
||||
|
||||
if (toolCalls.length === 0) {
|
||||
// 子 Agent 完成
|
||||
logInfo(`子 Agent 完成`, `${loopCount} 轮, ${resultContent.length} 字`);
|
||||
return {
|
||||
success: true,
|
||||
content: resultContent,
|
||||
loops: loopCount,
|
||||
duration: Date.now() - startTime
|
||||
};
|
||||
}
|
||||
|
||||
// 子 Agent 调用了工具(不支持,提示直接回答)
|
||||
messages.push({ role: 'assistant', content });
|
||||
messages.push({
|
||||
role: 'user',
|
||||
content: '你无法调用工具。请基于已有信息直接给出回答。如果需要工具才能完成,请说明你无法完成此任务及原因。'
|
||||
});
|
||||
} catch (err) {
|
||||
logError('子 Agent 调用失败', (err as Error).message);
|
||||
return { success: false, error: (err as Error).message, loops: loopCount, duration: Date.now() - startTime };
|
||||
}
|
||||
|
||||
// 达到最大轮次
|
||||
logWarn('子 Agent 达到最大轮次', `${loopCount} 轮`);
|
||||
return {
|
||||
success: true,
|
||||
content: resultContent || '(子任务执行超时)',
|
||||
loops: loopCount,
|
||||
duration: Date.now() - startTime,
|
||||
partial: true
|
||||
};
|
||||
} catch (err) {
|
||||
logError('子 Agent 错误', (err as Error).message);
|
||||
return {
|
||||
success: false,
|
||||
error: (err as Error).message,
|
||||
loops: loopCount,
|
||||
duration: Date.now() - startTime
|
||||
};
|
||||
// 无工具调用 → 完成
|
||||
if (toolCalls.length === 0) {
|
||||
logInfo(`子 Agent 完成`, `${loopCount} 轮, ${content.length} 字`);
|
||||
return { success: true, content, loops: loopCount, duration: Date.now() - startTime };
|
||||
}
|
||||
|
||||
// 执行工具
|
||||
messages.push({ role: 'assistant', content, tool_calls: toolCalls.map(tc => ({
|
||||
type: 'function' as const,
|
||||
function: { name: tc.name, arguments: tc.arguments }
|
||||
})) });
|
||||
|
||||
for (const tc of toolCalls) {
|
||||
try {
|
||||
const { executeTool } = await import('./tool-registry.js');
|
||||
const result = await executeTool(tc.name, tc.arguments);
|
||||
const resultStr = formatResult(tc.name, result);
|
||||
messages.push({
|
||||
role: 'tool',
|
||||
content: resultStr,
|
||||
tool_name: tc.name
|
||||
});
|
||||
} catch (err) {
|
||||
messages.push({
|
||||
role: 'tool',
|
||||
content: JSON.stringify({ success: false, error: (err as Error).message }),
|
||||
tool_name: tc.name
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logWarn('子 Agent 达到最大轮次', `${loopCount} 轮`);
|
||||
return { success: true, content: '达到最大轮次限制', loops: loopCount, duration: Date.now() - startTime, partial: true };
|
||||
}
|
||||
|
||||
/** 简洁格式化工具结果给子代理 */
|
||||
function formatResult(name: string, r: ToolResult): string {
|
||||
if (!r.success) return JSON.stringify({ success: false, error: r.error });
|
||||
switch (name) {
|
||||
case 'web_search': return JSON.stringify({ success: true, query: r.query, total: r.total, results: (r.results as any[])?.slice(0, 3) });
|
||||
case 'web_fetch': return JSON.stringify({ success: true, url: r.url, content: String(r.content || '').slice(0, 3000) });
|
||||
case 'read_file': return JSON.stringify({ success: true, path: r.path, content: String(r.content || '').slice(0, 3000) });
|
||||
default: return JSON.stringify({ success: true, ...r });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user