From 2d03c5f3487a7450ad83cef5d4d5bc6f87f7532f Mon Sep 17 00:00:00 2001 From: thzxx Date: Fri, 5 Jun 2026 10:50:14 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AD=90=E4=BB=A3=E7=90=86=E5=A2=9E=E5=BC=BA?= =?UTF-8?q?=EF=BC=9A=E6=94=AF=E6=8C=81=E5=8F=AA=E8=AF=BB=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E8=B0=83=E7=94=A8=20+=20=E5=8F=AF=E9=85=8D=E7=BD=AE=E5=8F=82?= =?UTF-8?q?=E6=95=B0=20+=2010=E8=BD=AE/5=E5=88=86=E9=92=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 15 个只读工具可用:文件读取、搜索、浏览器查看、记忆/会话/技能查询 - 内置 mini Agent Loop,独立执行工具调用链 - 可配置 maxLoops / timeout / model - agent-engine 批次并行已天然支持多个 spawn_task 并行 --- src/renderer/services/sub-agent.ts | 168 ++++++++++++++++++----------- 1 file changed, 104 insertions(+), 64 deletions(-) diff --git a/src/renderer/services/sub-agent.ts b/src/renderer/services/sub-agent.ts index cc4694a..ef34ea7 100644 --- a/src/renderer/services/sub-agent.ts +++ b/src/renderer/services/sub-agent.ts @@ -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 { +export async function executeSubAgent( + task: string, + context?: string, + options: SubAgentOptions = {} +): Promise { const api = state.get(KEYS.API); - const model = state.get('_defaultModel', ''); + const model = options.model || state.get('_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 }> = []; + let content = ''; + let toolCalls: Array<{ name: string; arguments: Record }> = []; + 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 }); } }