feat: v4.0.0 大版本升级 - SQLite3 + ReAct Agent

- P0: 存储层 IndexedDB → SQLite3 (better-sqlite3),7张表 + FTS5全文搜索
- P1: Agent Engine 升级为 ReAct 模式(思考→行动→观察→反思)
- P2: 新增 4 个工具(memory_search, memory_add, session_list, session_read)
- P3: 上下文管理器(滑动窗口 + 摘要压缩 + 记忆注入)
- P4: UI 改版(ReAct 执行面板样式 + 思考过程卡片)
- P5: IndexedDB → SQLite 数据迁移支持
- MAX_LOOPS 10→15, MAX_LOOP_TIME 5min→10min, 错误自动重试 2 次
This commit is contained in:
thzxx
2026-04-17 12:35:42 +08:00
parent 5fb36a93e6
commit 26cbac71fe
14 changed files with 2001 additions and 332 deletions
+206 -135
View File
@@ -1,6 +1,7 @@
/**
* Agent Engine - Agent Loop 核心引擎
* 管理带工具调用的流式对话循环
* Agent Engine - ReAct Agent Loop 核心引擎 (v4.0)
* ReAct 模式: Thought → Action → Observation → Reflection
* 支持任务复杂度评估、自动规划、错误重试、执行轨迹记录
*/
import { OllamaAPI } from '../api/ollama.js';
@@ -12,35 +13,79 @@ import {
} from './tool-registry.js';
import { searchMemories, buildMemoryContext, markMemoryUsed, isMemoryEnabled } from './memory-manager.js';
import { showToast } from '../components/toast.js';
import { logInfo, logWarn, logError, logToolStart, logToolResult, logThink, logStream, logAgentLoop, logModelResponse } from './log-service.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
ToolCallRecord,
TraceEntry
} from '../types.js';
const MAX_LOOPS = 10;
const MAX_LOOP_TIME = 300000; // 5 分钟总超时
const TOOL_EXEC_TIMEOUT = 30000; // 工具执行超时 30 秒
const STREAM_TIMEOUT = 120000; // 单次流式调用超时 2 分钟
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; // 工具错误自动重试次数
const REACT_SYSTEM_PROMPT = `你是一个具备 ReActReasoning + Acting)能力的 AI 助手。你必须严格按照以下格式与用户交互:
## ReAct 输出格式
每次响应时,你必须按照以下结构思考和行动:
**Thought:** [你对当前任务的分析和推理。思考用户需要什么、当前有哪些信息、下一步应该做什么。]
**Action:** [如果需要使用工具,选择一个工具并说明原因。如果不需要工具,直接给出最终答案。]
**Action Input:** [工具的参数,必须是合法 JSON 格式]
然后等待 Observation(工具返回的结果)。
收到 Observation 后:
**Thought:** [基于工具结果的进一步推理。结果是否满足需求?是否需要更多步骤?]
**Action:** [下一步行动,或者 "Final Answer" 如果任务已完成]
**Action Input:** [下一步工具参数,或者最终答案]
## 任务复杂度评估
在开始时评估任务:
- **简单**:直接回答(1 步),不需要工具或仅需 1 个工具调用
- **中等**:需要 2-4 步,可能需要多个工具协作
- **复杂**:需要 5+ 步,需要规划和分解
对于复杂任务,先输出一个简短的执行计划,再开始 ReAct 循环。
## 错误处理
- 如果工具返回错误,分析原因并尝试不同方法
- 最多重试 2 次,如果仍然失败,向用户说明情况
- 不要反复调用相同参数的相同工具
## 最终答案
当任务完成时,输出 **Final Answer:** 后面跟你的最终回答。回答应该:
- 汇总所有执行步骤和结果
- 直接回答用户的问题
- 如果有重要发现,明确指出`;
const TOOL_USAGE_GUIDE = `【工具使用规则】
1. 路径上下文:当用户说"修改这个文件"、"删除它"、"查看里面"等未指定路径时,必须从本次对话中最近的工具调用结果中提取路径。例如用户之前让你读取了 /home/user/project/config.json,后续说"修改配置"就应继续使用该路径,而不是推测新路径。
1. 路径上下文:当用户说"修改这个文件"、"删除它"、"查看里面"等未指定路径时,必须从本次对话中最近的工具调用结果中提取路径。
2. 工具调用结果中的 path 字段是权威的文件路径,优先使用它。
3. 如果对话中从未出现过相关路径,应先用 search_files 或 list_directory 定位,不要凭空猜测路径。
4. 对同一文件的连续操作(读取→修改、读取→删除),路径必须一致。
5. 当用户要求执行命令时,使用 run_command 工具执行。命令通过工作空间终端实时执行并显示,执行完成后将 stdout/stderr 结果告知用户。
6. 联网搜索联动:当用户询问需要最新信息的问题时(如版本号、新闻、价格等),先用 web_search 搜索,然后必须从搜索结果中选择最相关的 1-3 个 URL,用 web_fetch 抓取页面详细内容获取准确数据。不要仅凭搜索摘要回答——摘要往往不完整,必须抓取原文才能给出准确答案
7. 主动使用工具:当你不确定答案、信息可能已过时、或需要查阅实时数据时,主动调用工具而不是凭记忆猜测。以下场景必须使用工具:
- 用户问"最新版本"、"现在多少钱"、"最近有什么新闻" → web_search + web_fetch
- 用户让你查看/修改/分析文件 → read_file / list_directory / search_files
- 用户问项目状态、代码内容 → git status / read_file
- 用户让你执行操作 → run_command / write_file 等
宁可多调一次工具,也不要给出过时或编造的信息。
8. 严禁重复调用:如果本次对话中已经成功调用过某个工具并获得了结果,不要再次调用相同工具和相同参数。直接使用已有结果继续工作。例如:已经 web_fetch 抓取了页面内容,下一步应直接处理该内容(如 write_file 保存),而不是重新抓取。`;
5. 当用户要求执行命令时,使用 run_command 工具执行。
6. 联网搜索联动:当用户询问需要最新信息的问题时,先用 web_search 搜索,然后必须从搜索结果中选择最相关的 1-3 个 URL,用 web_fetch 抓取页面详细内容。
7. 主动使用工具:当你不确定答案、信息可能已过时、或需要查阅实时数据时,主动调用工具而不是凭记忆猜测。
8. 严禁重复调用:如果本次对话中已经成功调用过某个工具并获得了结果,不要再次调用相同工具和相同参数。
9. 记忆工具:你可以使用 memory_search 搜索过去的记忆,使用 memory_add 添加新的重要记忆。
10. 会话工具:你可以使用 session_list 列出历史会话,使用 session_read 读取历史会话内容。`;
/** 工具调用缓存:key = toolName + JSON(args),用于避免重复调用 */
const toolResultCache = new Map<string, ToolResult>();
@@ -48,7 +93,6 @@ const toolResultCache = new Map<string, ToolResult>();
/** 生成工具调用缓存 key */
function getToolCacheKey(name: string, args: Record<string, unknown>): string {
try {
// 对 arguments 排序 key 以保证一致性
return name + '::' + JSON.stringify(args, Object.keys(args).sort());
} catch {
return name + '::' + String(args);
@@ -56,10 +100,10 @@ function getToolCacheKey(name: string, args: Record<string, unknown>): string {
}
/** 检测当前轮次是否存在重复工具调用 */
function isDuplicateCall(call: ToolCall, loopCalls: ToolCall[]): boolean {
function isDuplicateCall(call: ToolCall, allCalls: ToolCall[]): boolean {
const callKey = getToolCacheKey(call.function.name, call.function.arguments);
return loopCalls.some((prev, idx) => {
if (idx === loopCalls.length - 1) return false; // 跳过自身
return allCalls.some((prev, idx) => {
if (idx === allCalls.length - 1) return false;
return getToolCacheKey(prev.function.name, prev.function.arguments) === callKey;
});
}
@@ -74,6 +118,26 @@ export interface AgentCallbacks {
onConfirmTool: (call: ToolCall) => Promise<boolean>;
}
/** 保存执行轨迹到 SQLite */
async function saveTrace(trace: Omit<TraceEntry, 'id' | 'createdAt'>): Promise<void> {
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[],
@@ -82,6 +146,8 @@ export async function runAgentLoop(
): Promise<void> {
const api = state.get<OllamaAPI>(KEYS.API);
const model = state.get<string>('_defaultModel', '');
const currentSession = state.get(KEYS.CURRENT_SESSION);
const sessionId = currentSession?.id || 'unknown';
if (!api || !model) {
showToast('请先选择模型', 'error');
@@ -93,12 +159,8 @@ export async function runAgentLoop(
// 检查模型是否支持 Tool Calling
const modelSupportsTools = state.get<boolean>('modelSupportsTools', false);
if (!modelSupportsTools) {
logWarn(`模型 ${model} 不支持 Tool Calling`, 'Agent Loop 可能不稳定');
}
const messages: OllamaMessage[] = [];
let systemPromptParts: string[] = [];
// 注入记忆上下文
@@ -121,12 +183,14 @@ export async function runAgentLoop(
文件操作工具(read_file、write_file 等)的相对路径基于此目录解析。`);
}
if (systemPromptParts.length > 0) {
messages.push({ role: 'system', content: systemPromptParts.join('\n\n') + '\n\n' + TOOL_USAGE_GUIDE });
} else {
messages.push({ role: 'system', content: TOOL_USAGE_GUIDE });
}
// 组合 system prompt
const fullSystemPrompt = systemPromptParts.length > 0
? systemPromptParts.join('\n\n') + '\n\n' + REACT_SYSTEM_PROMPT + '\n\n' + TOOL_USAGE_GUIDE
: REACT_SYSTEM_PROMPT + '\n\n' + TOOL_USAGE_GUIDE;
messages.push({ role: 'system', content: fullSystemPrompt });
// 添加历史消息(使用 context-manager 构建)
for (const msg of historyMessages) {
messages.push({
role: msg.role as 'user' | 'assistant',
@@ -135,6 +199,7 @@ export async function runAgentLoop(
});
}
// 用户消息
const userMsg: OllamaMessage = {
role: 'user',
content: userContent || (images?.length ? (images.length > 1 ? `请分析这 ${images.length} 张图片` : '请分析这张图片') : ''),
@@ -145,18 +210,19 @@ export async function runAgentLoop(
const tools = getEnabledToolDefinitions();
const useTools = tools.length > 0;
logInfo(`Agent Loop 启动: ${model}`, `工具: ${useTools ? '开启' : '关闭'}, 记忆: ${isMemoryEnabled() ? '开启' : '关闭'}`);
logInfo(`ReAct Agent Loop 启动: ${model}`, `工具: ${useTools ? '开启' : '关闭'}, 记忆: ${isMemoryEnabled() ? '开启' : '关闭'}`);
let loopCount = 0;
const allToolRecords: ToolCallRecord[] = [];
const loopStartTime = Date.now();
let content = '';
let totalEvalCount = 0;
/** 记录前一轮的工具调用 key,用于跨轮去重 */
let prevLoopToolKeys: string[] = [];
/** 每轮工具调用计数(用于去重) */
let allCallsThisLoop: ToolCall[] = [];
const makeStats = () => {
const totalDuration = (Date.now() - loopStartTime) * 1e6; // 转为纳秒
const totalDuration = (Date.now() - loopStartTime) * 1e6;
return { eval_count: totalEvalCount || undefined, total_duration: totalDuration };
};
@@ -165,14 +231,14 @@ export async function runAgentLoop(
// 全局超时检查
if (Date.now() - loopStartTime > MAX_LOOP_TIME) {
logWarn('Agent Loop 达到最大运行时间(5分钟),自动停止');
logWarn('ReAct Agent Loop 达到最大运行时间(10分钟),自动停止');
callbacks.onDone(content || '(达到最大运行时间限制)', allToolRecords, makeStats());
return;
}
// 检查是否已中止
if (state.get<AbortController | null>(KEYS.ABORT_CONTROLLER)?.signal.aborted) {
logInfo('Agent Loop 已中止');
logInfo('ReAct Agent Loop 已中止');
callbacks.onDone(content, allToolRecords.length > 0 ? allToolRecords : undefined, makeStats());
return;
}
@@ -182,6 +248,7 @@ export async function runAgentLoop(
let thinking = '';
content = '';
const toolCalls: ToolCall[] = [];
allCallsThisLoop = [];
const abortController = new AbortController();
state.set(KEYS.ABORT_CONTROLLER, abortController);
@@ -205,10 +272,6 @@ export async function runAgentLoop(
if (chunk.message?.thinking) {
thinking += chunk.message.thinking;
callbacks.onThinking(thinking);
// 每收到 200 字符记录一次思考日志
if (thinking.length % 200 < 10) {
logThink(thinking);
}
}
if (chunk.message?.content) {
content += chunk.message.content;
@@ -218,7 +281,6 @@ export async function runAgentLoop(
if (chunk.message?.tool_calls?.length) {
for (const tc of chunk.message.tool_calls) {
if (tc.function?.name) {
// 新的工具调用
toolCalls.push({
type: 'function',
function: {
@@ -227,7 +289,6 @@ export async function runAgentLoop(
}
});
} else if (toolCalls.length > 0) {
// 增量 arguments 追加到当前最后一个 tool_call
const last = toolCalls[toolCalls.length - 1];
if (tc.function?.arguments && typeof tc.function.arguments === 'object') {
Object.assign(last.function.arguments, tc.function.arguments);
@@ -246,24 +307,15 @@ export async function runAgentLoop(
if (abortController.signal.aborted) {
logInfo('流式调用已中止');
if (content || thinking) {
messages.push({
role: 'assistant',
content,
...(thinking && { 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('流式调用超时2分钟无数据)', (err as Error).message);
logError('流式调用超时', (err as Error).message);
if (content || thinking) {
messages.push({
role: 'assistant',
content: content || '(模型响应超时)',
...(thinking && { thinking })
});
messages.push({ role: 'assistant', content: content || '(模型响应超时)', ...(thinking && { thinking }) });
}
callbacks.onDone(content || '(模型响应超时,已自动中断)', allToolRecords.length > 0 ? allToolRecords : undefined, makeStats());
return;
@@ -271,6 +323,11 @@ export async function runAgentLoop(
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,
@@ -278,30 +335,54 @@ export async function runAgentLoop(
};
if (toolCalls.length > 0) {
assistantMsg.tool_calls = toolCalls;
// 官方示例要求:tool_calls 时同时保留 content 和 thinking
// 确保 content 不为空(模型可能在调用工具前说了话)
}
messages.push(assistantMsg);
logModelResponse(content.length, toolCalls.length);
// 检查是否是 Final Answer(没有工具调用且包含 Final Answer
const isFinalAnswer = content.includes('Final Answer:') || content.includes('最终答案:');
if (toolCalls.length === 0) {
logInfo('无工具调用,Agent Loop 结束');
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 { /* 不阻塞 */ }
}
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('检测到连续两轮工具调用完全相同,终止 Agent Loop', currentKeysStr.slice(0, 200));
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()
};
for (const call of toolCalls) {
// 检查是否已中止
if (abortController.signal.aborted) {
@@ -311,30 +392,22 @@ export async function runAgentLoop(
// 同一轮内重复调用检测
if (isDuplicateCall(call, toolCalls)) {
logWarn(`跳过重复工具调用: ${call.function.name}`, JSON.stringify(call.function.arguments).slice(0, 200));
logWarn(`跳过重复工具调用: ${call.function.name}`);
const cachedKey = getToolCacheKey(call.function.name, call.function.arguments);
const cached = toolResultCache.get(cachedKey);
if (cached) {
messages.push({
role: 'tool',
tool_name: call.function.name,
content: JSON.stringify(cached)
});
messages.push({ role: 'tool', tool_name: call.function.name, content: JSON.stringify(cached) });
callbacks.onToolCallResult(call.function.name, cached, call);
}
continue;
}
// 跨轮次缓存命中检测
// 跨轮次缓存命中
const cacheKey = getToolCacheKey(call.function.name, call.function.arguments);
const cachedResult = toolResultCache.get(cacheKey);
if (cachedResult) {
logInfo(`工具缓存命中: ${call.function.name}`, JSON.stringify(call.function.arguments).slice(0, 200));
messages.push({
role: 'tool',
tool_name: call.function.name,
content: JSON.stringify(cachedResult)
});
logInfo(`工具缓存命中: ${call.function.name}`);
messages.push({ role: 'tool', tool_name: call.function.name, content: JSON.stringify(cachedResult) });
callbacks.onToolCallResult(call.function.name, cachedResult, call);
continue;
}
@@ -344,13 +417,12 @@ export async function runAgentLoop(
if (needsConfirmation(call.function.name)) {
const confirmed = await callbacks.onConfirmTool(call);
// 确认后再次检查是否已中止
if (abortController.signal.aborted) {
callbacks.onDone(content, allToolRecords.length > 0 ? allToolRecords : undefined, makeStats());
return;
}
if (!confirmed) {
logWarn(`工具取消: ${call.function.name}`, '用户取消了操作');
logWarn(`工具取消: ${call.function.name}`);
const record: ToolCallRecord = {
name: call.function.name,
arguments: call.function.arguments,
@@ -359,77 +431,76 @@ export async function runAgentLoop(
timestamp: Date.now()
};
allToolRecords.push(record);
messages.push({
role: 'tool',
tool_name: call.function.name,
content: JSON.stringify({ success: false, error: '用户取消了操作' })
});
messages.push({ role: 'tool', tool_name: call.function.name, content: JSON.stringify({ success: false, error: '用户取消了操作' }) });
callbacks.onToolCallError(call.function.name, '用户取消', call);
continue;
}
}
try {
// 工具执行(run_command 无超时,其他工具 30 秒超时)
logInfo(`执行工具: ${call.function.name}`, JSON.stringify(call.function.arguments));
let result: ToolResult;
if (call.function.name === 'run_command') {
// run_command 通过 workspace IPC 执行,无超时限制
result = await executeTool(call.function.name, call.function.arguments);
} else {
// 其他工具保持 30 秒超时保护
result = await Promise.race([
executeTool(call.function.name, call.function.arguments).catch(err =>
({ success: false, error: err?.message || String(err) }) as ToolResult
),
new Promise<ToolResult>((_, reject) =>
setTimeout(() => reject(new Error('工具执行超时(30秒)')), TOOL_EXEC_TIMEOUT)
)
]);
}
const record: ToolCallRecord = {
name: call.function.name,
arguments: call.function.arguments,
result,
status: result.success ? 'success' : 'error',
timestamp: Date.now()
};
allToolRecords.push(record);
// 工具执行 + 自动重试
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<ToolResult>((_, reject) =>
setTimeout(() => reject(new Error('工具执行超时(30秒)')), TOOL_EXEC_TIMEOUT)
)
]);
}
messages.push({
role: 'tool',
tool_name: call.function.name,
content: JSON.stringify(result)
});
// 缓存成功结果(非 run_command,因为命令执行可能有副作用)
if (result.success && call.function.name !== 'run_command') {
toolResultCache.set(cacheKey, result);
// 成功
const record: ToolCallRecord = {
name: call.function.name,
arguments: call.function.arguments,
result,
status: result.success ? 'success' : 'error',
timestamp: Date.now()
};
allToolRecords.push(record);
messages.push({ role: 'tool', tool_name: call.function.name, content: JSON.stringify(result) });
if (result.success && call.function.name !== 'run_command') {
toolResultCache.set(cacheKey, result);
}
callbacks.onToolCallResult(call.function.name, result, call);
logToolResult(call.function.name, result.success, result.success ? undefined : result.error);
break; // 成功,退出重试循环
} 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);
const record: ToolCallRecord = {
name: call.function.name,
arguments: call.function.arguments,
result: { success: false, error: lastError },
status: 'error',
timestamp: Date.now()
};
allToolRecords.push(record);
messages.push({ role: 'tool', tool_name: call.function.name, content: JSON.stringify({ success: false, error: lastError }) });
callbacks.onToolCallError(call.function.name, lastError, call);
}
callbacks.onToolCallResult(call.function.name, result, call);
logToolResult(call.function.name, result.success, result.success ? undefined : result.error);
} catch (err) {
const errorMsg = (err as Error).message;
logError(`工具执行失败: ${call.function.name}`, errorMsg);
const record: ToolCallRecord = {
name: call.function.name,
arguments: call.function.arguments,
result: { success: false, error: errorMsg },
status: 'error',
timestamp: Date.now()
};
allToolRecords.push(record);
messages.push({
role: 'tool',
tool_name: call.function.name,
content: JSON.stringify({ success: false, error: errorMsg })
});
callbacks.onToolCallError(call.function.name, errorMsg, call);
}
}
// 保存本轮轨迹
traceStep.observation = allToolRecords.slice(-toolCalls.length).map(r =>
`${r.name}: ${r.result?.success ? 'success' : 'error'}`
).join('; ');
saveTrace(traceStep);
}
logWarn('Agent Loop 达到最大工具调用次数限制');
logWarn('ReAct Agent Loop 达到最大工具调用次数限制');
callbacks.onDone(content || '(达到最大工具调用次数限制)', allToolRecords, makeStats());
}