/**
 * 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 { resolve } from 'path';
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, MetonaErrorCode } from '../types';
import { estimateMessagesTokens } from '../utils/token-estimator';
import { ContentFilterError } from '../adapters/base-adapter';
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;
  /**
   * v0.3.18 修复: 最近一次 LLM 调用返回的真实输入 token 数
   * 用于校正压缩判断——估算值可能偏低（尤其中文场景），
   * 导致估算没到阈值不压缩，但真实 API 调用已经 413。
   * 压缩判断时取 max(估算值, 真实值) 作为实际占用。
   */
  private lastRealInputTokens = 0;
  /** 当前 run 的唯一标识（用于前端过滤旧流事件） */
  private runId: string = '';
  /** 正在进行的 run Promise（用于 run lock，防止并发 run 污染状态） */
  private currentRunPromise: Promise<AgentLoopOutput> | null = null;

  /** v0.3.0: 工具调用签名历史（用于死循环检测） */
  private toolCallHistory: string[] = [];

  constructor(
    config: Partial<AgentLoopConfig> = {},
    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;
    // #3 修复: 热切换 adapter 时同步 contextWindow，防止压缩阈值计算错误
    this.syncContextWindow();
  }

  /**
   * #3 修复: 从 adapter 同步 contextWindow 到 Engine 配置
   *
   * Engine 的 DEFAULT_CONFIG.contextWindow 硬编码为 128_000，但各 Provider 实际支持的
   * 上下文窗口差异巨大（DeepSeek 64K / Agnes 1M / Ollama 4096）。
   * 不同步会导致压缩阈值（compressionThreshold * contextWindow）计算错误。
   */
  private syncContextWindow(): void {
    const adapterCtx = this.adapter.getContextWindow?.();
    if (adapterCtx && adapterCtx > 0) {
      this.config.contextWindow = adapterCtx;
    }
  }

  /**
   * 热更新 Engine 配置（设置变更时调用）
   *
   * 支持 maxIterations, totalTimeoutMs, thinkingEnabled, thinkingEffort, contextLength
   */
  updateConfig(partial: Partial<AgentLoopConfig>): 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<AgentLoopOutput> {
    // 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<AgentLoopOutput> {
    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 };
    // v0.3.18 修复: 每个 run 重置 lastRealInputTokens，避免跨 run 污染
    this.lastRealInputTokens = 0;
    this.abortController = new AbortController();
    // C-2 修复: 将 abortController 的 signal 注入到 adapter，
    // 使正在进行的 fetch 可被用户中断，避免资源泄漏
    if (this.adapter.setAbortSignal) {
      this.adapter.setAbortSignal(this.abortController.signal);
    }
    // #3 修复: 每次 runStream 开始时从 adapter 同步 contextWindow，
    // 确保压缩阈值基于当前 Provider 的实际上下文窗口
    this.syncContextWindow();
    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',
              // CE-2 修复: 工具失败时 result.result 为 null，LLM 会看到 "null" 而非错误信息
              // 优先使用 error 字段，让 LLM 知道失败原因，避免重复调用导致死循环
              content: result.error
                ? result.error
                : (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 原因终止
      // P0-1 审查修复: 必须传 error 参数（第三参数），否则 finish() 不会发射 ERROR 事件
      // 前端只收到 DONE 会导致 agentStatus 被设为 'idle' 而非 'error'，且无错误消息卡片
      if (error instanceof DeadLoopError) {
        return this.finish(TerminationReason.DEAD_LOOP, undefined, error as Error);
      }
      const errMsg = (error as Error).message ?? '';
      // P2-9 修复: toLowerCase 避免大小写敏感导致超时误判为 ERROR
      // Node fetch 超时错误 "The operation timed out" / abort "Aborted" 都需覆盖
      const errMsgLower = errMsg.toLowerCase();
      if (this.aborted || errMsgLower.includes('aborted') || errMsgLower.includes('timed out') || errMsgLower.includes('timeout')) {
        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;
  }

  /**
   * #4 修复: 恢复 adapter 的 abort signal
   *
   * SubEngine 共享主 Engine 的 adapter 时，SubEngine 会覆盖 adapter 的 abort signal。
   * SubEngine 完成后，主 Engine 需调用此方法恢复自己的 signal，否则后续 fetch 无法被中断。
   */
  restoreAbortSignal(): void {
    if (this.abortController && this.adapter.setAbortSignal) {
      this.adapter.setAbortSignal(this.abortController.signal);
    }
  }

  /** 获取工作空间路径（供 SubAgent 继承） */
  getWorkspacePath(): string {
    return this.workspacePath;
  }

  /** 中断循环 */
  abort(): void {
    this.aborted = true;
    this.abortController?.abort();
    this.abortController = null;
    // C-2 修复: 清除 adapter 的 abort signal，防止旧的已 abort signal 影响后续请求
    if (this.adapter.setAbortSignal) {
      this.adapter.setAbortSignal(undefined);
    }
    this.emit('aborted');
  }

  /**
   * MT-1 修复: 等待当前 run 结束（用于 abortSession IPC handler）
   *
   * abort() 只是设置了标志和触发了 abortController，
   * 但 currentRunPromise 仍在进行中（可能在等待工具超时或 LLM 响应）。
   * 如果不等待就返回，用户立即重发会导致新消息卡在 runStream 的 currentRunPromise 等待中。
   *
   * @param timeoutMs 等待超时（默认 5 秒，防止永久挂起）
   * @returns true 表示 run 已结束，false 表示等待超时
   */
  async waitForAbort(timeoutMs: number = 5_000): Promise<boolean> {
    if (!this.currentRunPromise) return true;
    // #29 修复: 原实现 Promise.race 永不抛错（currentRunPromise.catch 已吞异常），
    // 导致 try/catch 永远走不到，总是返回 true。调用方误以为 abort 完成，
    // 立即重发的新消息会卡在 runStream 的 currentRunPromise 等待中。
    // 改为用 timedOut 标志检测超时，超时返回 false 让调用方提示用户"上一次操作未完成"。
    let timedOut = false;
    // 审查修复: 用 finally 块清理 timer，避免 currentRunPromise 先完成时 setTimeout 残留
    let timerHandle: NodeJS.Timeout | undefined;
    const timer = new Promise<void>((resolve) => {
      timerHandle = setTimeout(() => {
        timedOut = true;
        resolve();
      }, timeoutMs);
    });

    try {
      await Promise.race([
        this.currentRunPromise.catch(() => {}),
        timer,
      ]);
      return !timedOut; // 超时返回 false，run 正常结束返回 true
    } finally {
      // 审查修复: 无论 race 谁先完成，都清理 timer 防止事件循环残留
      if (timerHandle) clearTimeout(timerHandle);
    }
  }

  /**
   * 销毁引擎，清理所有监听器
   */
  destroy(): void {
    this.abort();
    this.removeAllListeners();
  }

  /** 获取当前状态 */
  getState(): AgentLoopState {
    return this.currentState;
  }

  // ========== 私有方法 ==========

  /**
   * 执行单次迭代（流式模式）
   */
  private async executeOneIterationStream(
    request: MetonaRequest,
    sessionId: string,
  ): Promise<IterationStep> {
    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<number, { name: string; argsBuffer: string }>();
      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 分支）
        // H-11 修复: 使用 MetonaErrorCode.RETRY 替代 'as string' 强制转换，确保类型安全
        if (event.type === MetonaStreamEventType.ERROR && event.error?.code === MetonaErrorCode.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,
              };
              // v0.3.18 修复: 记录最近一次 LLM 调用的真实输入 token，用于校正压缩判断
              // 估算值可能偏低（尤其中文场景），导致不压缩但 API 413
              const realInput = event.usage.inputTokens ?? 0;
              if (realInput > 0) {
                this.lastRealInputTokens = realInput;
              }
            }
            break;

          case MetonaStreamEventType.ERROR:
            // RETRY 已在循环入口过滤，此处只处理真正的错误
            if (event.error) {
              throw new Error(event.error.message);
            }
            break;
        }
      }

      // === v0.2.0: PARSING 状态 — 解析流式缓冲区中的工具调用 ===
      await this.transitionTo(AgentLoopState.PARSING);

      // L-19 修复: 提取 finalizeToolCallsFromBuffer 子方法（PARSING 阶段）
      this.finalizeToolCallsFromBuffer(step, toolCallsBuffer);

      // 记录 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);

        // L-19 修复: 提取 executeToolCallsParallel 子方法（EXECUTING 阶段）
        // 返回 null 表示被 abort 中断
        const raceResult = await this.executeToolCallsParallel(step.toolCalls, request.meta.requestId, sessionId);

        if (raceResult === null) {
          // 被 abort 中断，标记步骤并退出
          step.completedAt = Date.now();
          step.state = AgentLoopState.TERMINATED;
          return step;
        }

        step.toolResults = raceResult.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
      // v0.3.18 修复: 加默认值 128_000 保护，避免 config 都为 undefined 时 compressionThreshold 变 NaN 导致压缩永不触发
      const effectiveContextWindow = this.config.contextLength ?? this.config.contextWindow ?? 128_000;
      const estimatedTokens = this.estimateMessagesTokens(request.messages);
      // v0.3.18 修复: 取 max(估算值, 真实值) 作为实际占用，避免估算偏低导致不压缩但 API 413
      // 估算值用于 LLM 尚未返回 usage 时的早期判断（首轮或重试场景）
      // 真实值用于校正——LLM 返回的 inputTokens 是 BPE 真实分词结果，比字符估算准确
      const actualTokens = Math.max(estimatedTokens, this.lastRealInputTokens);
      const compressionThreshold = this.config.compressionThreshold * effectiveContextWindow;
      // v0.3.18 修复: 触发条件从"消息数 > 10"改为"消息数 >= 4"
      // 新压缩策略按 token 预算动态截断，不再依赖固定 10 条。
      // 至少 4 条消息（2 轮 user+assistant）才有压缩意义，否则保留区已是最小。
      if (actualTokens > compressionThreshold && request.messages.length >= 4) {
        await this.transitionTo(AgentLoopState.COMPRESSING);
        const compressed = await this.compressMessages(request.messages);
        if (compressed) {
          // 原地替换数组内容，确保外层 messages 引用同步更新
          request.messages.splice(0, request.messages.length, ...compressed);
          // v0.3.18 修复: 压缩后重置 lastRealInputTokens，下一轮 LLM 调用会返回新的（更小的）真实值
          this.lastRealInputTokens = 0;
          this.emit('compressed', {
            iteration: this.currentIteration,
            originalTokens: actualTokens,
            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;
    }
  }

  /**
   * L-19 修复: PARSING 阶段 — 解析流式接收期间累积的工具调用缓冲区，
   * 构造 MetonaToolCall[] 写入 step.toolCalls。
   *
   * 仅在缓冲区非空且 step 尚未通过 TOOL_CALL_COMPLETE 接收到完整调用时生效，
   * 避免覆盖已就绪的 toolCalls。
   */
  private finalizeToolCallsFromBuffer(
    step: IterationStep,
    toolCallsBuffer: Map<number, { name: string; argsBuffer: string }>,
  ): void {
    if (toolCallsBuffer.size > 0 && (!step.toolCalls || step.toolCalls.length === 0)) {
      step.toolCalls = [];
      for (const [, buf] of toolCallsBuffer) {
        let args: Record<string, unknown>;
        try {
          args = buf.argsBuffer ? JSON.parse(buf.argsBuffer) : {};
        } catch {
          args = {};
        }
        step.toolCalls.push({
          id: `tc_${nanoid(8)}`,
          name: buf.name,
          args,
          iteration: this.currentIteration,
          timestamp: Date.now(),
        });
      }
    }
  }

  /**
   * L-19 修复: EXECUTING 阶段 — 并行执行工具调用，转发结果到 UI，
   * 并与 abort 信号竞速。返回 null 表示被 abort 中断。
   *
   * 注意：rejected 的工具结果由调用方处理（构造失败 result 并转发），
   * 此处只负责转发 fulfilled 的结果。
   */
  private async executeToolCallsParallel(
    toolCalls: MetonaToolCall[],
    requestId: string,
    sessionId: string,
  ): Promise<PromiseSettledResult<MetonaToolResult>[] | null> {
    const executeAndForward = async (tc: MetonaToolCall): Promise<MetonaToolResult> => {
      const result = await this.executeToolSafely(tc);
      // 执行成功后转发结果到 UI（abort 后不再转发，避免污染新会话的流）
      if (!this.aborted) {
        this.emit('streamEvent', {
          type: MetonaStreamEventType.TOOL_RESULT,
          requestId,
          sessionId,
          iteration: this.currentIteration,
          seq: this.nextSeq(),
          timestamp: Date.now(),
          toolResult: result,
          runId: this.runId,
        });
      }
      return result;
    };

    // C-2 修复: 使用 Promise.allSettled 而非 Promise.all，确保单个工具失败不影响其他工具
    const toolsPromise = Promise.allSettled(toolCalls.map((tc) => executeAndForward(tc)));

    const controller = this.abortController;
    let onAbort: (() => void) | null = null;
    const abortPromise = new Promise<null>((resolve) => {
      if (this.aborted) {
        resolve(null);
        return;
      }
      if (controller) {
        onAbort = () => resolve(null);
        controller.signal.addEventListener('abort', onAbort, { once: true });
      }
    });

    const raceResult = (await Promise.race([toolsPromise, abortPromise])) as
      | PromiseSettledResult<MetonaToolResult>[]
      | null;

    // 清理 abort 监听器，避免事件循环中残留
    if (onAbort && controller && raceResult !== null) {
      controller.signal.removeEventListener('abort', onAbort);
    }

    return raceResult;
  }

  /**
   * 安全执行工具调用（经过 Hook 管道）
   */
  private async executeToolSafely(toolCall: MetonaToolCall): Promise<MetonaToolResult> {
    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(),
        };
      }
    }

    // H-5 修复: 精确保护工作空间根目录的 MEMORY.md
    // @see project_memory.md — Only the MEMORY.md in the workspace root directory is protected;
    //   subdirectory MEMORY.md files are unrestricted
    // 之前 permissions.ts 使用 /MEMORY\.md/i 粗粒度正则会误拦子目录的 MEMORY.md，
    // 现在改为在工具执行层进行精确校验，只阻止对根目录 MEMORY.md 的读写。
    // run_command 由 permissions.ts 的粗粒度正则保留保护（命令解析复杂）。
    if (['read_file', 'write_file', 'file_editor'].includes(toolCall.name)) {
      if (this.isTargetingRootMemoryMd(toolCall)) {
        return {
          toolCallId: toolCall.id, toolName: toolCall.name,
          result: null, success: false,
          error: 'Access to workspace root MEMORY.md is protected by security policy',
          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;
    // M-16 修复: 使用 try/finally 清理 setTimeout，防止事件循环 timer 堆积
    // 默认 120 秒超时下，多轮迭代会堆积大量未触发 timer
    let engineTimer: ReturnType<typeof setTimeout> | undefined;
    try {
      toolResult = await Promise.race([
        this.toolRegistry.execute(toolCall, {
          sessionId: this.currentSessionId ?? '',
          workspacePath: this.workspacePath,
          iteration: this.currentIteration,
          requestId: this.currentRequestId,
        }),
        new Promise<MetonaToolResult>((_, reject) => {
          engineTimer = 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;
    } finally {
      // M-16 修复: 清理未触发的 timeout timer
      if (engineTimer) clearTimeout(engineTimer);
    }

    // 后置 Hook 管道
    for (const hook of this.postToolHooks) {
      await hook.afterExecute(toolCall, toolResult, this.currentSessionId);
    }

    return toolResult;
  }

  /**
   * H-5 修复: 检查工具调用是否针对工作空间根目录的 MEMORY.md
   *
   * @see project_memory.md — Only the MEMORY.md in the workspace root directory is protected;
   *   subdirectory MEMORY.md files are unrestricted
   *
   * 之前 permissions.ts 使用 /MEMORY\.md/i 粗粒度正则会误拦子目录的 MEMORY.md，
   * 现在改为在工具执行层进行精确校验，只阻止对根目录 MEMORY.md 的读写。
   *
   * @param toolCall 工具调用
   * @returns 是否指向工作空间根目录的 MEMORY.md
   */
  private isTargetingRootMemoryMd(toolCall: MetonaToolCall): boolean {
    if (!this.workspacePath) return false;

    // 提取工具参数中的路径（不同工具使用不同的参数名）
    const args = toolCall.args;
    const pathStr = (args.path as string) || (args.file_path as string) ||
      (args.filePath as string) || (args.file as string) ||
      (args.target as string) || (args.destination as string);

    if (!pathStr || typeof pathStr !== 'string') return false;

    // 解析路径，判断是否指向工作空间根目录的 MEMORY.md
    // 使用 toLowerCase 处理 Windows 不区分大小写的文件系统
    const resolved = resolve(pathStr).toLowerCase();
    const rootMemoryPath = resolve(this.workspacePath, 'MEMORY.md').toLowerCase();

    // 精确匹配：路径必须等于 {workspacePath}/MEMORY.md
    return resolved === rootMemoryPath;
  }

  /**
   * 带重试的流式调用（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<MetonaStreamEvent> {
    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.sendStream(request);
          return;
        }
        // 重试时：先发送一个 retry 事件，让 UI 清空已接收的 delta
        // H-11 修复: 使用 MetonaErrorCode.RETRY 替代 'RETRY' as never，移除不安全的类型断言
        yield {
          type: MetonaStreamEventType.ERROR,
          requestId: request.meta.requestId,
          sessionId: request.meta.sessionId,
          iteration: request.meta.iteration,
          seq: 0,
          timestamp: Date.now(),
          error: {
            code: MetonaErrorCode.RETRY,
            message: `Retrying after error (attempt ${attempt + 1}/${this.config.retryCount + 1})`,
            retryable: true,
          },
        };
        yield* this.adapter.sendStream(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;
    // P2-9 一致性修复: toLowerCase 避免大小写敏感漏判
    // SSE 流中断 — 可重试（注意：用户主动 abort 已在 chatStreamWithRetry 入口由 this.aborted 提前拦截）
    const msg = err.message?.toLowerCase() ?? '';
    if (msg.includes('aborted') || msg.includes('socket hang up')) return true;
    // 其他错误（400/401/403/4xx）不重试
    return false;
  }

  /** 生成下一个事件序列号 */
  private nextSeq(): number {
    return ++this.eventSeq;
  }

  private async transitionTo(state: AgentLoopState): Promise<void> {
    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.0 token/字，ASCII 0.25 token/字，其他 1 token/字
   * v0.3.18: CJK 系数从 1.5 调整为 1.0，更贴近 BPE 实际值
   * @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<unknown> = 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<string, unknown>).sort();
        return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify((obj as Record<string, unknown>)[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;
  }

  /**
   * 上下文压缩 — 将旧消息摘要为一条 assistant 消息，保留近期完整对话
   *
   * v0.3.18 修复: 保留策略从"固定 10 条"改为"按 token 预算动态截断"
   * 之前固定保留最近 10 条，若 10 条里有超长 tool_result（如 read_file 读了 5000 行），
   * 压缩后仍超 token 限制，下一轮 LLM 调用会返回 413。
   * 现在按 effectiveContextWindow 的 50% 预算从末尾反向累加消息，
   * 直到预算用尽或只剩 1 条可压缩消息。
   *
   * 策略：
   * 1. 计算 keepBudget = effectiveContextWindow * 0.5（保留区预算）
   * 2. 从末尾反向累加消息到 toKeep，直到累加 token 超过 keepBudget
   * 3. 将前面的所有消息交给 LLM 生成摘要
   * 4. 用 [Context Summary] assistant 消息 + 占位 user + 近期消息替换原数组
   * 5. 若单条消息超 keepBudget（超长 tool_result），单独二次截断
   *
   * @returns 压缩后的消息数组，压缩失败时返回 null（调用方保持原数组）
   */
  private async compressMessages(messages: MetonaMessage[]): Promise<MetonaMessage[] | null> {
    // v0.3.18 修复: 动态计算保留预算，避免固定 10 条在超长消息场景仍超限
    // 加默认值 128_000 保护，避免 config 都为 undefined 时 keepBudget 变 NaN
    const effectiveContextWindow = this.config.contextLength ?? this.config.contextWindow ?? 128_000;
    const keepBudget = Math.floor(effectiveContextWindow * 0.5); // 保留区占上下文窗口 50%
    const minKeepCount = 2; // 至少保留最后 2 条（user + assistant），保证有可推理上下文

    // 从末尾反向累加，确定 toKeep 范围
    let keepStartIdx = messages.length;
    let keepTokens = 0;
    for (let i = messages.length - 1; i >= 0; i--) {
      const msgTokens = this.estimateMessagesTokens([messages[i]]);
      if (keepTokens + msgTokens > keepBudget && i < messages.length - minKeepCount) {
        break;
      }
      keepTokens += msgTokens;
      keepStartIdx = i;
    }

    // 至少保留 minKeepCount 条，且至少要有 1 条可压缩
    if (keepStartIdx < minKeepCount) keepStartIdx = minKeepCount;
    if (keepStartIdx >= messages.length) return null;

    let toCompress = messages.slice(0, keepStartIdx);
    let toKeep = messages.slice(keepStartIdx);

    // MT-3 修复: 确保 toKeep 不以孤立的 tool 消息开头
    // OpenAI/DeepSeek API 要求 tool 消息前必须有带 tool_calls 的 assistant 消息
    // 如果压缩边界正好切在 assistant(tool_calls) 和 tool 之间，下一轮 LLM 调用会返回 400
    if (toKeep.length > 0 && toKeep[0].role === 'tool' && toCompress.length > 0) {
      // 反向查找最近的带 tool_calls 的 assistant 消息
      let assistantIdx = -1;
      for (let i = toCompress.length - 1; i >= 0; i--) {
        if (toCompress[i].role === 'assistant' && toCompress[i].toolCalls?.length) {
          assistantIdx = i;
          break;
        }
      }
      if (assistantIdx < 0) {
        // 找不到配对的 assistant 消息，直接丢弃孤立的 tool 消息
        toKeep = toKeep.slice(1);
      } else {
        // 将配对的 assistant 消息及其之后的所有消息移到 toKeep 开头
        const moved = toCompress.slice(assistantIdx);
        toCompress = toCompress.slice(0, assistantIdx);
        toKeep = [...moved, ...toKeep];
      }
    }

    // 如果 toCompress 为空，无法生成摘要
    if (toCompress.length === 0) return null;

    // 构建摘要请求
    // 不截断单条消息——摘要请求是独立 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.send(summaryRequest);
      const summary = response.content.trim();

      if (!summary) return null;

      const summaryMessage: MetonaMessage = {
        // #30 修复: 改用 assistant 角色注入摘要，避免语义混淆
        // 原 CE-1 修复用 'user' 角色，会导致 LLM 将摘要误视为新的用户指令，
        // 可能基于"Summary of previous conversation"字面意思执行奇怪操作。
        // 工单建议方案 A（system 角色）不可行：buildOpenAICompatibleMessages 会
        // 过滤所有 role === 'system' 的消息（只保留 systemPrompt 构建的 system 消息），
        // 用 system 角色摘要会被丢弃，压缩无效。
        // 采用 assistant 角色：既不会被过滤，又保持语义中立（摘要是 AI 生成的总结），
        // LLM 不会将其视为新的用户指令。
        role: 'assistant',
        content: `[Context Summary] The following is a summary of earlier conversation:\n\n${summary}`,
        timestamp: Date.now(),
      };

      // v0.3.18 修复: 若 toKeep 中仍有单条消息超 keepBudget，对其做二次截断
      // 超长 tool_result（如 read_file 5000 行）即使保留也会撑爆上下文
      const finalKeep = toKeep.map((msg) => {
        const msgTokens = this.estimateMessagesTokens([msg]);
        if (msgTokens > keepBudget && msg.content) {
          // 截断内容，保留头部和尾部，中间用省略标记
          const halfBudget = Math.floor(keepBudget / 2);
          // v0.3.18 修复: charsPerToken 从 2 调整为 1.0（与 CJK_TOKEN_RATIO 一致）
          // 原值 2 对中文偏激进（2 字符/token），实际中文约 1 字符/token，
          // 导致截断后保留字符过多，实际 token 仍超 keepBudget
          const charsPerToken = 1.0;
          const keepChars = Math.floor(halfBudget * charsPerToken);
          if (msg.content.length > keepChars * 2) {
            const head = msg.content.slice(0, keepChars);
            const tail = msg.content.slice(-keepChars);
            return {
              ...msg,
              content: `${head}\n\n... [truncated, ${msgTokens} tokens] ...\n\n${tail}`,
            };
          }
        }
        return msg;
      });

      log.info(`[AgentLoop] Context compressed: ${toCompress.length} messages → 1 summary, kept ${finalKeep.length} recent (${keepTokens} tokens budget)`);

      // 审查修复: #30 修复将摘要改为 assistant 角色，可能导致连续两个 assistant 消息
      // (summary + 带 tool_calls 的 assistant)，部分 Provider 会返回 400。
      // 插入占位 user 消息保证对话流清晰。
      return [
        summaryMessage,
        // 审查修复: 插入占位 user 消息避免连续 assistant 消息
        { role: 'user', content: '[Continue from the summary above.]', timestamp: Date.now() },
        ...finalKeep,
      ];
    } 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 {
    // P0-1 修复: ERROR/DEAD_LOOP 终止时先发 ERROR 流式事件，让前端能看到错误
    // v0.3.0 删除了 emit('error') 导致所有 adapter 错误对前端不可见
    // 此处用 emit('streamEvent', { type: ERROR }) 不会触发 EventEmitter 的同步 throw
    if ((reason === TerminationReason.ERROR || reason === TerminationReason.DEAD_LOOP) && error) {
      // v0.3.17: 识别 ContentFilterError，映射为 CONTENT_FILTERED 错误码 + 友好消息
      const isContentFilter = error instanceof ContentFilterError;
      this.emit('streamEvent', {
        type: MetonaStreamEventType.ERROR,
        requestId: this.currentRequestId,
        sessionId: this.currentSessionId,
        iteration: this.currentIteration,
        seq: this.nextSeq(),
        timestamp: Date.now(),
        runId: this.runId,
        error: {
          code: isContentFilter ? MetonaErrorCode.CONTENT_FILTERED : MetonaErrorCode.UNKNOWN,
          message: isContentFilter
            ? '内容被 Provider 安全审核拦截，请修改图片或文本后重试'
            : error.message,
          retryable: false,
        },
      });
    }

    // 发送唯一的最终 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,
      terminationReason: reason,
    });

    // 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 },
    };
  }
}
