/** * Agent Engine - ReAct Agent Loop 核心引擎 (v4.0) * ReAct 模式: Thought → Action → Observation → Reflection * 支持任务复杂度评估、自动规划、错误重试、执行轨迹记录 */ import { OllamaAPI } from '../api/ollama.js'; import { state, KEYS } from '../state/state.js'; import { executeTool, getEnabledToolDefinitions, needsConfirmation } from './tool-registry.js'; import { searchMemories, buildMemoryContext, markMemoryUsed, isMemoryEnabled } from './memory-manager.js'; import { extractSkillsFromToolRecords, matchSkills, buildSkillContext } from './skill-manager.js'; import { PERSONALITY_SYSTEM_PROMPTS } from '../components/settings-modal.js'; import { showToast } from '../components/toast.js'; import { logInfo, logWarn, logError, logToolStart, logToolResult, logThink, logAgentLoop, logModelResponse } from './log-service.js'; import { getWorkspaceDirPath } from '../components/workspace-panel.js'; import { generateId } from '../utils/utils.js'; import type { OllamaMessage, OllamaStreamChunk, ToolCall, ToolResult, ToolCallRecord, TraceEntry } from '../types.js'; const MAX_LOOPS = 15; const MAX_LOOP_TIME = 600000; // 10 分钟总超时 const TOOL_EXEC_TIMEOUT = 30000; // 工具执行超时 30 秒 const STREAM_TIMEOUT = 120000; // 单次流式调用超时 2 分钟 const MAX_RETRIES = 2; // 工具错误自动重试次数 /** v4.1: 工具并行执行 — 依赖检测用 */ const TOOLS_WITH_DATA_DEPS = new Set(['web_fetch', 'edit_file', 'append_file', 'move_file', 'delete_file']); const AGENT_SYSTEM_PROMPT = `你是一个具备工具调用能力的 AI 助手。你有 25 个工具可以调用,包括文件操作、联网搜索、网页抓取、命令执行、记忆管理等。 ## 核心规则 1. 直接调用工具,不要写"我来帮你搜索"然后结束。有工具就调用,调用完根据结果继续下一步。 2. 多步任务必须逐步完成:搜到结果 → 抓取详情 → 给出答案。不要在中间步骤停下。 3. 需要最新信息(版本号、新闻、日期等)时,必须先用工具获取,不要凭记忆猜测。 4. 工具出错时分析原因并换方法重试,最多重试 2 次。 5. 不要重复调用相同参数的同一工具。 ## 多工具协同 你可以在一轮回复中调用多个工具(并行调用),也可以在一个工具的结果基础上继续调用下一个工具(链式调用)。这是正常且鼓励的工作方式。 常见链式调用模式: - web_search → web_fetch(搜索 → 抓取详情) - list_directory → read_file(列出目录 → 读取文件) - search_files → edit_file(搜索 → 修改) - run_command → read_file(执行命令 → 读取输出文件) ## 重要 永远不要只输出"我将执行xxx"或"让我来搜索"然后结束。如果说了要做什么,就必须真正调用对应的工具。调用 run_command 时命令可能需要很长时间,请耐心等待。`; const TOOL_USAGE_GUIDE = `【工具链规则(强制执行)】 1. 搜索后必须抓取:web_search 搜到结果后,必须选择最相关的 1-3 个 URL,用 web_fetch 抓取详细内容。只看搜索摘要就回答是不够的。这是强制规则。 2. 先搜索再回答:需要最新信息时,先 search → 再 fetch → 再回答。不要凭记忆猜测版本号、日期、价格等实时数据。 3. 文件路径上下文:用户说"修改这个文件"等模糊指代时,从最近的工具调用结果中提取实际路径。不要猜测。 4. 不要重复调用:已经成功调用过的工具+参数组合不要再调用。 5. 记忆工具:可以用 memory_search 搜索过去记忆,用 memory_add 添加重要信息。 6. 会话工具:可以用 session_list 列出历史会话,用 session_read 读取会话内容。 7. 并行调用:当多个工具调用互相独立时,可以在同一次回复中同时调用它们。`; /** 工具名白名单:用于文本解析兜底时过滤非法工具名 */ const VALID_TOOL_NAMES = new Set([ 'read_file', 'write_file', 'list_directory', 'search_files', 'create_directory', 'delete_file', 'run_command', 'move_file', 'copy_file', 'web_fetch', 'web_search', 'append_file', 'edit_file', 'get_file_info', 'tree', 'download_file', 'diff_files', 'replace_in_files', 'read_multiple_files', 'git', 'compress', 'memory_search', 'memory_add', 'session_list', 'session_read' ]); /** * 文本解析兜底:当模型没有通过 tool_calls 字段返回工具调用, * 而是在文本中写了 "Action: xxx" / "Action Input: {...}" 时, * 从文本中提取工具调用。 */ function parseToolCallsFromText(content: string): ToolCall[] { const calls: ToolCall[] = []; // 匹配:Action: tool_name(可选加粗标记) // 然后紧跟 Action Input: {json} // 支持格式: // Action: write_file Action: write_file // Action Input: {...} **Action Input:** {...} // 也支持写在同一段落的情况 const actionRegex = /\*{0,2}Action:?\*{0,2}\s*(\w+)\s+[\r\n\s]*\*{0,2}Action\s*Input:?\*{0,2}\s*(\{[\s\S]*?\})/gi; let match; while ((match = actionRegex.exec(content)) !== null) { const toolName = match[1].trim(); const argsStr = match[2].trim(); if (!VALID_TOOL_NAMES.has(toolName)) continue; try { // 清理 JSON:去除可能的 markdown 代码块包裹 let cleaned = argsStr.replace(/```json?\s*/g, '').replace(/```/g, '').trim(); const args = JSON.parse(cleaned); calls.push({ type: 'function', function: { name: toolName, arguments: args } }); } catch { // JSON 解析失败,尝试修复常见问题 try { let fixed = argsStr .replace(/'/g, '"') .replace(/,\s*}/g, '}') .replace(/,\s*]/g, ']') .replace(/```json?\s*/g, '') .replace(/```/g, '') .trim(); const args = JSON.parse(fixed); calls.push({ type: 'function', function: { name: toolName, arguments: args } }); } catch { logWarn(`文本解析兜底: 工具 ${toolName} 的参数 JSON 解析失败`, argsStr.slice(0, 100)); } } } if (calls.length > 0) { logInfo(`文本解析兜底: 从回复中提取到 ${calls.length} 个工具调用`, calls.map(c => c.function.name).join(', ')); } return calls; } /** v4.2: 从对话中自动推断用户画像 */ async function inferUserProfile(userMsg: string, assistantMsg: string): Promise { const bridge = (window as any).metonaDesktop; if (!bridge?.db?.getSetting) return; const profile: Record = await bridge.db.getSetting('user_profile', {}) || {}; // 技术栈检测 const techPatterns: Record = { 'Python': /\b(python|pip|py|django|flask|fastapi|pandas|numpy)\b/i, 'TypeScript': /\b(typescript|ts|tsx|deno)\b/i, 'JavaScript': /\b(javascript|js|jsx|node\.?js|npm|yarn|pnpm)\b/i, 'Go': /\b(golang|go\s+(run|build|mod)|\.go\b)/i, 'Rust': /\b(rust|cargo|rustc|\.rs\b)/i, 'Java': /\b(java|maven|gradle|spring|\.java\b)/i, 'React': /\b(react|next\.?js|jsx|tsx|vite)\b/i, 'Vue': /\b(vue|nuxt|vuex|pinia)\b/i, 'Docker': /\b(docker|dockerfile|docker-compose|container)\b/i, 'Kubernetes': /\b(k8s|kubernetes|kubectl|helm)\b/i, 'Linux': /\b(linux|ubuntu|debian|centos|bash|shell|terminal)\b/i, 'Git': /\b(git|github|gitee|gitlab|commit|push|pull|branch|merge)\b/i, 'SQL': /\b(sql|sqlite|mysql|postgres|database|查询)\b/i, 'AI/ML': /\b(machine learning|深度学习|神经网络|模型训练|ollama|llm|transformer)\b/i, }; const combinedText = userMsg + ' ' + assistantMsg; const currentStack: string[] = (profile.tech_stack as string[]) || []; let changed = false; for (const [tech, pattern] of Object.entries(techPatterns)) { if (pattern.test(combinedText) && !currentStack.includes(tech)) { currentStack.push(tech); changed = true; } } // 限制技术栈数量 if (currentStack.length > 15) { currentStack.splice(0, currentStack.length - 15); changed = true; } if (changed) { profile.tech_stack = currentStack; await bridge.db.saveSetting('user_profile', profile); } } const toolResultCache = new Map(); /** 生成工具调用缓存 key */ function getToolCacheKey(name: string, args: Record): string { try { return name + '::' + JSON.stringify(args, Object.keys(args).sort()); } catch { return name + '::' + String(args); } } /** 检测当前轮次是否存在重复工具调用 */ function isDuplicateCall(call: ToolCall, allCalls: ToolCall[]): boolean { const callKey = getToolCacheKey(call.function.name, call.function.arguments); return allCalls.some((prev, idx) => { if (idx === allCalls.length - 1) return false; return getToolCacheKey(prev.function.name, prev.function.arguments) === callKey; }); } /** * 格式化工具结果,生成模型友好的简洁表示 * 原始 ToolResult 对象可能包含大量冗余字段(results数组、formatted字符串等), * 直接 JSON.stringify 会生成臃肿的 JSON,干扰模型理解和后续工具调用。 */ function formatToolResultForModel(toolName: string, result: ToolResult): string { if (!result.success) { return JSON.stringify({ success: false, error: result.error || '工具执行失败' }); } switch (toolName) { case 'web_search': { const raw = result.results as Array<{ title: string; url: string; snippet: string }> | undefined; if (!raw?.length) return JSON.stringify({ success: true, message: '未找到结果' }); // 只保留 top 5,简洁格式 const top = raw.slice(0, 5).map((r, i) => `[${i + 1}] ${r.title}\n URL: ${r.url}\n ${r.snippet}` ).join('\n\n'); return JSON.stringify({ success: true, query: result.query, total: result.total, results: top }); } case 'web_fetch': { let content = (result.content as string) || ''; // 截断过长内容,避免撑爆上下文 if (content.length > 15000) content = content.slice(0, 15000) + '\n... (已截断)'; return JSON.stringify({ success: true, url: result.url, content }); } case 'read_file': { return JSON.stringify({ success: true, path: result.path, content: result.content, lines: result.lines, truncated: result.truncated, line_range: result.line_range }); } case 'read_multiple_files': { return JSON.stringify({ success: true, files: result.files, total: result.total }); } case 'list_directory': { return JSON.stringify({ success: true, path: result.path, entries: result.entries, total: result.total, truncated: result.truncated }); } case 'write_file': { return JSON.stringify({ success: true, path: result.path, bytesWritten: result.bytesWritten, created: result.created }); } case 'run_command': { return JSON.stringify({ success: true, stdout: result.stdout, stderr: result.stderr, exitCode: result.exitCode, duration: result.duration }); } case 'git': { return JSON.stringify({ success: true, action: result.action, output: result.output, branch: result.branch, files: result.files, commits: result.commits }); } case 'search_files': { return JSON.stringify({ success: true, query: result.query, total_matches: result.total_matches, total_files: result.total_files, matches: result.matches }); } default: { // 通用清理:移除内部元数据字段 const clean: Record = {}; for (const [k, v] of Object.entries(result)) { if (k === 'success' || k === 'formatted' || k === 'content_type' || k === 'status' || k === 'length' || k === 'isDirectory') continue; clean[k] = v; } return JSON.stringify(clean); } } } export interface AgentCallbacks { onThinking: (text: string) => void; onContent: (text: string) => void; onToolCallStart: (call: ToolCall) => void; onToolCallResult: (name: string, result: ToolResult, call: ToolCall) => void; onToolCallError: (name: string, error: string, call: ToolCall) => void; onDone: (finalContent: string, toolRecords?: ToolCallRecord[], stats?: { eval_count?: number; total_duration?: number }) => void; onConfirmTool: (call: ToolCall) => Promise; } /** 保存执行轨迹到 SQLite */ async function saveTrace(trace: Omit): Promise { try { const bridge = (window as any).metonaDesktop; if (!bridge?.db) return; const entry = { id: `trace_${generateId()}`, session_id: trace.sessionId, step_index: trace.stepIndex, thought: trace.thought, action: trace.action, action_input: trace.actionInput, observation: trace.observation, loop_count: trace.loopCount, created_at: trace.createdAt }; await bridge.db.saveTrace(entry); } catch { /* 不阻塞主流程 */ } } export async function runAgentLoop( userContent: string, images: string[], historyMessages: Array<{ role: string; content: string; images?: string[] }>, callbacks: AgentCallbacks ): Promise { const api = state.get(KEYS.API); const model = state.get('_defaultModel', ''); const currentSession = state.get(KEYS.CURRENT_SESSION); const sessionId = currentSession?.id || 'unknown'; if (!api || !model) { showToast('请先选择模型', 'error'); return; } // 新一轮对话,清空工具缓存 toolResultCache.clear(); // 检查模型是否支持 Tool Calling const modelSupportsTools = state.get('modelSupportsTools', false); // 提前获取工具列表,供后续系统 prompt 构建使用 const tools = getEnabledToolDefinitions(); const useTools = tools.length > 0; const messages: OllamaMessage[] = []; let systemPromptParts: string[] = []; // 注入记忆上下文 if (isMemoryEnabled() && userContent) { const relevantMemories = searchMemories(userContent, 6); if (relevantMemories.length > 0) { systemPromptParts.push(buildMemoryContext(relevantMemories)); for (const m of relevantMemories) { await markMemoryUsed(m.id); } } } // 注入工作空间上下文 const workspaceDir = getWorkspaceDirPath(); if (workspaceDir) { systemPromptParts.push(`【工作空间】 当前工作空间目录: ${workspaceDir} 你可以使用 run_command 工具在此目录下执行命令,所有命令通过工作空间进程管理执行,无超时限制。 文件操作工具(read_file、write_file 等)的相对路径基于此目录解析。`); } // v4.2 注入匹配的技能上下文 if (useTools && userContent) { const matchedSkills = await matchSkills(userContent, 3); if (matchedSkills.length > 0) { const skillContext = buildSkillContext(matchedSkills); if (skillContext) { systemPromptParts.push(skillContext); logInfo(`技能匹配: ${matchedSkills.length} 个技能已注入`, matchedSkills.map(s => s.name).join(', ')); } } } // v4.2 注入用户画像 const db = state.get(KEYS.DB); if (db) { try { const userProfile = await db.getSetting('user_profile', null); if (userProfile && typeof userProfile === 'object') { const parts: string[] = []; if (userProfile.tech_stack?.length) parts.push(`技术栈: ${userProfile.tech_stack.join(', ')}`); if (userProfile.role) parts.push(`角色: ${userProfile.role}`); if (userProfile.style) parts.push(`代码风格: ${userProfile.style}`); if (parts.length > 0) { systemPromptParts.push(`【用户画像】\n${parts.join('\n')}`); } } } catch { /* 不阻塞 */ } } // 组合 system prompt const personality = state.get('personality', 'default'); const personalityPrompt = PERSONALITY_SYSTEM_PROMPTS[personality] || ''; const fullSystemPrompt = [ ...systemPromptParts, AGENT_SYSTEM_PROMPT, ...(personalityPrompt ? [personalityPrompt] : []), TOOL_USAGE_GUIDE ].join('\n\n'); messages.push({ role: 'system', content: fullSystemPrompt }); // 添加历史消息(使用 context-manager 构建) for (const msg of historyMessages) { messages.push({ role: msg.role as 'user' | 'assistant', content: msg.content, ...(msg.images?.length && { images: msg.images }) }); } // 用户消息 const userMsg: OllamaMessage = { role: 'user', content: userContent || (images?.length ? (images.length > 1 ? `请分析这 ${images.length} 张图片` : '请分析这张图片') : ''), ...(images?.length && { images }) }; messages.push(userMsg); logInfo(`ReAct Agent Loop 启动: ${model}`, `工具: ${useTools ? '开启' : '关闭'}, 记忆: ${isMemoryEnabled() ? '开启' : '关闭'}`); let loopCount = 0; const allToolRecords: ToolCallRecord[] = []; const loopStartTime = Date.now(); let content = ''; let totalEvalCount = 0; let prevLoopToolKeys: string[] = []; /** 每轮工具调用计数(用于去重) */ let allCallsThisLoop: ToolCall[] = []; const makeStats = () => { const totalDuration = (Date.now() - loopStartTime) * 1e6; return { eval_count: totalEvalCount || undefined, total_duration: totalDuration }; }; while (loopCount < MAX_LOOPS) { loopCount++; // 全局超时检查 if (Date.now() - loopStartTime > MAX_LOOP_TIME) { logWarn('ReAct Agent Loop 达到最大运行时间(10分钟),自动停止'); callbacks.onDone(content || '(达到最大运行时间限制)', allToolRecords, makeStats()); return; } // 检查是否已中止 if (state.get(KEYS.ABORT_CONTROLLER)?.signal.aborted) { logInfo('ReAct Agent Loop 已中止'); callbacks.onDone(content, allToolRecords.length > 0 ? allToolRecords : undefined, makeStats()); return; } logAgentLoop(loopCount, MAX_LOOPS); let thinking = ''; content = ''; const toolCalls: ToolCall[] = []; allCallsThisLoop = []; const abortController = new AbortController(); state.set(KEYS.ABORT_CONTROLLER, abortController); try { // 带超时保护的流式调用 await Promise.race([ api.chatStream( { model, messages, stream: true, think: state.get('thinkEnabled', false), options: { num_ctx: state.get(KEYS.NUM_CTX, 24576), temperature: state.get('temperature', 0.7) }, ...(useTools && { tools }) }, (chunk: OllamaStreamChunk) => { if (chunk.message?.thinking) { thinking += chunk.message.thinking; callbacks.onThinking(thinking); } if (chunk.message?.content) { content += chunk.message.content; callbacks.onContent(content); } if (chunk.eval_count) totalEvalCount = chunk.eval_count; if (chunk.message?.tool_calls?.length) { for (const tc of chunk.message.tool_calls) { if (tc.function?.name) { toolCalls.push({ type: 'function', function: { name: tc.function.name, arguments: tc.function.arguments || {} } }); } else if (toolCalls.length > 0) { const last = toolCalls[toolCalls.length - 1]; if (tc.function?.arguments && typeof tc.function.arguments === 'object') { Object.assign(last.function.arguments, tc.function.arguments); } } } } }, abortController ), new Promise((_, reject) => setTimeout(() => reject(new Error('模型响应超时(2分钟无数据)')), STREAM_TIMEOUT) ) ]); } catch (err) { if (abortController.signal.aborted) { logInfo('流式调用已中止'); if (content || thinking) { messages.push({ role: 'assistant', content, ...(thinking && { thinking }) }); } callbacks.onDone(content, allToolRecords.length > 0 ? allToolRecords : undefined, makeStats()); return; } if ((err as Error).message.includes('超时')) { logError('流式调用超时', (err as Error).message); if (content || thinking) { messages.push({ role: 'assistant', content: content || '(模型响应超时)', ...(thinking && { thinking }) }); } callbacks.onDone(content || '(模型响应超时,已自动中断)', allToolRecords.length > 0 ? allToolRecords : undefined, makeStats()); return; } throw err; } // 提取 ReAct 思考过程 const thoughtMatch = content.match(/\*\*Thought:\*\*\s*([\s\S]*?)(?=\*\*Action:\*\*|\*\*Final Answer:\*\*|$)/i); const thought = thoughtMatch ? thoughtMatch[1].trim() : ''; // 保存 assistant 消息 const assistantMsg: OllamaMessage = { role: 'assistant', content, ...(thinking && { thinking }) }; if (toolCalls.length > 0) { assistantMsg.tool_calls = toolCalls; } messages.push(assistantMsg); logModelResponse(content.length, toolCalls.length); // 文本解析兜底:模型没通过 tool_calls 返回但文本中写了 Action if (toolCalls.length === 0 && useTools) { const parsedCalls = parseToolCallsFromText(content); if (parsedCalls.length > 0) { toolCalls.push(...parsedCalls); } } // 检查是否是 Final Answer(没有工具调用且包含 Final Answer) const isFinalAnswer = content.includes('Final Answer:') || content.includes('最终答案:'); // 处理空响应:如果之前有工具调用但模型返回空内容,不立即结束 if (toolCalls.length === 0 && !content.trim() && loopCount > 1 && allToolRecords.length > 0) { logWarn('模型返回空内容(有未处理的工具结果),继续循环'); // 移除空的 assistant 消息,避免 Ollama 解析问题 messages.pop(); // 添加一个提示性的 user 消息引导模型继续 messages.push({ role: 'user', content: '请根据上面的工具调用结果继续回答。如果需要更多信息,可以继续调用工具。如果已有足够信息,请给出最终回答。' }); continue; } if (toolCalls.length === 0) { // 真正的最终回答(有内容或首次循环就无工具) logInfo('无工具调用,ReAct Agent Loop 结束'); // 自动提取记忆(仅在对话结束后) if (isMemoryEnabled() && messages.length >= 4) { try { const { extractMemoriesFromConversation } = await import('./memory-manager.js'); await extractMemoriesFromConversation( messages.filter(m => m.role === 'user' || m.role === 'assistant').map(m => ({ role: m.role, content: m.content })), currentSession?.title ); } catch { /* 不阻塞 */ } } // v4.2 自动提取技能 if (allToolRecords.length >= 2) { try { await extractSkillsFromToolRecords(allToolRecords, userContent, currentSession?.title || ''); } catch { /* 不阻塞 */ } } // v4.2 自动推断用户画像 try { await inferUserProfile(userContent, content); } catch { /* 不阻塞 */ } callbacks.onDone(content || '(模型未返回内容)', allToolRecords.length > 0 ? allToolRecords : undefined, makeStats()); return; } // 跨轮次完整重复检测 const currentLoopKeys = toolCalls.map(c => getToolCacheKey(c.function.name, c.function.arguments)).sort(); const currentKeysStr = JSON.stringify(currentLoopKeys); const prevKeysStr = JSON.stringify([...prevLoopToolKeys].sort()); if (currentKeysStr === prevKeysStr && currentLoopKeys.length > 0) { logWarn('检测到连续两轮工具调用完全相同,终止 ReAct Loop', currentKeysStr.slice(0, 200)); callbacks.onDone(content || '(检测到重复工具调用,已自动停止)', allToolRecords, makeStats()); return; } prevLoopToolKeys = currentLoopKeys; // 记录 ReAct 轨迹 const traceStep = { sessionId, stepIndex: loopCount, thought: thought || content.slice(0, 200), action: toolCalls.map(t => t.function.name).join(', '), actionInput: JSON.stringify(toolCalls.map(t => t.function.arguments)), observation: '', loopCount, createdAt: Date.now() }; // ── v4.1 工具并行执行:将独立工具分批并行调用 ── // 将工具调用分成批次:同一批次内的工具互相独立,可并行执行 // 跨批次的工具有依赖关系(如 web_fetch 依赖前面的 web_search) const batches: ToolCall[][] = []; let currentBatch: ToolCall[] = []; const batchDeps = new Map(); // 记录每个工具依赖的前序工具名 for (const call of toolCalls) { if (currentBatch.length === 0) { currentBatch.push(call); } else { // 检查当前工具是否依赖当前批次中任何工具的结果 const needsPrevResult = currentBatch.some(prev => TOOLS_WITH_DATA_DEPS.has(call.function.name) && prev.function.name !== call.function.name ); if (needsPrevResult) { // 有依赖,当前批次结束,开启新批次 batches.push(currentBatch); batchDeps.set(call, currentBatch[currentBatch.length - 1].function.name); currentBatch = [call]; } else { currentBatch.push(call); } } } if (currentBatch.length > 0) batches.push(currentBatch); if (batches.length > 1) { logInfo(`工具并行执行: ${toolCalls.length} 个工具 → ${batches.length} 批次(首批 ${batches[0].length} 个并行)`); } /** 执行单个工具(含重试),返回 [ToolCallRecord, 缓存key|null] */ const executeSingleTool = async (call: ToolCall): Promise<[ToolCallRecord, string | null]> => { const cacheKey = getToolCacheKey(call.function.name, call.function.arguments); // 重复调用检测 if (isDuplicateCall(call, toolCalls)) { logWarn(`跳过重复工具调用: ${call.function.name}`); const cached = toolResultCache.get(cacheKey); if (cached) { return [{ name: call.function.name, arguments: call.function.arguments, result: cached, status: 'success' as const, timestamp: Date.now() }, null]; } } // 跨轮次缓存命中 const cachedResult = toolResultCache.get(cacheKey); if (cachedResult) { logInfo(`工具缓存命中: ${call.function.name}`); return [{ name: call.function.name, arguments: call.function.arguments, result: cachedResult, status: 'success' as const, timestamp: Date.now() }, null]; } callbacks.onToolCallStart(call); logToolStart(call.function.name, JSON.stringify(call.function.arguments)); // 需要确认的工具(目前只有 run_command 在 confirm 模式下) if (needsConfirmation(call.function.name)) { const confirmed = await callbacks.onConfirmTool(call); if (!confirmed) { logWarn(`工具取消: ${call.function.name}`); const cancelResult = { success: false, error: '用户取消了操作' }; return [{ name: call.function.name, arguments: call.function.arguments, result: cancelResult, status: 'cancelled' as const, timestamp: Date.now() }, null]; } } // 执行 + 自动重试 let lastError = ''; for (let retry = 0; retry <= MAX_RETRIES; retry++) { try { let result: ToolResult; if (call.function.name === 'run_command') { result = await executeTool(call.function.name, call.function.arguments); } else { result = await Promise.race([ executeTool(call.function.name, call.function.arguments).catch(err => ({ success: false, error: err?.message || String(err) }) as ToolResult ), new Promise((_, reject) => setTimeout(() => reject(new Error('工具执行超时(30秒)')), TOOL_EXEC_TIMEOUT) ) ]); } logToolResult(call.function.name, result.success, result.success ? undefined : result.error); return [{ name: call.function.name, arguments: call.function.arguments, result, status: result.success ? 'success' as const : 'error' as const, timestamp: Date.now() }, result.success && call.function.name !== 'run_command' ? cacheKey : null]; } catch (err) { lastError = (err as Error).message; if (retry < MAX_RETRIES) { logWarn(`工具重试 ${retry + 1}/${MAX_RETRIES}: ${call.function.name}`, lastError); await new Promise(r => setTimeout(r, 500)); continue; } logError(`工具执行失败: ${call.function.name}`, lastError); return [{ name: call.function.name, arguments: call.function.arguments, result: { success: false, error: lastError }, status: 'error' as const, timestamp: Date.now() }, null]; } } // unreachable return [{ name: call.function.name, arguments: call.function.arguments, result: { success: false, error: lastError }, status: 'error' as const, timestamp: Date.now() }, null]; }; // 按批次执行:批次内并行,批次间串行 for (const batch of batches) { if (abortController.signal.aborted) { callbacks.onDone(content, allToolRecords.length > 0 ? allToolRecords : undefined, makeStats()); return; } const results = await Promise.all(batch.map(call => executeSingleTool(call))); for (const [record, cacheKey] of results) { allToolRecords.push(record); messages.push({ role: 'tool', tool_name: record.name, content: formatToolResultForModel(record.name, record.result!) }); if (cacheKey) toolResultCache.set(cacheKey, record.result!); if (record.status === 'success') { callbacks.onToolCallResult(record.name, record.result!, batch.find(c => c.function.name === record.name)!); } else if (record.status === 'cancelled') { callbacks.onToolCallError(record.name, '用户取消', batch.find(c => c.function.name === record.name)!); } else { callbacks.onToolCallError(record.name, record.result?.error || '执行失败', batch.find(c => c.function.name === record.name)!); } } } // 保存本轮轨迹 traceStep.observation = allToolRecords.slice(-toolCalls.length).map(r => `${r.name}: ${r.result?.success ? 'success' : 'error'}` ).join('; '); saveTrace(traceStep); } logWarn('ReAct Agent Loop 达到最大工具调用次数限制'); callbacks.onDone(content || '(达到最大工具调用次数限制)', allToolRecords, makeStats()); }