/** * Agent Loop — ReAct 状态机引擎 * * 生产级 ReAct Agent Loop,负责: * 1. 状态机管理循环生命周期 * 2. 调用 Provider Adapter 获取 LLM 响应(支持流式) * 3. 解析输出、执行工具、注入观察 * 4. 流式事件推送 UI * 5. 超时、重试、上下文压缩 * * @see docs/生产级通用 AI Agent 智能体桌面应用:完整设计与构建指南.html — 第四章 */ import { EventEmitter } from 'events'; import { nanoid } from 'nanoid'; import { AgentLoopState, TerminationReason, type IterationStep, type Thought, type AgentLoopConfig, type AgentLoopOutput, type TokenUsage, } from './types'; import type { MetonaRequest, MetonaResponse, MetonaMessage, MetonaSystemPrompt, MetonaToolCall, MetonaToolResult, MetonaStreamEvent, IMetonaProviderAdapter, MetonaToolDef, } from '../types'; import { MetonaStreamEventType, MetonaFinishReason } from '../types'; import { estimateMessagesTokens } from '../utils/token-estimator'; import log from 'electron-log'; /** * v0.3.0: 死循环错误 — 在 executeOneIterationStream 中抛出,主循环捕获后以 DEAD_LOOP 原因终止 */ class DeadLoopError extends Error { constructor(message: string) { super(message); this.name = 'DeadLoopError'; } } const DEFAULT_CONFIG: AgentLoopConfig = { maxIterations: 20, timeoutMs: 120_000, totalTimeoutMs: 600_000, enableReflection: false, compressionThreshold: 0.8, contextWindow: 128_000, retryCount: 3, temperature: 0.0, maxTokens: 63488, thinkingEnabled: true, thinkingEffort: 'high', toolExecutionTimeoutMs: 120_000, }; /** * ReAct Agent Loop 引擎 */ export class AgentLoopEngine extends EventEmitter { private currentState: AgentLoopState = AgentLoopState.INIT; private iterations: IterationStep[] = []; private currentIteration = 0; private startTime = 0; private totalTokens: TokenUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 }; private aborted = false; private config: AgentLoopConfig; private tools: MetonaToolDef[] = []; private currentSessionId: string = ''; private currentRequestId: string = ''; private workspacePath: string = ''; private abortController: AbortController | null = null; private eventSeq = 0; /** 当前 run 的唯一标识(用于前端过滤旧流事件) */ private runId: string = ''; /** 正在进行的 run Promise(用于 run lock,防止并发 run 污染状态) */ private currentRunPromise: Promise | null = null; /** v0.3.0: 工具调用签名历史(用于死循环检测) */ private toolCallHistory: string[] = []; constructor( config: Partial = {}, private adapter: IMetonaProviderAdapter, private toolRegistry?: import('../tools/registry').ToolRegistry, private preToolHooks: import('../hooks/pre-tool').PreToolHook[] = [], private postToolHooks: import('../hooks/post-tool').PostToolHook[] = [], ) { super(); this.config = { ...DEFAULT_CONFIG, ...config }; } /** * 设置工作空间路径(工具执行时传入 context) */ setWorkspacePath(path: string): void { this.workspacePath = path; } /** * 设置可用工具列表 */ setTools(tools: MetonaToolDef[]): void { this.tools = tools; } /** * 获取当前工具列表(用于子任务恢复) */ getTools(): MetonaToolDef[] { return [...this.tools]; } /** * 热切换 Provider Adapter(设置变更时调用) */ setAdapter(adapter: IMetonaProviderAdapter): void { this.adapter = adapter; } /** * 热更新 Engine 配置(设置变更时调用) * * 支持 maxIterations, totalTimeoutMs, thinkingEnabled, thinkingEffort, contextLength */ updateConfig(partial: Partial): void { this.config = { ...this.config, ...partial }; } /** * 执行完整的 ReAct 循环(流式模式) * * @param userInput 用户输入文本 * @param sessionId 会话 ID * @param history 历史消息 * @param systemPrompt System Prompt */ async runStream( userMessage: MetonaMessage, sessionId: string, history: MetonaMessage[], systemPrompt: MetonaSystemPrompt, ): Promise { // C-4/H-6: 等待上一次 run 完全结束,防止并发 run 污染状态和旧 DONE 中断新流 if (this.currentRunPromise) { await this.currentRunPromise.catch(() => {}); } this.currentRunPromise = this.executeRunStream(userMessage, sessionId, history, systemPrompt); try { return await this.currentRunPromise; } finally { this.currentRunPromise = null; } } private async executeRunStream( userMessage: MetonaMessage, sessionId: string, history: MetonaMessage[], systemPrompt: MetonaSystemPrompt, ): Promise { this.startTime = Date.now(); this.aborted = false; this.runId = `run_${nanoid(8)}`; this.iterations = []; this.currentIteration = 0; this.currentSessionId = sessionId; this.totalTokens = { promptTokens: 0, completionTokens: 0, totalTokens: 0 }; this.abortController = new AbortController(); this.eventSeq = 0; // v0.3.0: 重置工具调用历史(用于死循环检测) this.toolCallHistory = []; try { await this.transitionTo(AgentLoopState.INIT); // 构建消息列表(保留 images 字段) const messages: MetonaMessage[] = [ ...history, { role: 'user', content: userMessage.content, images: userMessage.images, timestamp: Date.now(), }, ]; // === 主循环 === while ( this.currentIteration < this.config.maxIterations && !this.aborted && Date.now() - this.startTime < this.config.totalTimeoutMs ) { this.currentIteration++; // 构建请求 const request: MetonaRequest = { meta: { sessionId, iteration: this.currentIteration, requestId: `r_${nanoid(12)}`, timestamp: Date.now(), agentVersion: '1.0.0', }, systemPrompt, messages, tools: this.tools.length > 0 ? this.tools : undefined, params: { maxTokens: this.config.maxTokens, temperature: this.config.temperature, stream: true, thinkingEnabled: this.config.thinkingEnabled, thinkingEffort: this.config.thinkingEffort, contextLength: this.config.contextLength, }, }; const step = await this.executeOneIterationStream(request, sessionId); this.iterations.push(step); // 将 assistant 回复加入消息历史 if (step.thought) { const assistantMsg: MetonaMessage = { role: 'assistant', content: step.thought.content, reasoningContent: step.thought.reasoningContent, toolCalls: step.toolCalls, timestamp: Date.now(), iteration: this.currentIteration, }; messages.push(assistantMsg); } // 如果没有工具调用,视为最终输出 if (!step.toolCalls || step.toolCalls.length === 0) { return this.finish(TerminationReason.COMPLETED, step.thought?.content); } // v0.3.0: 死循环检测已移入 executeOneIterationStream 的 PARSING 阶段后, // 通过抛出 DeadLoopError 在此处 catch 块捕获处理 // 执行工具并将结果加入消息 if (step.toolResults) { for (const result of step.toolResults) { messages.push({ role: 'tool', content: typeof result.result === 'string' ? result.result : JSON.stringify(result.result), toolResult: result, timestamp: Date.now(), iteration: this.currentIteration, }); } } } // 循环退出判断 if (this.aborted) return this.finish(TerminationReason.USER_INTERRUPT); if (this.currentIteration >= this.config.maxIterations) return this.finish(TerminationReason.MAX_ITERATIONS); return this.finish(TerminationReason.TIMEOUT); } catch (error) { // v0.3.0: 捕获 DeadLoopError — 以 DEAD_LOOP 原因终止 if (error instanceof DeadLoopError) { return this.finish(TerminationReason.DEAD_LOOP, error.message); } const errMsg = (error as Error).message; if (this.aborted || errMsg.includes('Aborted')) { return this.finish(TerminationReason.USER_INTERRUPT); } // v0.3.0 修复: 不使用 emit('error') — Node EventEmitter 对无监听器的 'error' 事件会同步 throw, // 导致 finish() 被中断、DONE 事件丢失、前端卡死。改为日志记录,让 finish 正常执行 log.error(`[AgentLoop] Run failed: ${errMsg}`); return this.finish(TerminationReason.ERROR, undefined, error as Error); } } /** 获取当前 Provider Adapter(供 SubAgent 创建独立引擎实例) */ getAdapter(): IMetonaProviderAdapter { return this.adapter; } /** 获取工作空间路径(供 SubAgent 继承) */ getWorkspacePath(): string { return this.workspacePath; } /** 中断循环 */ abort(): void { this.aborted = true; this.abortController?.abort(); this.abortController = null; this.emit('aborted'); } /** * 销毁引擎,清理所有监听器 */ destroy(): void { this.abort(); this.removeAllListeners(); } /** 获取当前状态 */ getState(): AgentLoopState { return this.currentState; } // ========== 私有方法 ========== /** * 执行单次迭代(流式模式) */ private async executeOneIterationStream( request: MetonaRequest, sessionId: string, ): Promise { this.currentRequestId = request.meta.requestId; const step: IterationStep = { iteration: this.currentIteration, state: AgentLoopState.THINKING, startedAt: Date.now(), }; try { // === THINKING: 流式调用 LLM === // transitionTo 已发射 stateChange 事件,无需重复 emit await this.transitionTo(AgentLoopState.THINKING); let fullContent = ''; let reasoningContent = ''; const toolCallsBuffer = new Map(); let tokenUsage: TokenUsage | undefined; // 流式接收响应 for await (const event of this.chatStreamWithRetry(request)) { if (this.aborted) break; // 过滤掉每轮的 DONE 事件 — 只在全部迭代完成后发送一个最终 DONE if (event.type === MetonaStreamEventType.DONE) continue; // 过滤掉 RETRY 类型的 ERROR 事件 — 不转发到前端,避免触发虚假错误 UI // RETRY 事件仅用于 Engine 内部清空缓冲区(见下方 switch 分支) if (event.type === MetonaStreamEventType.ERROR && (event.error?.code as string) === 'RETRY') { // 内部处理:清空已累积的内容和缓冲区(重试会从头开始接收) fullContent = ''; reasoningContent = ''; toolCallsBuffer.clear(); continue; } // 转发流式事件到渲染进程(注入 runId 供前端过滤旧流) this.emit('streamEvent', { ...event, runId: this.runId }); switch (event.type) { case MetonaStreamEventType.TEXT_DELTA: if (event.delta) fullContent += event.delta; break; case MetonaStreamEventType.REASONING_DELTA: if (event.delta) reasoningContent += event.delta; break; case MetonaStreamEventType.TOOL_CALL_DELTA: if (event.toolCallDelta) { const { index, name, argsDelta } = event.toolCallDelta; if (!toolCallsBuffer.has(index)) { toolCallsBuffer.set(index, { name: name ?? '', argsBuffer: '' }); } const buf = toolCallsBuffer.get(index)!; if (name) buf.name = name; if (argsDelta) buf.argsBuffer += argsDelta; } break; case MetonaStreamEventType.TOOL_CALL_COMPLETE: if (event.toolCall) { // 工具调用完成,记录到 step if (!step.toolCalls) step.toolCalls = []; step.toolCalls.push(event.toolCall); } break; case MetonaStreamEventType.USAGE: if (event.usage) { tokenUsage = { promptTokens: event.usage.inputTokens ?? 0, completionTokens: event.usage.outputTokens ?? 0, totalTokens: event.usage.totalTokens ?? 0, }; } break; case MetonaStreamEventType.ERROR: // RETRY 已在循环入口过滤,此处只处理真正的错误 if (event.error) { throw new Error(event.error.message); } break; } } // === v0.2.0: PARSING 状态 — 解析流式缓冲区中的工具调用 === await this.transitionTo(AgentLoopState.PARSING); // 处理缓冲区中的工具调用(TOOL_CALL_DELTA 拼接) if (toolCallsBuffer.size > 0 && (!step.toolCalls || step.toolCalls.length === 0)) { step.toolCalls = []; for (const [, buf] of toolCallsBuffer) { let args: Record; try { args = buf.argsBuffer ? JSON.parse(buf.argsBuffer) : {}; } catch { // JSON 解析失败,使用空参数(LLM 仍请求了该工具调用) args = {}; } step.toolCalls.push({ id: `tc_${nanoid(8)}`, name: buf.name, args, iteration: this.currentIteration, timestamp: Date.now(), }); } } // 记录 Thought if (fullContent || reasoningContent) { step.thought = { id: `thought-${this.currentIteration}`, content: fullContent, reasoningContent, timestamp: Date.now(), iteration: this.currentIteration, }; } // 记录 Token 使用 if (tokenUsage) { step.tokenUsage = tokenUsage; this.accumulateTokens(tokenUsage); } // v0.3.0 修复: 死循环检测 — 在 PARSING 阶段完成后、EXECUTING 阶段开始前检测 // 确保第3轮重复调用的副作用不会产生(工具尚未执行) if (step.toolCalls && step.toolCalls.length > 0) { if (this.detectDeadLoop(step.toolCalls)) { log.warn(`[AgentLoop] Dead loop detected at iteration ${this.currentIteration} (before tool execution)`); this.emit('deadLoop', { iteration: this.currentIteration, runId: this.runId, sessionId: this.currentSessionId, }); // 抛出特殊错误,主循环捕获后以 DEAD_LOOP 原因终止 throw new DeadLoopError( `Detected a potential infinite loop: the same tool calls were repeated for 3 consecutive iterations. Please refine the approach or provide more specific instructions.`, ); } } // === EXECUTING: 执行工具调用 === if (step.toolCalls && step.toolCalls.length > 0) { // transitionTo 已发射 stateChange 事件,无需重复 emit await this.transitionTo(AgentLoopState.EXECUTING); // 并行执行所有工具调用(独立工具之间无依赖,可安全并发) const executeAndForward = async (tc: MetonaToolCall): Promise => { const result = await this.executeToolSafely(tc); // H-5: abort 后不转发工具结果(避免 DONE 后到达的残留事件污染新流) if (!this.aborted) { // 立即转发工具结果到渲染进程(不等其他工具完成) this.emit('streamEvent', { type: MetonaStreamEventType.TOOL_RESULT, requestId: request.meta.requestId, sessionId, iteration: this.currentIteration, seq: this.nextSeq(), timestamp: Date.now(), toolResult: result, runId: this.runId, }); } return result; }; // H-5: abort 时提前退出工具执行等待,不再阻塞至所有工具完成 const toolsPromise = Promise.allSettled( step.toolCalls.map((tc) => executeAndForward(tc)), ); const controller = this.abortController; let onAbort: (() => void) | null = null; const abortPromise = new Promise((resolve) => { if (this.aborted) return resolve(null); if (controller) { onAbort = () => resolve(null); controller.signal.addEventListener('abort', onAbort, { once: true }); } }); const raceResult = await Promise.race([toolsPromise, abortPromise]) as | PromiseSettledResult[] | null; // v0.3.0 修复: 若 toolsPromise 先完成(正常执行),手动移除 abort 监听器,避免监听器堆积 if (onAbort && controller && raceResult !== null) { controller.signal.removeEventListener('abort', onAbort); } if (raceResult === null) { // 被 abort 中断,标记步骤并退出 step.completedAt = Date.now(); step.state = AgentLoopState.TERMINATED; return step; } const settledResults = raceResult; step.toolResults = settledResults.map((result, idx) => { const tc = step.toolCalls![idx]; if (result.status === 'fulfilled') return result.value; // rejected:构造失败 result const errorResult = { toolCallId: tc.id, toolName: tc.name, result: null, success: false, error: `Tool execution failed: ${(result.reason as Error)?.message ?? String(result.reason)}`, durationMs: 0, timestamp: Date.now(), }; // 转发错误结果到 UI this.emit('streamEvent', { type: MetonaStreamEventType.TOOL_RESULT, requestId: request.meta.requestId, sessionId, iteration: this.currentIteration, seq: this.nextSeq(), timestamp: Date.now(), toolResult: errorResult, runId: this.runId, }); return errorResult; }); } // === OBSERVING === await this.transitionTo(AgentLoopState.OBSERVING); // === v0.2.0: REFLECTING 状态 — 观察工具结果,决定是否继续 === // 如果有工具调用且需要后续推理,进入 REFLECTING 状态 if (this.config.enableReflection && step.toolCalls && step.toolCalls.length > 0) { await this.transitionTo(AgentLoopState.REFLECTING); // 检查工具执行是否有错误,如果有严重错误可以提前终止 const hasErrors = step.toolResults?.some((r) => !r.success); if (hasErrors) { log.warn(`[AgentLoop] Iteration ${this.currentIteration} had tool errors`); } } // === 上下文压缩(基于 token 使用率触发) === // 有效上下文窗口:Ollama 使用 contextLength (numCtx),其他 Provider 使用 contextWindow const effectiveContextWindow = this.config.contextLength ?? this.config.contextWindow; const estimatedTokens = this.estimateMessagesTokens(request.messages); const compressionThreshold = this.config.compressionThreshold * effectiveContextWindow; if (estimatedTokens > compressionThreshold && request.messages.length > 10) { await this.transitionTo(AgentLoopState.COMPRESSING); const compressed = await this.compressMessages(request.messages); if (compressed) { // 原地替换数组内容,确保外层 messages 引用同步更新 request.messages.splice(0, request.messages.length, ...compressed); this.emit('compressed', { iteration: this.currentIteration, originalTokens: estimatedTokens, compressedTokens: this.estimateMessagesTokens(compressed), }); } // 压缩后回到 OBSERVING await this.transitionTo(AgentLoopState.OBSERVING); } step.completedAt = Date.now(); step.state = AgentLoopState.OBSERVING; return step; } catch (error) { step.completedAt = Date.now(); step.state = AgentLoopState.TERMINATED; // v0.3.0 修复: DeadLoopError 抛出时,将 step 加入 iterations 数组, // 确保死循环轮的 LLM thought 内容不丢失(用户可观察 Agent 被终止前的最后思考) if (error instanceof DeadLoopError) { this.iterations.push(step); } throw error; } } /** * 安全执行工具调用(经过 Hook 管道) */ private async executeToolSafely(toolCall: MetonaToolCall): Promise { const startTs = Date.now(); if (!this.toolRegistry) { return { toolCallId: toolCall.id, toolName: toolCall.name, result: null, success: false, error: `Tool '${toolCall.name}' not available: no ToolRegistry configured`, durationMs: Date.now() - startTs, timestamp: Date.now(), }; } // 前置 Hook 管道 for (const hook of this.preToolHooks) { const result = await hook.beforeExecute(toolCall, this.currentSessionId); if (result.blocked) { return { toolCallId: toolCall.id, toolName: toolCall.name, result: null, success: false, error: `Blocked: ${result.reason}`, durationMs: Date.now() - startTs, timestamp: Date.now(), }; } } // 执行工具(带超时) // 兜底超时取 max(配置值, 工具自定义 timeoutMs),确保工具能跑满自己声明的超时 const configuredTimeout = this.config.toolExecutionTimeoutMs ?? 120_000; const toolDef = this.toolRegistry.get(toolCall.name)?.definition; const toolTimeout = Math.max(configuredTimeout, toolDef?.timeoutMs ?? 0); let toolResult: MetonaToolResult; try { toolResult = await Promise.race([ this.toolRegistry.execute(toolCall, { sessionId: this.currentSessionId ?? '', workspacePath: this.workspacePath, iteration: this.currentIteration, requestId: this.currentRequestId, }), new Promise((_, reject) => setTimeout(() => reject(new Error(`Tool '${toolCall.name}' timed out after ${toolTimeout}ms`)), toolTimeout), ), ]); } catch (err) { toolResult = { toolCallId: toolCall.id, toolName: toolCall.name, result: null, success: false, error: (err as Error).message, durationMs: 0, timestamp: Date.now(), }; // 仍执行 post-hook for (const hook of this.postToolHooks) { await hook.afterExecute(toolCall, toolResult, this.currentSessionId); } return toolResult; } // 后置 Hook 管道 for (const hook of this.postToolHooks) { await hook.afterExecute(toolCall, toolResult, this.currentSessionId); } return toolResult; } /** * 带重试的流式调用(v0.2.0: 指数退避) * * 如果 adapter 抛出错误,在 retryCount 次数内重试。 * v0.2.0: 使用指数退避替代固定 1 秒等待 * 等待时间 = baseDelay * 2^attempt(1s, 2s, 4s, 8s...) * 上限 30 秒,加上 ±20% 随机抖动(jitter)避免惊群效应 */ private async *chatStreamWithRetry(request: MetonaRequest): AsyncIterable { let lastError: unknown; const baseDelayMs = 1_000; const maxDelayMs = 30_000; for (let attempt = 0; attempt <= this.config.retryCount; attempt++) { try { // 首次尝试直接 yield if (attempt === 0) { yield* this.adapter.chatStream(request); return; } // 重试时:先发送一个 retry 事件,让 UI 清空已接收的 delta yield { type: MetonaStreamEventType.ERROR, requestId: request.meta.requestId, sessionId: request.meta.sessionId, iteration: request.meta.iteration, seq: 0, timestamp: Date.now(), error: { code: 'RETRY' as never, message: `Retrying after error (attempt ${attempt + 1}/${this.config.retryCount + 1})`, retryable: true, }, } as MetonaStreamEvent; yield* this.adapter.chatStream(request); return; } catch (error) { lastError = error; if (this.aborted) throw error; if (attempt < this.config.retryCount) { // 检查是否为可重试错误 if (!this.isRetryableError(error)) throw error; // 指数退避 + 抖动 const delay = Math.min(maxDelayMs, baseDelayMs * Math.pow(2, attempt)); const jitter = delay * 0.2 * (Math.random() * 2 - 1); // ±20% jitter const waitMs = Math.max(500, delay + jitter); log.warn(`[AgentLoop] Retry ${attempt + 1}/${this.config.retryCount} after ${Math.round(waitMs)}ms: ${(error as Error).message}`); await new Promise((resolve, reject) => { const timer = setTimeout(() => { // v0.3.0 修复: timer 先触发时移除 abort 监听器,避免监听器堆积 if (onAbort && signal) signal.removeEventListener('abort', onAbort); resolve(undefined); }, waitMs); // 支持 abort 中断等待 const signal = this.abortController?.signal; let onAbort: (() => void) | null = null; if (signal) { if (signal.aborted) { clearTimeout(timer); reject(new Error('Aborted')); return; } onAbort = () => { clearTimeout(timer); reject(new Error('Aborted')); }; signal.addEventListener('abort', onAbort, { once: true }); } }); } } } throw lastError; } /** 判断错误是否可重试 */ private isRetryableError(error: unknown): boolean { const err = error as { status?: number; code?: string; message?: string }; // 429 Too Many Requests — 可重试 if (err.status === 429) return true; // 5xx 服务器错误 — 可重试 if (err.status && err.status >= 500 && err.status < 600) return true; // 网络超时/连接错误 — 可重试 if (err.code === 'ECONNRESET' || err.code === 'ETIMEDOUT' || err.code === 'ENOTFOUND') return true; // SSE 流中断 — 可重试 if (err.message?.includes('aborted') || err.message?.includes('socket hang up')) return true; // 其他错误(400/401/403/4xx)不重试 return false; } /** 生成下一个事件序列号 */ private nextSeq(): number { return ++this.eventSeq; } private async transitionTo(state: AgentLoopState): Promise { const previous = this.currentState; this.currentState = state; this.emit('stateChange', { previous, current: state, state, sessionId: this.currentSessionId, iteration: this.currentIteration, runId: this.runId, }); } /** * 估算消息列表的 token 数 * 使用智能字符估算:中文 1.5 token/字,ASCII 0.25 token/字,其他 1 token/字 * @see electron/harness/utils/token-estimator.ts */ private estimateMessagesTokens(messages: MetonaMessage[]): number { return estimateMessagesTokens(messages); } /** * v0.3.0: 死循环检测 * * 检测策略: * 将每轮的工具调用序列化为签名字符串,检查最近3轮的签名是否完全相同。 * 如果连续3轮使用完全相同的参数调用相同的工具,判定为死循环。 * * v0.3.0 修复: * - 对 args 的键进行排序,避免 JSON.stringify 键顺序不一致导致漏报 * * @param toolCalls 当前轮次的工具调用 * @returns 是否检测到死循环 */ private detectDeadLoop(toolCalls: MetonaToolCall[]): boolean { // 将当前轮次的工具调用序列化为签名 // v0.3.0 修复:使用 stable stringify,对对象键排序,确保相同内容不同键顺序产生相同签名 // v0.3.0 修复:添加 visited Set 防循环引用,深度上限防过度递归 const stableStringify = (obj: unknown, visited: Set = new Set(), depth = 0): string => { if (depth > 10) return '...'; // 深度上限防止过度递归 if (obj === null || typeof obj !== 'object') return JSON.stringify(obj); if (visited.has(obj)) return '"[Circular]"'; // 循环引用防护 visited.add(obj); try { if (Array.isArray(obj)) return `[${obj.map((v) => stableStringify(v, visited, depth + 1)).join(',')}]`; const keys = Object.keys(obj as Record).sort(); return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify((obj as Record)[k], visited, depth + 1)}`).join(',')}}`; } finally { visited.delete(obj); } }; const signature = toolCalls .map((tc) => `${tc.name}(${stableStringify(tc.args)})`) .join('|'); this.toolCallHistory.push(signature); // 只保留最近5轮的记录(足够检测3轮重复,同时避免内存增长) if (this.toolCallHistory.length > 5) { this.toolCallHistory.shift(); } // 需要至少3轮数据才能检测 if (this.toolCallHistory.length < 3) return false; const len = this.toolCallHistory.length; const r1 = this.toolCallHistory[len - 1]; // 当前轮 const r2 = this.toolCallHistory[len - 2]; // 上一轮 const r3 = this.toolCallHistory[len - 3]; // 上上一轮 // 连续3轮完全相同 → 死循环 return r1 === r2 && r2 === r3; } /** * 上下文压缩 — 将旧消息摘要为一条 system 消息,保留最近 3 轮完整对话 * * 策略: * 1. 保留最后 keepRecent 条消息(约 3 轮对话) * 2. 将前面的所有消息交给 LLM 生成摘要 * 3. 用 [Context Summary] system 消息 + 近期消息替换原数组 * * @returns 压缩后的消息数组,压缩失败时返回 null(调用方保持原数组) */ private async compressMessages(messages: MetonaMessage[]): Promise { const keepRecent = 10; // 保留最近 10 条消息(约 3 轮 user+assistant+tool) if (messages.length <= keepRecent) return null; const toCompress = messages.slice(0, messages.length - keepRecent); const toKeep = messages.slice(messages.length - keepRecent); // 构建摘要请求 // 不截断单条消息——摘要请求是独立 API 调用,不共享主对话上下文窗口 const conversationText = toCompress.map((m) => { const role = m.role.toUpperCase(); return `[${role}] ${m.content}`; }).join('\n\n'); const summaryRequest: MetonaRequest = { meta: { sessionId: this.currentSessionId, iteration: this.currentIteration, requestId: `r_${nanoid(12)}`, timestamp: Date.now(), agentVersion: '1.0.0', }, systemPrompt: { roleDefinition: 'You are a conversation summarizer.', outputConstraints: 'Summarize the following conversation history concisely. Preserve key facts, decisions, tool results, and context needed for future reasoning. Output in the same language as the conversation. Maximum 300 words.', safetyGuidelines: 'Do not include sensitive data like passwords or API keys in the summary.', }, messages: [{ role: 'user', content: `Please summarize the following conversation history:\n\n${conversationText}`, timestamp: Date.now(), }], params: { maxTokens: 2048, temperature: 0.0, stream: false, thinkingEnabled: false, thinkingEffort: 'low', }, }; try { const response = await this.adapter.chat(summaryRequest); const summary = response.content.trim(); if (!summary) return null; const summaryMessage: MetonaMessage = { role: 'system', content: `[Context Summary] The following is a summary of earlier conversation:\n\n${summary}`, timestamp: Date.now(), }; log.info(`[AgentLoop] Context compressed: ${toCompress.length} messages → 1 summary, kept ${toKeep.length} recent`); return [summaryMessage, ...toKeep]; } catch (error) { log.warn('[AgentLoop] Context compression failed, keeping original messages:', (error as Error).message); return null; } } private accumulateTokens(usage: TokenUsage): void { this.totalTokens.promptTokens += usage.promptTokens; this.totalTokens.completionTokens += usage.completionTokens; this.totalTokens.totalTokens += usage.totalTokens; } private finish( reason: TerminationReason, answer?: string, error?: Error, ): AgentLoopOutput { // 发送唯一的最终 DONE 事件 — UI 只在此处结束流式状态 this.emit('streamEvent', { type: MetonaStreamEventType.DONE, requestId: this.currentRequestId, sessionId: this.currentSessionId, iteration: this.currentIteration, seq: this.nextSeq(), timestamp: Date.now(), runId: this.runId, }); // H-3: 通过 stateChange 发射 TERMINATED 状态(前端可据此清理 UI) const prevTerminated = this.currentState; this.currentState = AgentLoopState.TERMINATED; this.emit('stateChange', { previous: prevTerminated, current: AgentLoopState.TERMINATED, state: AgentLoopState.TERMINATED, sessionId: this.currentSessionId, iteration: this.currentIteration, runId: this.runId, }); // 发射 complete 事件(供托盘通知等外部监听器使用) this.emit('complete', { sessionId: this.currentSessionId, durationMs: Date.now() - this.startTime, terminationReason: reason, iterations: this.iterations.length, totalTokens: this.totalTokens.totalTokens, }); return { finalAnswer: answer ?? (error ? error.message : 'No answer produced'), terminationReason: reason, iterations: this.iterations, totalTokenUsage: this.totalTokens, durationMs: Date.now() - this.startTime, metadata: { config: this.config, error: error?.message }, }; } }