import { spawn } from "node:child_process";
import type { AgentSdkAdapter, AgentCreateSpec, AgentSendSpec } from "./AgentSdkAdapter.ts";
import { normalizeUiLang, type UiLang } from "../panel/PanelUiLang.ts";
import {
  buildStaticGoogleTools,
  defaultLeaderGoogleTools,
  parseAllowedToolsFromEnv,
  resolveOneShotPythonBin,
  shouldUseFcopOneShot,
} from "./FcopMcpOneShot.ts";
import { ensureAgentSkillRoutingInSystemPrompt } from "../skills/AgentSkillRouting.ts";
import { resolveAndInjectAgentContextSkills } from "../skills/SkillContextRouter.ts";
import { ensurePmCoreCapabilitiesInSystemPrompt } from "../pm/PmCoreCapabilities.ts";
import { buildGeminiUserParts } from "./geminiMultimodalParts.ts";
import {
  buildGeminiGenerateConfig,
  consumeGeminiContentStream,
} from "./geminiContentStream.ts";
import { GeminiChatMemory, type GeminiChatContent } from "./geminiChatMemory.ts";
import {
  buildFunctionResponseUserTurn,
  isMeaningfulGeminiPart,
  repairFunctionResponseUserTurn,
  sanitizeGeminiContents,
  stripFunctionCallTurns,
  validateFunctionResponseAlignment,
  type GeminiToolResult,
} from "./geminiChatHistory.ts";
import {
  functionCallOrderMismatch,
  resolveOrderedFunctionCalls,
} from "./geminiFunctionCallsOrder.ts";
import { sanitizeSchemaForGemini } from "./geminiSchemaSanitize.ts";
import { buildGeminiWorkspaceToolDeclarations } from "./geminiWorkspaceTools.ts";
import {
  buildPatrolSummaryFromCache,
  buildWriteReportAckBreakSummary,
  formatReusedToolOutput,
  hasWriteReportDoneInCache,
  recordToolDedupeResult,
  resolveToolDedupe,
  shouldBreakPatrolToolLoop,
  shouldBreakWriteReportAckLoop,
  toolDedupeKey,
} from "./geminiToolDedupe.ts";
import {
  releaseWriteReportReserve,
  tryReserveWriteReportExecute,
  WRITE_REPORT_DEDUPE_REUSE_MESSAGE,
} from "./geminiWriteReportGuard.ts";
import {
  formatWriteReportTaskIdMismatchPayload,
  guardWriteReportTaskId,
  resolveExplicitSessionPinnedTaskId,
  resolveSessionPinnedTaskId,
  WRITE_REPORT_TASK_ID_AUTOFILL_TAG,
} from "./geminiWriteReportTaskIdGuard.ts";
import { maybeRecordActionEvidenceFromToolCall } from "../logs/ActionEvidenceFromToolCall.ts";
import { extractTaskIdPrefixesFromText } from "../pm/pmAdminRejectPrompt.ts";
import {
  readonlyParallelForCall,
  runPreparedToolSlots,
  type GeminiFunctionCallLike,
  type PreparedToolSlot,
} from "./geminiToolParallel.ts";
import {
  isAdminRejectColdPathPrompt,
  isPmDevColdDispatchContext,
  isPmSelfExecuteHotPathPrompt,
  isPmSelfReportOnlyMode,
  isRuntimeTaskDispatchPrompt,
  PM_SELF_REPORT_ONLY_BLOCKED,
  resolveGeminiToolSelectionMode,
  shouldSplitPromptOnSystemDelimiter,
  GEMINI_TOOL_SCHEMA_TOKEN_ESTIMATE,
} from "./geminiToolSelection.ts";
import { PM_DEV_COLD_DISPATCH_BLOCKED } from "../pm/pmDevDispatchPolicy.ts";
import { guardPmDevDispatchWriteReport } from "../pm/guardPmDevDispatchWriteReport.ts";
import {
  guardPmProductWorkerWriteTask,
  pinPmWorkerTaskLineage,
} from "../pm/ProductDeliveryRuntimeGate.ts";
import { describeGoogleGeminiRuntime } from "./googleGeminiRuntime.ts";
import {
  filterGoogleToolsByMode,
  resolveGoogleToolMode,
  VISION_INSPECT_ATTACHMENT_TOOL,
  type GoogleToolMode,
} from "./GoogleToolMode.ts";
import {
  classifyGoogleContractToolOutcome,
  createGoogleContractState,
  recordGoogleContractFrMismatch,
  recordGoogleContractHitMaxRounds,
  recordGoogleContractInvalidOutput,
  recordGoogleContractModelNotFound,
  recordGoogleContractToolResult,
  recordGoogleContractZeroToolcall,
  shouldStopGoogleContractLoop,
} from "./FcopGoogleRuntimeContract.ts";
import { settleGoogleFcopRun } from "./ReportSettlement.ts";
import {
  buildGoogleVisionToolDeclarations,
  routeGoogleToolCall,
} from "./GoogleToolCallRouter.ts";
import {
  formatGeminiModelNotFoundMessage,
  isGeminiModelNotFoundError,
} from "./googleGeminiApiErrors.ts";
import { isLeaderRoleAgentId } from "../skill/FcopToolProfile.ts";
import {
  EXECUTOR_ADAPTER_BLOCKED_RULE_EN,
  EXECUTOR_ADAPTER_BLOCKED_RULE_ZH,
  PM_REWORK_ADAPTER_BODY_CHECKLIST_ZH,
} from "./googleAdapterAcceptance.ts";
import type { RunHandle, RuntimeEvent, RuntimeEventType, Unsubscribe } from "../types/state.ts";
import type { SessionRun } from "@codeflowmu/protocol";
import {
  buildGoogleFailurePayloadFields,
  type SessionRunWithGoogleFailure,
} from "../session/google-failure-detector.ts";

/** Optional failure context passed into `GoogleRunHandle.settle` on failed runs. */
export interface GoogleSettleFailureContext {
  error_message?: string;
  failed_tool_names?: string[];
  hit_max_rounds?: boolean;
  raw?: unknown;
  failure_code?: string;
  operation_fingerprint?: string;
  operation_classification?: "approval_required" | "approval_creation_pending";
  retry_policy?: "none";
  next_safe_action?: string;
  report_required?: boolean;
  operation_outcome?: Record<string, unknown>;
}

const GEMINI_BACKOFF_BASE_MS = 1_000;
const GEMINI_BACKOFF_CAP_MS = 20_000;
const MCP_DEFAULT_CALL_TIMEOUT_MS = 45_000;
const MCP_TOOLS_LIST_TIMEOUT_MS = 15_000;
const MCP_INITIALIZE_TIMEOUT_MS = 15_000;
const GOOGLE_REQUEST_GAP_MS = 500;

let googleGenerateQueue: Promise<unknown> = Promise.resolve();

function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

export function googleRequestGapMs(): number {
  return parseNonNegativeInt(
    process.env.CODEFLOW_GOOGLE_REQUEST_GAP_MS,
    GOOGLE_REQUEST_GAP_MS,
  );
}

export function enqueueGoogleGenerate<T>(task: () => Promise<T>): Promise<T> {
  const previous = googleGenerateQueue.catch(() => undefined);
  const run = previous.then(task);
  googleGenerateQueue = run.then(
    async () => {
      await sleep(googleRequestGapMs());
    },
    async () => {
      await sleep(googleRequestGapMs());
    },
  );
  return run;
}

type GeminiToolDecl = {
  name: string;
  description: string;
  parameters: unknown;
};

export function appendAuxiliaryGoogleToolDeclarations(input: {
  mode: GoogleToolMode;
  googleTools: GeminiToolDecl[];
  agentId: string;
  pmSelfReportOnly: boolean;
  pmDevColdDispatch: boolean;
  taskDispatchForWorkspace: boolean;
  selectedToolNames: string[];
}): void {
  const existing = new Set(input.googleTools.map((t) => t.name));
  const leaderAgent = isLeaderRoleAgentId(input.agentId);
  const add = (decl: GeminiToolDecl) => {
    if (!existing.has(decl.name)) {
      input.googleTools.push(decl);
      existing.add(decl.name);
    }
  };

  const wantsVision =
    input.selectedToolNames.includes(VISION_INSPECT_ATTACHMENT_TOOL) ||
    input.mode === "fcop_minimal" ||
    input.mode === "workspace_read";
  if (wantsVision) {
    for (const decl of buildGoogleVisionToolDeclarations()) {
      add(decl);
    }
  }

  if (input.mode === "fcop_minimal") {
    for (const decl of buildGeminiWorkspaceToolDeclarations({
      includeWriteFile: false,
      includeListDir: false,
    })) {
      if (
        decl.name.startsWith("web_") ||
        decl.name === "skill_search" ||
        decl.name === "skill_learn" ||
        (leaderAgent && decl.name === "skill_publish")
      ) {
        add(decl);
      }
    }
    return;
  }

  const includeListDir =
    !input.pmSelfReportOnly &&
    (leaderAgent || !input.taskDispatchForWorkspace);

  if (input.mode === "workspace_read") {
    for (const decl of buildGeminiWorkspaceToolDeclarations({
      includeWriteFile: false,
      includeListDir,
    })) {
      if (decl.name !== "write_file" && decl.name !== "skill_publish") {
        add(decl);
      }
    }
    return;
  }

  for (const decl of buildGeminiWorkspaceToolDeclarations({
    includeWriteFile:
      leaderAgent && !input.pmSelfReportOnly && !input.pmDevColdDispatch,
    includeListDir,
  })) {
    if (decl.name !== "skill_publish" || leaderAgent) add(decl);
  }
}

function parseRouterAuthorityDenied(
  error?: string,
): { code: string; message: string; role?: string; toolName?: string } | null {
  if (!error) return null;
  try {
    const parsed = JSON.parse(error) as {
      ok?: boolean;
      error?: {
        code?: string;
        message?: string;
        role?: string;
        toolName?: string;
      };
    };
    if (parsed?.error?.code === "AUTHORITY_DENIED") {
      return parsed.error as {
        code: string;
        message: string;
        role?: string;
        toolName?: string;
      };
    }
  } catch {
    /* not JSON authority payload */
  }
  return null;
}

/**
 * GoogleRunHandle — RunHandle implementation for Google Gemini API runs.
 * Matches all events emitted by Cursor SDK runs for seamless compatibility.
 */
export class GoogleRunHandle implements RunHandle {
  readonly run_id: string;
  readonly session_id: string;
  readonly agent_id: string;
  
  private _active = true;
  private readonly _listeners = new Set<(event: RuntimeEvent) => void>();
  private readonly _settlePromise: Promise<SessionRun>;
  private _resolveSettle!: (run: SessionRun) => void;
  private _rejectSettle!: (err: Error) => void;
  private readonly _startedAt: string;

  constructor(opts: { runId: string; sessionId: string; agentId: string }) {
    this.run_id = opts.runId;
    this.session_id = opts.sessionId;
    this.agent_id = opts.agentId;
    this._startedAt = new Date().toISOString();
    
    this._settlePromise = new Promise((resolve, reject) => {
      this._resolveSettle = resolve;
      this._rejectSettle = reject;
    });
  }

  isActive(): boolean {
    return this._active;
  }

  async cancel(reason: string): Promise<void> {
    void reason;
    if (!this._active) return;
    this._active = false;
    
    this.settle("cancelled", 0);
  }

  whenSettled(): Promise<SessionRun> {
    return this._settlePromise;
  }

  onEvent(listener: (event: RuntimeEvent) => void): Unsubscribe {
    this._listeners.add(listener);
    return () => {
      this._listeners.delete(listener);
    };
  }

  emit(type: RuntimeEventType, payload?: unknown): void {
    const event: RuntimeEvent = {
      event_id: `evt-${Math.random().toString(36).slice(2, 10)}`,
      at: new Date().toISOString(),
      event_type: type,
      session_id: this.session_id,
      run_id: this.run_id,
      agent_id: this.agent_id,
      payload,
    };
    
    for (const listener of this._listeners) {
      try {
        listener(event);
      } catch (err) {
        console.error(`[GoogleRunHandle] listener threw on run_id="${this.run_id}":`, err);
      }
    }
  }

  settle(
    status: SessionRun["status"],
    toolCallsCount: number,
    failureCtx?: GoogleSettleFailureContext,
  ): void {
    this._active = false;
    const endedAt = new Date().toISOString();
    const durationMs = Math.max(
      0,
      new Date(endedAt).getTime() - new Date(this._startedAt).getTime(),
    );
    const isFailed = String(status).toLowerCase() === "failed";
    const sdkFailureFields =
      isFailed
        ? buildGoogleFailurePayloadFields({
            status: "error",
            tool_call_count: toolCallsCount,
            duration_ms: durationMs,
            raw:
              failureCtx?.raw ??
              (failureCtx?.error_message
                ? { status: "error", message: failureCtx.error_message }
                : { status: "error" }),
            error_message: failureCtx?.error_message,
            failed_tool_names: failureCtx?.failed_tool_names,
            hit_max_rounds: failureCtx?.hit_max_rounds,
            agent_id: this.agent_id,
            session_id: this.session_id,
          })
        : undefined;

    const sessionRun: SessionRun & SessionRunWithGoogleFailure = {
      run_id: this.run_id,
      started_at: this._startedAt,
      ended_at: endedAt,
      status,
      tool_calls_count: toolCallsCount,
      ...(isFailed && failureCtx?.error_message
        ? { sdk_error: failureCtx.error_message }
        : {}),
      ...(isFailed && sdkFailureFields
        ? { sdk_failure_detail: sdkFailureFields }
        : {}),
      ...(failureCtx?.failure_code ? { failure_code: failureCtx.failure_code } : {}),
      ...(failureCtx?.operation_fingerprint
        ? { operation_fingerprint: failureCtx.operation_fingerprint }
        : {}),
      ...(failureCtx?.operation_classification
        ? { operation_classification: failureCtx.operation_classification }
        : {}),
      ...(failureCtx?.retry_policy ? { retry_policy: failureCtx.retry_policy } : {}),
      ...(failureCtx?.next_safe_action
        ? { next_safe_action: failureCtx.next_safe_action }
        : {}),
      ...(failureCtx?.report_required != null
        ? { report_required: failureCtx.report_required }
        : {}),
      ...(failureCtx?.operation_outcome
        ? { operation_outcome: failureCtx.operation_outcome }
        : {}),
    };
    this._resolveSettle(sessionRun);
  }
}

/**
 * McpClient — Minimal Standard JSON-RPC 2.0 Stdio client to invoke local MCP Server tools.
 */
class McpClient {
  private static startLock: Promise<void> = Promise.resolve();

  private process: any = null;
  private messageId = 0;
  private buffer = "";
  private pendingResolves = new Map<number, (res: any) => void>();

  constructor(
    private command: string,
    private args: string[],
    private cwd?: string,
    private extraEnv?: Record<string, string>
  ) {}

  async start(): Promise<void> {
    const prevLock = McpClient.startLock;
    let resolveLock!: () => void;
    McpClient.startLock = new Promise<void>((resolve) => {
      resolveLock = resolve;
    });

    await prevLock;
    try {
      await this._startInternal();
    } finally {
      resolveLock();
    }
  }

  private async _startInternal(): Promise<void> {
    let cmd = this.command === "python" || this.command === "python3" 
      ? process.env.PYTHON_BIN || this.command 
      : this.command;

    let spawnArgs = [...this.args];

    // 🚨 完美跨平台适配：在 Windows 下若命令是 tsx，我们寻找项目内可用的 tsx.cmd 绝对路径，并使用 shell: true 启动
    // 这比拼装 node --import tsx 更加鲁棒，因为 npm 软链接的 bin 脚本会自动处理好模块加载路径，绝不会发生 module 找不到的 code 1 崩溃！
    let useShell = false;
    if (process.platform === "win32" && cmd === "tsx") {
      const path = await import("node:path");
      const fs = await import("node:fs");
      
      const rootDir = this.cwd || process.cwd();
      const localTsxCmd = path.join(rootDir, "node_modules", ".bin", "tsx.cmd");
      const shellTsxCmd = path.join(rootDir, "codeflowmu-shell", "node_modules", ".bin", "tsx.cmd");
      
      if (fs.existsSync(localTsxCmd)) {
        cmd = localTsxCmd;
        useShell = true;
      } else if (fs.existsSync(shellTsxCmd)) {
        cmd = shellTsxCmd;
        useShell = true;
      } else {
        // 兜底方案
        cmd = "node";
        spawnArgs = ["--import", "tsx", ...this.args];
      }
    }

    // 🚨 完美开启 Python 无缓冲运行：当命令是 python 时，args 加入 -u，并设置 PYTHONUNBUFFERED 环境变量
    // 这彻底解决了 Python 在重定向 stdout 时因为全缓冲导致 stdio 卡死和 MCP Tool call timeout (initialize) 的惊天大 Bug！
    const isPython = cmd.toLowerCase().includes("python") || this.command.toLowerCase().includes("python");
    if (isPython && !spawnArgs.includes("-u")) {
      spawnArgs.unshift("-u");
    }

    // 🚨 注入 NODE_PATH：彻底打通 Windows 下 tsx 模块路径，作为双重保险
    const path = await import("node:path");
    const shellNodeModules = path.join(this.cwd || process.cwd(), "codeflowmu-shell", "node_modules");
    const runtimeNodeModules = path.join(this.cwd || process.cwd(), "packages", "codeflowmu-runtime", "node_modules");
    
    const env: Record<string, string | undefined> = { 
      ...process.env,
      ...this.extraEnv,
      NODE_PATH: [shellNodeModules, runtimeNodeModules, process.env.NODE_PATH].filter(Boolean).join(path.delimiter)
    };

    if (isPython) {
      env.PYTHONUNBUFFERED = "1";
    }

    console.log(`[GoogleGenAiAdapter] McpClient spawning: "${cmd}" ${spawnArgs.join(" ")}`);

    this.process = spawn(cmd, spawnArgs, {
      cwd: this.cwd || process.cwd(),
      env,
      ...(useShell ? { shell: true } : {})
    });

    return new Promise((resolve, reject) => {
      let started = false;

      this.process.on("error", (err: Error) => {
        console.error(`[GoogleGenAiAdapter] McpClient process error:`, err);
        if (!started) {
          reject(new Error(`Failed to start MCP Process: ${err.message}`));
        }
      });

      this.process.on("exit", (code: number | null, signal: string | null) => {
        console.warn(`[GoogleGenAiAdapter] McpClient process exited with code ${code}, signal ${signal}`);
        if (!started) {
          reject(new Error(`MCP Process exited prematurely with code ${code}`));
        }
      });

      this.process.stdout.on("data", (data: Buffer) => {
        this.buffer += data.toString();
        this._processBuffer();
      });

      this.process.stderr.on("data", (data: Buffer) => {
        console.error(`[GoogleGenAiAdapter] McpClient stderr: ${data.toString().trim()}`);
      });

      // 发起握手并在握手成功后 resolve
      this.call("initialize", {
        protocolVersion: "2024-11-05",
        capabilities: {},
        clientInfo: { name: "codeflowmu-gemini-bridge", version: "1.0.0" }
      }).then(() => {
        this.notify("notifications/initialized", {});
        started = true;
        resolve();
      }).catch((err) => {
        if (!started) {
          reject(err);
        }
      });
    });
  }

  async call(method: string, params: any): Promise<any> {
    if (!this.process) {
      throw new Error(`MCP Server not started`);
    }

    const id = ++this.messageId;
    const request = {
      jsonrpc: "2.0",
      id,
      method,
      params,
    };

    return new Promise((resolve, reject) => {
      const timeoutMs = mcpTimeoutMsFor(method);
      const timeout = setTimeout(() => {
        this.pendingResolves.delete(id);
        try {
          this.process?.kill();
        } catch {
          /* ignore */
        }
        this.process = null;
        reject(new Error(`MCP ${method} timeout after ${timeoutMs}ms`));
      }, timeoutMs);

      this.pendingResolves.set(id, (response: any) => {
        clearTimeout(timeout);
        if (response.error) {
          reject(new Error(response.error.message || `MCP Error`));
        } else {
          resolve(response.result);
        }
      });

      this.process.stdin.write(JSON.stringify(request) + "\n");
    });
  }

  notify(method: string, params: any): void {
    if (!this.process) return;
    const notification = {
      jsonrpc: "2.0",
      method,
      params,
    };
    this.process.stdin.write(JSON.stringify(notification) + "\n");
  }

  private _processBuffer() {
    let newlineIndex = this.buffer.indexOf("\n");
    while (newlineIndex !== -1) {
      const rawLine = this.buffer.slice(0, newlineIndex).trim();
      this.buffer = this.buffer.slice(newlineIndex + 1);
      
      if (rawLine) {
        try {
          const parsed = JSON.parse(rawLine);
          if (parsed.id && this.pendingResolves.has(parsed.id)) {
            const resolver = this.pendingResolves.get(parsed.id);
            this.pendingResolves.delete(parsed.id);
            if (resolver) resolver(parsed);
          }
        } catch {
          // 忽略非 JSON-RPC 噪声
        }
      }
      newlineIndex = this.buffer.indexOf("\n");
    }
  }

  isAlive(): boolean {
    return this.process != null && this.process.killed !== true;
  }

  async stop() {
    if (this.process) {
      this.process.kill();
      this.process = null;
    }
  }
}

/**
 * GoogleGenAiAdapter — Twin-Channel Google Gen AI SDK backend adapter.
 * Uses lazy loading to prevent start-up crash when the package is not installed.
 */
export type GoogleGenAiAdapterOptions = {
  apiKey?: string;
  /** Fallback when agent registry still has Cursor-style `default` / `auto`. */
  defaultModel?: string;
};

export class GoogleGenAiAdapter implements AgentSdkAdapter {
  private client: any = null;
  private readonly _knownAgents = new Set<string>();
  private readonly _chatMemory = new GeminiChatMemory();

  constructor(private opts: GoogleGenAiAdapterOptions) {}

  /**
   * Safe Lazy Loading client resolver to prevent start-up crash on missing package.
   * Integrates HTTP/HTTPS proxy auto-detection and custom baseURL override to support domestic environments.
   */
  private async _getClient(): Promise<any> {
    if (!this.client) {
      try {
        // 🚨 动态代理设置：如果有代理环境变量，使用 undici 的 ProxyAgent (Node 18+ 内置原生 fetch 调度器)
        const proxy = process.env.HTTP_PROXY || process.env.HTTPS_PROXY || process.env.http_proxy || process.env.https_proxy;
        if (proxy) {
          try {
            const { ProxyAgent, setGlobalDispatcher } = await import("undici");
            const proxyAgent = new ProxyAgent(proxy);
            setGlobalDispatcher(proxyAgent);
            console.log(`[GoogleGenAiAdapter] Global ProxyAgent set successfully to: ${proxy}`);
          } catch (proxyErr) {
            console.warn(`[GoogleGenAiAdapter] Failed to set undici proxy agent:`, proxyErr);
          }
        }

        // 🚨 动态懒加载：只有在运行 Google API 时才去引入包，若未安装将抛出极度友好的中文引导
        const { GoogleGenAI } = await import("@google/genai");

        // 🚨 允许通过 GEMINI_BASE_URL 或是 GEMINI_NEXT_GEN_API_BASE_URL 自定义中转 baseUrl
        // 在新版 SDK @google/genai 中，baseUrl 必须放在 httpOptions.baseUrl 中，而不是传给顶层 constructor
        const baseURL = process.env.GEMINI_BASE_URL || process.env.GEMINI_NEXT_GEN_API_BASE_URL;

        this.client = new GoogleGenAI({
          apiKey: this.opts.apiKey || process.env.GEMINI_API_KEY,
          httpOptions: baseURL ? { baseUrl: baseURL } : undefined
        });
      } catch (err: any) {
        console.error(`[GoogleGenAiAdapter] _getClient initialization failed with raw error:`, err);
        throw new Error(
          `【系统提示】Google Gen AI 模块初始化失败。错误原因: ${err.message || String(err)}。` +
          `请检查网络、模块安装情况或密钥有效性。`
        );
      }
    }
    return this.client;
  }

  async create(spec: AgentCreateSpec): Promise<{ sdk_agent_id: string }> {
    const id = `gemini-agent-${spec.agentId}`;
    this._knownAgents.add(id);
    return { sdk_agent_id: id };
  }

  async list(): Promise<string[]> {
    const listIds = new Set<string>();

    // 🟢 动态读取持久化 agents.json，获取之前登记过的所有 sdk_agent_id。
    // 这可以让无论为 Cursor 的旧 UUID 还是 Gemini 新 ID，都能 100% 对准，强力打通并消除「孤儿（orphaned）」死锁！
    try {
      const fs = await import("node:fs");
      const path = await import("node:path");

      const home = process.env.USERPROFILE || process.env.HOME || "C:\\Users\\Administrator";
      const projectsDir = path.join(home, ".codeflowmu", "projects");

      if (fs.existsSync(projectsDir)) {
        const subdirs = fs.readdirSync(projectsDir);
        for (const subdir of subdirs) {
          const agentsJsonPath = path.join(projectsDir, subdir, "agents.json");
          if (fs.existsSync(agentsJsonPath)) {
            try {
              const raw = fs.readFileSync(agentsJsonPath, "utf-8");
              const parsed = JSON.parse(raw);
              if (Array.isArray(parsed)) {
                for (const item of parsed) {
                  const id = item?.protocol?.sdk_agent_id;
                  if (id) {
                    listIds.add(id);
                  }
                }
              }
            } catch (err) {
              // Ignore single file parse errors
            }
          }
        }
      }
    } catch (readErr) {
      console.error(`[GoogleGenAiAdapter] Failed to dynamically sync agents.json during list():`, readErr);
    }

    // 🟢 合入内存中实时活跃的 ID
    for (const id of this._knownAgents) {
      listIds.add(id);
    }

    // 🟢 降级兜底方案
    if (listIds.size === 0) {
      const defaults = ["PM-01", "DEV-01", "OPS-01", "QA-01", "EVAL-01"];
      return defaults.map(role => `gemini-agent-${role}`);
    }

    return [...listIds];
  }

  async resume(sdkAgentId: string): Promise<void> {
    // 探活时执行懒加载校验，如果未安装则在此时安全报错
    await this._getClient();
    this._knownAgents.add(sdkAgentId);
  }

  async listModels(): Promise<string[]> {
    const fallback = [
      "gemini-3.5-flash",   // GA 2026-05-19，agentic/coding 主推
      "gemini-3.1-pro",    // Preview 2026-02-19，复杂推理
      "gemini-3.1-flash-lite", // GA 2026-05，低延迟
      "gemini-2.5-pro",    // 稳定兜底
      "gemini-2.5-flash",  // 稳定兜底（免费 tier 可用）
    ];
    try {
      const client = await this._getClient();
      const pager = await client.models.list();
      const rawModels: unknown[] = [];
      if (pager && Array.isArray((pager as { page?: unknown[] }).page)) {
        rawModels.push(...(pager as { page: unknown[] }).page);
      } else if (pager && typeof (pager as AsyncIterable<unknown>)[Symbol.asyncIterator] === "function") {
        for await (const item of pager as AsyncIterable<{ name?: string }>) {
          rawModels.push(item);
        }
      }
      const models = rawModels
        .map((m) => {
          const name = (m as { name?: string })?.name ?? "";
          return name.startsWith("models/") ? name.slice(7) : name;
        })
        .filter((name) => name.includes("gemini") || name.includes("learnlm"));
      if (models.length > 0) return [...new Set(models)].sort();
    } catch (err) {
      console.warn("[GoogleGenAiAdapter] models.list failed, using fallback:", err);
    }
    return fallback;
  }

  async send(spec: AgentSendSpec, sdkAgentId: string): Promise<RunHandle> {
    const uiLang: UiLang = normalizeUiLang(spec.uiLang);
    const handle = new GoogleRunHandle({
      runId: `run-g-${Math.random().toString(36).slice(2, 10)}`,
      sessionId: spec.sessionId,
      agentId: spec.agentId,
    });

    setImmediate(async () => {
      let mcpClient: McpClient | null = null;
      try {
        handle.emit("runtime.session_started");

        console.log(`[GoogleGenAiAdapter] ${describeGoogleGeminiRuntime()}`);

        // 1. Google mode must receive a concrete Gemini model id from the agent config.
        const model = resolveGoogleModel(spec.modelId, this.opts.defaultModel);

        // 2. 动态解析并初始化懒加载的 Google Client 
        const client = await this._getClient();

        // 3. 将传入的 mcpServers 转换为 Google Function Declarations
        let googleTools: any[] = [];
        let mcpConfig: any = null;
        const useFcopOneShot =
          shouldUseFcopOneShot() &&
          !!(spec.mcpServers && Object.keys(spec.mcpServers).length > 0);
        const seenToolKeysGlobal = new Set<string>();
        const toolResultCache = new Map<string, string>();
        const writeReportReservedKeys = new Set<string>();
        const routingAgentIdEarly = spec.agentId || sdkAgentId || "";
        const pmSelfReportOnly =
          spec.runMode === "pm_self_report_only" ||
          isPmSelfReportOnlyMode(
            spec.text,
            routingAgentIdEarly,
            spec.runMode,
          );
        const pmDevColdDispatch = isPmDevColdDispatchContext(
          spec.text,
          routingAgentIdEarly,
          spec.runMode,
        );
        const googleToolMode = resolveGoogleToolMode();
        let selectedFcopToolNames: string[] = [];

        if (spec.mcpServers && Object.keys(spec.mcpServers).length > 0) {
          const firstKey = Object.keys(spec.mcpServers)[0]!;
          mcpConfig = spec.mcpServers[firstKey];

          if (mcpConfig && mcpConfig.command) {
            if (useFcopOneShot) {
              const allowed = parseAllowedToolsFromEnv(mcpConfig.env);
              const catalog =
                allowed.length > 0
                  ? allowed
                  : defaultLeaderGoogleTools().map((t: any) => t.name);
              selectedFcopToolNames = filterGoogleToolsByMode({
                mode: googleToolMode,
                agentId: spec.agentId ?? sdkAgentId ?? "",
                allowedTools: catalog,
                promptText: spec.text,
                runMode: spec.runMode,
              });
              googleTools =
                selectedFcopToolNames.length > 0
                  ? buildStaticGoogleTools(selectedFcopToolNames)
                  : defaultLeaderGoogleTools();
              console.log(
                `[GoogleGenAiAdapter] Windows one-shot FCoP: ${describeGeminiToolBudget(catalog.length, googleTools.length)} mode=${googleToolMode} (no stdio MCP server)`,
              );
            } else {
              if (process.platform === "win32") {
                console.warn(
                  "[GoogleGenAiAdapter] Windows 使用长驻 stdio MCP（未启用 one-shot）。fcop_report/fcop_check 可能接近 90s；请重启 shell 并确认 packages 已更新。",
                );
              }
              mcpClient = new McpClient(
                mcpConfig.command,
                mcpConfig.args || [],
                mcpConfig.cwd,
                mcpConfig.env,
              );
              await mcpClient.start();

              const toolListRes = await mcpClient.call("tools/list", {});
              if (toolListRes && toolListRes.tools) {
                const allowed = (toolListRes.tools ?? []).map((t: any) =>
                  String(t.name),
                );
                selectedFcopToolNames = filterGoogleToolsByMode({
                  mode: googleToolMode,
                  agentId: spec.agentId ?? sdkAgentId ?? "",
                  allowedTools: allowed,
                  promptText: spec.text,
                  runMode: spec.runMode,
                });
                const selected = new Set(selectedFcopToolNames);
                googleTools = toolListRes.tools
                  .filter((t: any) => selected.has(String(t.name)))
                  .map((t: any) => {
                  const normalizedParams = t.inputSchema
                    ? sanitizeSchemaForGemini(t.inputSchema)
                    : { type: "OBJECT", properties: {} };
                  return {
                    name: t.name,
                    description: t.description || `Invoke ${t.name} tool`,
                    parameters: normalizedParams,
                  };
                });
                console.log(
                  `[GoogleGenAiAdapter] stdio FCoP: ${describeGeminiToolBudget(toolListRes.tools.length, googleTools.length)} mode=${googleToolMode}`,
                );
              }
            }
          }
        }

        const projectRootForWorkspace = mcpConfig?.cwd || process.cwd();
        const leaderAgent = isLeaderRoleAgentId(spec.agentId ?? "");
        const taskDispatchForWorkspace = isRuntimeTaskDispatchPrompt(spec.text);
        appendAuxiliaryGoogleToolDeclarations({
          mode: googleToolMode,
          googleTools,
          agentId: spec.agentId ?? sdkAgentId ?? "",
          pmSelfReportOnly,
          pmDevColdDispatch,
          taskDispatchForWorkspace,
          selectedToolNames: selectedFcopToolNames,
        });
        const includeListDirTool =
          !pmSelfReportOnly && (leaderAgent || !taskDispatchForWorkspace);
        const includeWriteFileLeader =
          leaderAgent &&
          !pmSelfReportOnly &&
          !pmDevColdDispatch &&
          googleToolMode !== "fcop_minimal" &&
          googleToolMode !== "workspace_read";
        console.log(
          `[GoogleGenAiAdapter] auxiliary tools mode=${googleToolMode} (root=${projectRootForWorkspace}) web_search, web_extract, web_research${googleToolMode === "fcop_minimal" ? "" : ", read_file, grep_files"}${googleToolMode !== "fcop_minimal" && includeListDirTool ? ", list_dir" : ""}${includeWriteFileLeader ? ", write_file (leader)" : ""}`,
        );

        // 4. 精准拆分系统提示词与任务主体，实现大模型的常驻灵魂身份定位！
        // 路由/工具裁剪须用完整 spec.text：chat 预载块在正文前含 `\n\n---`，若只查 split 后 userText 会漏掉 Cold Path 标记。
        const promptRoutingText = spec.text;
        const pinnedResolution = pmSelfReportOnly
          ? resolveExplicitSessionPinnedTaskId({
              pinnedTaskId: spec.pinnedTaskId,
              taskFilepath: spec.taskFilepath,
              frontmatterTaskId: spec.frontmatterTaskId,
            })
          : resolveSessionPinnedTaskId({
              pinnedTaskId: spec.pinnedTaskId,
              taskFilepath: spec.taskFilepath,
              frontmatterTaskId: spec.frontmatterTaskId,
              promptText: promptRoutingText,
            });
        const pinnedTaskId = pinnedResolution.pinnedTaskId;
        console.log(
          `[GoogleGenAiAdapter] session pinnedTaskId=${pinnedTaskId ?? "(none)"} source=${pinnedResolution.source} sessionId=${spec.sessionId ?? ""} agentId=${spec.agentId ?? sdkAgentId ?? ""}`,
        );
        const routingAgentId = spec.agentId || sdkAgentId || "";
        const adminRejectHotPath = isPmSelfExecuteHotPathPrompt(
          promptRoutingText,
          routingAgentId,
        );
        const adminRejectColdPath =
          !adminRejectHotPath &&
          isAdminRejectColdPathPrompt(promptRoutingText, routingAgentId);

        let systemInstruction: string | undefined = undefined;
        let userText = spec.text;

        if (shouldSplitPromptOnSystemDelimiter(spec.text, routingAgentId)) {
          const splitParts = spec.text.split("\n\n---");
          systemInstruction = splitParts[0];
          userText = splitParts.slice(1).join("\n\n---").trim();
        }

        // 🚨 极致兜底角色注入：如果在聊天模式下（没有 \n\n---），自动根据 sdkAgentId / spec.agentId 动态提取并生成强大的 FCoP 身份扮演 Prompt！
        if (!systemInstruction) {
          const targetId = spec.agentId || sdkAgentId || "";
          const match = targetId.match(/(PM|DEV|OPS|QA|REVIEW)(-\d+)?/i);
          const role = match?.[1]?.toUpperCase();
          if (role) {
            if (uiLang === "zh") {
              systemInstruction = `你是 ${targetId}（角色：${role}），CodeFlowMu 团队核心成员。
本角色 FCoP 职责要点：
${role === "PM" ? `- 认领 ADMIN 下发的任务。
- 分析需求并向 DEV/OPS/QA 派发 write_task（references + thread_key 必填；返工子任务 body 含 Adapter 六层验收）。
- ADMIN 打回 **Hot Path**：TASK 已预载则 PM **亲自** fcop_report → fcop_check → 探针 → write_report(status=done)；**不代表**可修改产品代码；若需代码/UI/API/测试实现则 write_task 派责任角色（DEV/OPS/QA/EVAL）。
- ADMIN 打回 **Cold Path**：TASK 已预载则**直接** write_task 派 DEV/QA/OPS。存在 DEV→QA 顺序时，先建 DEV，再建 QA；QA references 必须同时包含父 task_id 与本轮新 DEV task_id（工具支持时同步 depends_on）；thread_key 继承父任务，body 仅 Markdown，勿嵌 YAML。勿 read_task/read_file 同任务；勿仅 write_report ack。
- 汇总完成后 write_report 向 ADMIN 汇报；finish 并 archive。` : role === "REVIEW" ? `- 认领 PM/ADMIN 任务。
- 审查产物；写 REVIEW-*.md 并向 PM 汇报。
- finish 并 archive。` : `- 认领 PM 派发的任务。
- 执行实际工作；验收 Google 工具运行时（GoogleGenAiAdapter 全链路），非外部 Google Cloud。
- 用 write_report 向 PM 汇报（blocked 须附已调 tool 名与错误原文）；finish 并 archive。`}

严格遵守 FCoP v3。不得脱离角色或臆造。

**实时思考流（LIVE）**：你的内部推理、计划说明、工具调用前的思路必须使用**简体中文**。
巡查/开工后：必须用简体中文向 ADMIN 给出清晰总结（检查了什么、发现、下一步）；仅输出工具 JSON 不算回复。`;
            } else {
              systemInstruction = `You are ${targetId} (role: ${role}), a key member of the CodeFlowMu team.
Your core FCoP playbook responsibilities for this role:
${role === "PM" ? `- Claim incoming tasks from ADMIN.
- Analyze requirements and dispatch tasks to DEV, OPS, or QA using write_task (references + thread_key required; rework body includes Adapter six-layer acceptance).
- ADMIN reject **Hot Path**: when TASK is pre-loaded, PM executes personally — fcop_report → fcop_check → probes → write_report(status=done); do NOT write_task downstream.
- FCoP shell fallback: if MCP is unavailable, only use real read-only Python Project APIs such as status(), is_initialized(), list_tasks(), list_reports(), list_issues(); do not call Project.report() or Project.check().
- ADMIN reject **Cold Path**: when TASK is pre-loaded, write_task to DEV/QA/OPS first. For DEV→QA sequencing, create DEV first and make QA references include both the parent task id and the new DEV task id (also set depends_on when supported); inherit thread_key and keep body markdown-only. Do NOT read_task/read_file the same task; no write_report ack-only before dispatch.
- After downstream work completes, consolidate and write_report to ADMIN; finish and archive when authorised.` : role === "REVIEW" ? `- Claim tasks from PM/ADMIN.
- Inspect and audit artifacts.
- Write REVIEW-*.md and report to PM.
- Finish and archive tasks.` : `- Claim assigned tasks from PM.
- Perform actual work; validate Google tool runtime (GoogleGenAiAdapter chain), not external Google Cloud.
- Report to PM via write_report (blocked must cite tools tried + verbatim errors); finish and archive when authorised.`}

Always obey FCoP v3 protocol rules strictly. Do not hallucinate or act out of character. You are currently communicating directly with the user.

**LIVE thinking stream**: internal reasoning, planning notes, and pre-tool thoughts MUST be in **English**.
After any patrol or wake: end with a clear English summary for ADMIN (what you checked, findings, next steps). Tool JSON alone is not a reply.`;
            }
          }
        }

        // 🚨 全局 FCoP v3 工具调用强约束：无论如何，都将 FCoP v3 标准工具的正确参数规范作为系统指令后置注入
        // 这将百分之百消除因大模型幻觉/惯性引发的 Pydantic 校验报错（如 write_report 参数带上 sender/subject，或漏传 reporter 等）
        const adminRejectInPromptEarly =
          leaderAgent && (adminRejectHotPath || adminRejectColdPath);
        const leaderReadRule = leaderAgent
          ? adminRejectHotPath
            ? `1b. **Current TASK** is pre-loaded below — do NOT read_task/claim_task/read_file for that same task_id.
1c. **Hot Path ADMIN reject**: first actions MCP fcop_report → fcop_check → read_file/grep_files probes → write_report(status=done). Do NOT write_task to DEV/QA/OPS.
1d. **Other ledger docs**: use read_file (fcop paths) or list_tasks; read_task may be unavailable in one-shot mode.`
            : adminRejectInPromptEarly
            ? `1b. **Current TASK** is pre-loaded below — do NOT read_task/claim_task/read_file for that same task_id.
1c. **First action**: MCP write_task to DEV/QA/OPS. For DEV→QA sequencing, create DEV first, then create QA with references=[parent task_id, new DEV task_id] (and the same depends_on when supported); inherit thread_key and keep body markdown-only. Do NOT write_report ack-only before dispatch.
1d. **Other ledger docs**: use read_file (fcop paths) or list_tasks; read_task may be unavailable in one-shot mode.`
            : `1b. **Current TASK** is pre-loaded below — do NOT read_task/claim_task for that same task_id.
1c. **Other ledger docs**: use read_task, read_report, list_issues for sibling TASK/REPORT/ISSUE not pre-loaded.`
          : `1. **TASK body is already in this session** (injected by CodeFlowMu Runtime). Do NOT call claim_task / read_task / finish_task before starting work.`;

        const writeFileRule = leaderAgent
          ? `\n10. **Governance files**: write_file — allowlist only \`.codeflowmu/**\`, \`docs/skills/**\` (panel-ui-lang, skill manifests). Not for source or fcop envelopes.`
          : "";

        const adminRejectInPrompt = leaderAgent && adminRejectColdPath;
        const routingRole = (() => {
          const targetId = spec.agentId || sdkAgentId || "";
          const m = targetId.match(/(PM|DEV|OPS|QA|REVIEW)(-\d+)?/i);
          return m?.[1]?.toUpperCase();
        })();
        const patrolRule = adminRejectHotPath
          ? `7. ADMIN reject Hot Path (PM): TASK pre-loaded — fcop_report → fcop_check → read_file/grep_files probes → write_report(status=done); governance/coordination only — **not** product code edit; use write_task for DEV/OPS/QA/EVAL when implementation is needed.`
          : adminRejectColdPath && adminRejectInPromptEarly
            ? `7. ADMIN reject Cold Path (PM): TASK pre-loaded — **first** MCP write_task to DEV/QA/OPS. For DEV→QA, create DEV first and create QA with references=[parent task_id, new DEV task_id] (plus depends_on when supported); inherit thread_key and keep body markdown-only. Do not read_file/read_task same task; no write_report ack before dispatch.`
            : adminRejectInPrompt
              ? `7. ADMIN reject rework (PM): read_file → write_task 派 DEV/QA/OPS first; **禁止** 打回未派单前 write_report ack.`
              : `7. Patrol (PM): fcop_report / get_team_status / list_tasks — do not re-read the task already shown below.`;
        const adapterAcceptanceBlock =
          leaderAgent && adminRejectColdPath && adminRejectInPromptEarly
            ? `\n11. **PM rework dispatch body** must include Google tool runtime (Adapter) acceptance checklist:\n${PM_REWORK_ADAPTER_BODY_CHECKLIST_ZH}`
            : routingRole && routingRole !== "PM" && routingRole !== "REVIEW"
              ? `\n11. ${uiLang === "zh" ? EXECUTOR_ADAPTER_BLOCKED_RULE_ZH : EXECUTOR_ADAPTER_BLOCKED_RULE_EN}`
              : "";
        const toolGuideline = `\n\n[CRITICAL TOOL USAGE RULE - CodeFlowMu unified core: TASK → Adapter → REPORT → submit_review]
${leaderReadRule}
2. 'write_report' — completion signal (required: task_id, reporter, recipient, body; optional: status).
   ⚠️ NEVER pass 'sender' or 'subject' to write_report.
3. After REPORT: call submit_task (CodeFlowMu semantic: submit_review) to move active → review. Do NOT use finish_task as default (skips review/done authority per FCoP-PENDING-0001).
4. approve_task / archive_task — only if you hold done_authority / archive_authority (main line default: ADMIN).
5. 'write_issue' / 'drop_suggestion' — blockers and protocol feedback.
6. 'write_task' — PM/leader dispatch only (sender, recipient, subject, body required; optional references, thread_key, priority). **Never** put YAML frontmatter in body — MCP writes frontmatter. Use references (not parent) to link parent TASK.
${patrolRule}
8. ISSUE files are not TASK — use list_issues; never read_task on ISSUE paths.
9. Workspace: read_file / grep_files / list_dir — read or search skills/*/SKILL.md, agent-skills manifest, fcop ledger, source, and docs under project root. Use these instead of guessing file contents.
10. Web research: web_search discovers ranked source_url values; web_extract opens one source and extracts readable text/tables (dynamic=true uses Playwright); web_research runs search + extraction and returns a sourced report draft. Never claim web evidence without source_url.
11. Skill evolution: skill_search discovers local/GitHub/web candidates; skill_learn creates a source-backed learning pack and validation plan; skill_publish is leader-only and requires real validation evidence plus source URLs.${writeFileRule}${adapterAcceptanceBlock}`;

        // Shared Agent Skill Routing (same block as TaskDispatcher / Cursor SDK prefix).
        systemInstruction = ensureAgentSkillRoutingInSystemPrompt(systemInstruction ?? "");
        try {
          const chatSkills = await resolveAndInjectAgentContextSkills(projectRootForWorkspace, {
            role: routingRole || "UNKNOWN",
            message: spec.text,
            intent: "chat",
            sessionId: spec.sessionId,
            maxSkills: 3,
          });
          if (chatSkills.promptBlock) {
            systemInstruction += `\n\n${chatSkills.promptBlock}`;
          }
        } catch (err) {
          console.warn(
            "[skill-router] chat auto_inject failed:",
            err instanceof Error ? err.message : String(err),
          );
        }
        if (routingRole === "PM") {
          systemInstruction = ensurePmCoreCapabilitiesInSystemPrompt(systemInstruction);
        }
        systemInstruction += `\n\n[GOOGLE STREAM STABILITY]
- Keep LIVE thinking and pre-tool notes to at most 2 short sentences.
- Prefer making the needed tool call or returning the final summary over narrating a long plan.`;
        systemInstruction += toolGuideline;

        const userParts = await buildGeminiUserParts(userText, spec.images);
        if (spec.images?.length && userParts.every((p) => !("inlineData" in p))) {
          console.warn(
            `[GoogleGenAiAdapter] ${spec.images.length} image attachment(s) could not be encoded; sending text only.`,
          );
        } else if (spec.images?.length) {
          const imageCount = userParts.filter((p) => "inlineData" in p).length;
          console.log(
            `[GoogleGenAiAdapter] multimodal send: ${imageCount} inline image part(s) + text`,
          );
        }

        const isTaskDispatch = taskDispatchForWorkspace;
        const memoryKey = GeminiChatMemory.buildKey(sdkAgentId, spec.agentId);
        const priorContents = isTaskDispatch ? [] : this._chatMemory.load(memoryKey);
        let chatHistory: GeminiChatContent[] = sanitizeGeminiContents([
          ...priorContents,
          { role: "user", parts: userParts },
        ]);

        const generateConfig = buildGeminiGenerateConfig({
          systemInstruction,
          googleTools,
        });
        const geminiToolsEnabled = Boolean(
          (generateConfig as { tools?: unknown[] }).tools?.length,
        );

        let rounds = 0;
        const maxRounds = spec.maxToolRounds ?? 16;
        let settledStatus: SessionRun["status"] = "finished";
        let hadAssistantText = false;
        let hadToolFailure = false;
        let hitMaxRounds = false;
        const tokenStats = createGeminiTokenStats(model);
        const toolLog: string[] = [];
        const failedToolNames = new Set<string>();
        let contractState = createGoogleContractState();
        let operationApprovalWait: GoogleSettleFailureContext | null = null;

        const ingestRuntimeAlert = (payload: {
          code: string;
          severity: "P0" | "P1" | "P2" | "P3";
          category:
            | "sdk_network"
            | "role_boundary"
            | "shell_tool"
            | "lifecycle"
            | "report_gate"
            | "wake_dispatch"
            | "persistence"
            | "panel_perf"
            | "concurrency_lock";
          title: string;
          message: string;
        }) => {
          void import("../alerts/RuntimeAlertManager.ts").then(
            ({ runtimeAlertManager }) => {
              runtimeAlertManager.ingest({
                ...payload,
                affected_agent: spec.agentId,
              });
            },
          );
        };

        const emitToolCompleted = (
          name: string,
          args: unknown,
          outcome: "success" | "failed" | "waiting_approval",
          detail: string,
          callId: string,
          extra?: Record<string, unknown>,
        ) => {
          const payload = {
            raw: {
              name,
              args,
              status:
                outcome === "success"
                  ? "completed"
                  : outcome === "waiting_approval"
                    ? "waiting_approval"
                    : "failed",
              call_id: callId,
              id: callId,
            },
          };
          handle.emit("sdk.tool_call", payload);
          handle.emit("sdk.tool_result", {
            raw: {
              name,
              status: outcome,
              result: detail.slice(0, 2000),
              call_id: callId,
              id: callId,
              ...extra,
            },
          });
          const taskIds = extractTaskIdPrefixesFromText(spec.text ?? "");
          maybeRecordActionEvidenceFromToolCall({
            projectRoot: projectRootForWorkspace,
            agent_id: spec.agentId ?? sdkAgentId ?? "",
            session_id: handle.session_id,
            payload,
            task_id: pinnedTaskId ?? taskIds[0],
          });
        };

        const restartMcpIfNeeded = async (): Promise<void> => {
          if (useFcopOneShot) return;
          if (!mcpConfig?.command) return;
          if (mcpClient?.isAlive()) return;
          mcpClient = new McpClient(
            mcpConfig.command,
            mcpConfig.args || [],
            mcpConfig.cwd,
            mcpConfig.env,
          );
          await mcpClient.start();
        };

        while (rounds < maxRounds && handle.isActive()) {
          if (!geminiToolsEnabled) {
            chatHistory = stripFunctionCallTurns(chatHistory);
          }
          chatHistory = sanitizeGeminiContents(chatHistory);

          let streamResult: Awaited<ReturnType<typeof consumeGeminiContentStream>>;
          let retryCount = 0;
          const maxRetries = 3;
          let assistantTextForRound = "";
          let geminiModelNotFoundThisRound = false;

          while (true) {
            try {
              const tokenEstimate = estimateGeminiRequestTokens({
                systemInstruction,
                contents: chatHistory,
                toolCount: googleTools.length,
              });
              tokenStats.estimated_input_tokens += tokenEstimate.total_input_tokens;
              tokenStats.estimated_system_tokens += tokenEstimate.system_tokens;
              tokenStats.estimated_history_tokens += tokenEstimate.history_tokens;
              tokenStats.estimated_tool_schema_tokens += tokenEstimate.tool_schema_tokens;
              tokenStats.request_count += 1;

              assistantTextForRound = "";
              streamResult = await enqueueGoogleGenerate(async () => {
                const stream = await client.models.generateContentStream({
                  model,
                  contents: chatHistory,
                  config: generateConfig,
                });
                return consumeGeminiContentStream(stream, {
                  onThinkingDelta: (delta) => {
                    if (delta) handle.emit("sdk.thinking", { raw: { text: delta } });
                  },
                  onAssistantDelta: (delta) => {
                    if (delta) {
                      assistantTextForRound += delta;
                    }
                  },
                });
              });
              addGeminiUsageMetadata(tokenStats, streamResult.usageMetadata);
              break;
            } catch (err: any) {
              if (isGeminiModelNotFoundError(err)) {
                const errMsg = formatGeminiModelNotFoundMessage(err, model);
                geminiModelNotFoundThisRound = true;
                settledStatus = "failed";
                hadToolFailure = true;
                failedToolNames.add("gemini_model");
                handle.emit("sdk.assistant", {
                  raw: { text: `⚠️ ${errMsg}` },
                });
                handle.emit("sdk.status", {
                  raw: {
                    status: "failed",
                    error: errMsg,
                    code: "MODEL_NOT_FOUND",
                  },
                });
                ingestRuntimeAlert({
                  code: "MODEL_NOT_FOUND",
                  severity: "P0",
                  category: "sdk_network",
                  title: "Gemini 模型不可用",
                  message: errMsg,
                });
                console.error(
                  `[GoogleGenAiAdapter] MODEL_NOT_FOUND — no retry: ${errMsg}`,
                );
                break;
              }

              const errStr = String(err);
              const is503 = errStr.includes("503") || errStr.toLowerCase().includes("unavailable");
              const is429 = errStr.includes("429") || errStr.toLowerCase().includes("exhausted") || errStr.toLowerCase().includes("rate limit");
              
              if ((is503 || is429) && retryCount < maxRetries) {
                retryCount++;
                const sleepMs = computeGeminiBackoffMs(retryCount);
                console.warn(`[GoogleGenAiAdapter] Gemini API transient error (503/429) detected. Retrying in ${sleepMs.toFixed(0)}ms with exponential backoff+jitter... (Attempt ${retryCount}/${maxRetries})`);
                await new Promise((resolve) => setTimeout(resolve, sleepMs));
              } else {
                throw err;
              }
            }
          }

          if (geminiModelNotFoundThisRound) {
            recordGoogleContractModelNotFound(contractState);
            break;
          }

          const replyText = streamResult!.replyText;
          const partList = streamResult!.modelParts;
          const functionCalls = streamResult!.functionCalls as Array<{
            name?: string;
            args?: unknown;
            id?: string;
          }>;

          if (!functionCalls || functionCalls.length === 0) {
            if (assistantTextForRound.trim()) {
              hadAssistantText = true;
              handle.emit("sdk.assistant", {
                raw: { text: assistantTextForRound },
              });
            } else if (
              rounds > 0 &&
              (toolResultCache.size > 0 || toolLog.length > 0)
            ) {
              const summary = buildPatrolSummaryFromCache(toolResultCache, {
                uiLang: uiLang === "en" ? "en" : "zh",
                toolLog,
                variant: "no_model_text",
                adminRejectHotPathNextSteps:
                  adminRejectHotPath &&
                  !hasWriteReportDoneInCache(toolResultCache),
              });
              handle.emit("sdk.assistant", { raw: { text: summary } });
              hadAssistantText = true;
              console.warn(
                `[GoogleGenAiAdapter] model returned no text after ${rounds} tool round(s); emitting cached summary`,
              );
            }
            const finalParts = (partList ?? []).filter((p) => {
              if ((p as { functionCall?: unknown }).functionCall) return false;
              return isMeaningfulGeminiPart(p);
            });
            if (finalParts.length === 0 && replyText?.trim()) {
              finalParts.push({ text: replyText });
            }
            if (finalParts.length > 0) {
              chatHistory.push({ role: "model", parts: finalParts });
              chatHistory = sanitizeGeminiContents(chatHistory);
            }
            if (geminiToolsEnabled) {
              if (rounds > 0) {
                recordGoogleContractZeroToolcall(contractState);
              } else if (!assistantTextForRound.trim()) {
                if (pinnedTaskId) {
                  recordGoogleContractZeroToolcall(contractState);
                } else if (
                  toolResultCache.size === 0 &&
                  toolLog.length === 0
                ) {
                  recordGoogleContractInvalidOutput(contractState);
                }
              }
            }
            break;
          }

          rounds++;
          const orderedCalls = resolveOrderedFunctionCalls(
            partList ?? [],
            functionCalls ?? [],
          );
          if (
            functionCallOrderMismatch(partList ?? [], functionCalls ?? [])
          ) {
            console.warn(
              `[GoogleGenAiAdapter] functionCall order corrected from model parts (stream=${functionCalls.length} parts=${orderedCalls.length}) — prevents Gemini FR misalignment`,
            );
          }

          const seenToolKeysThisRound = new Set<string>();
          let executedThisRound = 0;
          let reusedThisRound = 0;
          let writeReportReuseThisRound = 0;

          const writeReportReserveByCallId = new Map<string, string>();

          // 5. 函数调用：dedupe / write_report guard → 只读并行批次 + 写工具串行
          const toolSlots: PreparedToolSlot[] = [];
          for (const call of orderedCalls) {
            const toolName = String(call.name ?? "unknown_tool");
            const dedupeDecision = resolveToolDedupe({
              toolName,
              args: call.args ?? {},
              seenThisRound: seenToolKeysThisRound,
              seenGlobal: seenToolKeysGlobal,
              cache: toolResultCache,
            });
            if (dedupeDecision.action === "reuse") {
              reusedThisRound++;
              if (toolName === "write_report") {
                writeReportReuseThisRound++;
              }
              console.warn(
                `[GoogleGenAiAdapter] skip duplicate tool call: ${call.name}`,
              );
              const reusedRawOutput = dedupeDecision.output ?? "";
              if (toolName === "write_report" || toolName === "submit_task") {
                const reusedClassified = classifyGoogleContractToolOutcome(
                  toolName,
                  reusedRawOutput,
                );
                recordGoogleContractToolResult(contractState, {
                  toolName,
                  output: reusedRawOutput,
                  ok: reusedClassified.outcome === "success",
                });
                if (reusedClassified.outcome === "success") {
                  failedToolNames.delete(toolName);
                  if (failedToolNames.size === 0) {
                    hadToolFailure = false;
                    if (!hitMaxRounds) {
                      settledStatus = "finished";
                    }
                  }
                }
              }
              toolSlots.push({
                kind: "immediate",
                result: {
                  name: toolName,
                  id: call.id,
                  response: {
                    output: formatReusedToolOutput(
                      toolName,
                      dedupeDecision.output,
                    ),
                  },
                },
              });
              continue;
            }

            if (toolName === "write_report") {
              const writeReportReserveKey = toolDedupeKey(
                toolName,
                call.args ?? {},
              );
              if (
                !tryReserveWriteReportExecute(
                  writeReportReservedKeys,
                  writeReportReserveKey,
                )
              ) {
                reusedThisRound++;
                writeReportReuseThisRound++;
                console.warn(
                  `[GoogleGenAiAdapter] write_report physical guard: ${writeReportReserveKey}`,
                );
                toolSlots.push({
                  kind: "immediate",
                  result: {
                    name: toolName,
                    id: call.id,
                    response: {
                      output: WRITE_REPORT_DEDUPE_REUSE_MESSAGE,
                    },
                  },
                });
                continue;
              }
              const slotCallId =
                call.id ||
                `gemini-${toolName}-${rounds}-${toolLog.length}`;
              writeReportReserveByCallId.set(
                slotCallId,
                writeReportReserveKey,
              );
            }

            toolSlots.push({
              kind: "execute",
              call: call as GeminiFunctionCallLike,
              readonlyParallel: readonlyParallelForCall(
                call as GeminiFunctionCallLike,
              ),
            });
          }

          const executeGeminiToolCall = async (
            call: GeminiFunctionCallLike,
          ): Promise<GeminiToolResult> => {
            const toolName = String(call.name ?? "unknown_tool");
            executedThisRound++;

            const callId =
              call.id ||
              `gemini-${toolName}-${rounds}-${toolLog.length}`;
            const writeReportReserveKey =
              writeReportReserveByCallId.get(callId) ?? null;

            handle.emit("sdk.tool_call", {
              raw: {
                name: toolName,
                args: call.args,
                status: "running",
                call_id: callId,
                id: callId,
              },
            });

            let effectiveArgs =
              ((call.args as Record<string, unknown>) || {}) as Record<
                string,
                unknown
              >;
            if (toolName === "write_task" || toolName === "create_task") {
              effectiveArgs = pinPmWorkerTaskLineage(effectiveArgs, pinnedTaskId);
              call.args = effectiveArgs;
            }
            if (
              (toolName === "pm.wake_downstream" || toolName === "pm.close_admin_task") &&
              pinnedTaskId
            ) {
              effectiveArgs = {
                ...effectiveArgs,
                current_task_id: pinnedTaskId,
              };
              call.args = effectiveArgs;
            }
            let writeReportRepairExtra: Record<string, unknown> | undefined;

            if (
              toolName === "submit_task" &&
              contractState.hadSubmitTaskSuccess
            ) {
              const alreadyOutput = JSON.stringify({
                ok: true,
                status: "already_submitted",
                message:
                  "submit_task skipped: task already submitted this session",
              });
              toolLog.push(`${toolName}: 成功（已提交，跳过重复 submit）`);
              recordGoogleContractToolResult(contractState, {
                toolName,
                output: alreadyOutput,
                ok: true,
              });
              failedToolNames.delete(toolName);
              if (failedToolNames.size === 0) {
                hadToolFailure = false;
                if (!hitMaxRounds) {
                  settledStatus = "finished";
                }
              }
              emitToolCompleted(
                toolName,
                effectiveArgs,
                "success",
                alreadyOutput,
                callId,
              );
              recordToolDedupeResult({
                toolName,
                args: effectiveArgs,
                output: alreadyOutput,
                seenThisRound: seenToolKeysThisRound,
                seenGlobal: seenToolKeysGlobal,
                cache: toolResultCache,
              });
              return {
                name: toolName,
                id: call.id,
                response: { output: alreadyOutput },
              };
            }

            if (toolName === "write_report") {
              console.log(
                `[GoogleGenAiAdapter] write_report task_id guard: pinnedTaskId=${pinnedTaskId ?? "(none)"} provided=${String(effectiveArgs.task_id ?? effectiveArgs.taskId ?? "") || "(missing)"}`,
              );
              const taskIdGuard = guardWriteReportTaskId(
                effectiveArgs,
                pinnedTaskId,
              );
              console.log(
                `[GoogleGenAiAdapter] write_report task_id guard result: action=${taskIdGuard.action}${taskIdGuard.action === "repair" ? ` autofill→${taskIdGuard.pinnedTaskId}` : ""}`,
              );
              if (taskIdGuard.action === "reject") {
                if (writeReportReserveKey) {
                  releaseWriteReportReserve(
                    writeReportReservedKeys,
                    writeReportReserveKey,
                  );
                  writeReportReserveByCallId.delete(callId);
                }
                const mismatchPayload =
                  formatWriteReportTaskIdMismatchPayload(taskIdGuard);
                const mismatchText = JSON.stringify(mismatchPayload);
                hadToolFailure = true;
                failedToolNames.add(toolName);
                settledStatus = "failed";
                toolLog.push(
                  `${toolName}: TASK_ID_MISMATCH — ${taskIdGuard.message}`,
                );
                emitToolCompleted(
                  toolName,
                  effectiveArgs,
                  "failed",
                  mismatchText,
                  callId,
                  {
                    code: "TASK_ID_MISMATCH",
                    pinned_task_id: taskIdGuard.pinnedTaskId,
                    provided_task_id: taskIdGuard.providedTaskId,
                  },
                );
                handle.emit("sdk.status", {
                  raw: {
                    status: "failed",
                    error: taskIdGuard.message,
                    code: "TASK_ID_MISMATCH",
                    tool: toolName,
                  },
                });
                recordToolDedupeResult({
                  toolName,
                  args: effectiveArgs,
                  output: mismatchText,
                  seenThisRound: seenToolKeysThisRound,
                  seenGlobal: seenToolKeysGlobal,
                  cache: toolResultCache,
                });
                return {
                  name: toolName,
                  id: call.id,
                  response: { ok: false, error: mismatchPayload },
                };
              }
              if (taskIdGuard.action === "repair") {
                effectiveArgs = taskIdGuard.args;
                call.args = effectiveArgs;
                writeReportRepairExtra = {
                  tool_args_repaired: WRITE_REPORT_TASK_ID_AUTOFILL_TAG,
                  pinned_task_id: taskIdGuard.pinnedTaskId,
                };
                console.log(
                  `[GoogleGenAiAdapter] write_report task_id autofill from session: ${taskIdGuard.pinnedTaskId}`,
                );
              } else {
                effectiveArgs = taskIdGuard.args;
                call.args = effectiveArgs;
              }
            }

            if (
              pmDevColdDispatch &&
              PM_DEV_COLD_DISPATCH_BLOCKED.has(toolName)
            ) {
              const denialMessage = `PM dev cold dispatch 禁止调用 ${toolName}（只读分析 + patch 写入 DEV 子任务）`;
              const denialPayload = {
                ok: false,
                code: "PM_DEV_COLD_DISPATCH_BLOCKED",
                error: denialMessage,
                message: denialMessage,
                tool: toolName,
              };
              const denialText = JSON.stringify(denialPayload);
              hadToolFailure = true;
              failedToolNames.add(toolName);
              settledStatus = "failed";
              toolLog.push(
                `${toolName}: PM_DEV_COLD_DISPATCH_BLOCKED — ${denialMessage}`,
              );
              emitToolCompleted(
                toolName,
                effectiveArgs,
                "failed",
                denialText,
                callId,
                { code: "PM_DEV_COLD_DISPATCH_BLOCKED", tool: toolName },
              );
              handle.emit("sdk.status", {
                raw: {
                  status: "failed",
                  error: denialMessage,
                  code: "PM_DEV_COLD_DISPATCH_BLOCKED",
                  tool: toolName,
                },
              });
              recordToolDedupeResult({
                toolName,
                args: effectiveArgs,
                output: denialText,
                seenThisRound: seenToolKeysThisRound,
                seenGlobal: seenToolKeysGlobal,
                cache: toolResultCache,
              });
              return {
                name: toolName,
                id: call.id,
                response: { ok: false, error: denialPayload },
              };
            }

            if (
              pmSelfReportOnly &&
              (PM_SELF_REPORT_ONLY_BLOCKED.has(toolName) ||
                toolName === "list_dir" ||
                toolName === "write_file")
            ) {
              const denialMessage = `run_mode=pm_self_report_only 禁止调用 ${toolName}`;
              const denialPayload = {
                ok: false,
                code: "PM_SELF_REPORT_ONLY_BLOCKED",
                error: denialMessage,
                message: denialMessage,
                tool: toolName,
              };
              const denialText = JSON.stringify(denialPayload);
              hadToolFailure = true;
              failedToolNames.add(toolName);
              settledStatus = "failed";
              toolLog.push(`${toolName}: PM_SELF_REPORT_ONLY_BLOCKED — ${denialMessage}`);
              emitToolCompleted(
                toolName,
                effectiveArgs,
                "failed",
                denialText,
                callId,
                { code: "PM_SELF_REPORT_ONLY_BLOCKED", tool: toolName },
              );
              handle.emit("sdk.status", {
                raw: {
                  status: "failed",
                  error: denialMessage,
                  code: "PM_SELF_REPORT_ONLY_BLOCKED",
                  tool: toolName,
                },
              });
              recordToolDedupeResult({
                toolName,
                args: effectiveArgs,
                output: denialText,
                seenThisRound: seenToolKeysThisRound,
                seenGlobal: seenToolKeysGlobal,
                cache: toolResultCache,
              });
              return {
                name: toolName,
                id: call.id,
                response: { ok: false, error: denialPayload },
              };
            }

            const projectRoot = mcpConfig?.cwd || process.cwd();

            if (toolName === "write_task" || toolName === "create_task") {
              const productGate = await guardPmProductWorkerWriteTask({
                projectRoot,
                agentId: spec.agentId ?? sdkAgentId ?? "",
                args: effectiveArgs,
              });
              if (!productGate.allowed) {
                const denialPayload = {
                  ok: false,
                  code: productGate.code,
                  reason: productGate.reason,
                  required_action: productGate.required_action,
                  findings: productGate.findings,
                  tool: toolName,
                };
                const denialText = JSON.stringify(denialPayload);
                hadToolFailure = true;
                failedToolNames.add(toolName);
                settledStatus = "failed";
                emitToolCompleted(toolName, effectiveArgs, "failed", denialText, callId, {
                  code: productGate.code,
                  tool: toolName,
                });
                handle.emit("sdk.status", {
                  raw: {
                    status: "failed",
                    error: productGate.reason,
                    code: productGate.code,
                    findings: productGate.findings,
                    tool: toolName,
                  },
                });
                recordToolDedupeResult({
                  toolName,
                  args: effectiveArgs,
                  output: denialText,
                  seenThisRound: seenToolKeysThisRound,
                  seenGlobal: seenToolKeysGlobal,
                  cache: toolResultCache,
                });
                return {
                  name: toolName,
                  id: call.id,
                  response: { ok: false, error: denialPayload },
                };
              }
            }

            if (toolName === "write_report") {
              const devDispatchGuard = await guardPmDevDispatchWriteReport(
                projectRoot,
                effectiveArgs,
                {
                  agentId: spec.agentId ?? sdkAgentId ?? "",
                  promptText: promptRoutingText,
                  runMode: spec.runMode,
                },
              );
              if (!devDispatchGuard.allowed) {
                if (writeReportReserveKey) {
                  releaseWriteReportReserve(
                    writeReportReservedKeys,
                    writeReportReserveKey,
                  );
                  writeReportReserveByCallId.delete(callId);
                }
                const denialMessage = devDispatchGuard.message;
                const denialPayload = {
                  ok: false,
                  code: devDispatchGuard.code,
                  error: denialMessage,
                  message: denialMessage,
                  tool: toolName,
                  skipped_reason: devDispatchGuard.skipped_reason,
                  findings: devDispatchGuard.findings,
                };
                const denialText = JSON.stringify(denialPayload);
                hadToolFailure = true;
                failedToolNames.add(toolName);
                settledStatus = "failed";
                toolLog.push(
                  `${toolName}: ${devDispatchGuard.code} — ${denialMessage}`,
                );
                emitToolCompleted(
                  toolName,
                  effectiveArgs,
                  "failed",
                  denialText,
                  callId,
                  {
                    code: devDispatchGuard.code,
                    tool: toolName,
                    skipped_reason: devDispatchGuard.skipped_reason,
                  },
                );
                handle.emit("sdk.status", {
                  raw: {
                    status: "failed",
                    error: denialMessage,
                    code: devDispatchGuard.code,
                    tool: toolName,
                  },
                });
                recordToolDedupeResult({
                  toolName,
                  args: effectiveArgs,
                  output: denialText,
                  seenThisRound: seenToolKeysThisRound,
                  seenGlobal: seenToolKeysGlobal,
                  cache: toolResultCache,
                });
                return {
                  name: toolName,
                  id: call.id,
                  response: { ok: false, error: denialPayload },
                };
              }
            }

            let routed;
            try {
              routed = await routeGoogleToolCall({
                projectRoot,
                agentId: spec.agentId ?? sdkAgentId ?? "",
                toolName,
                args: effectiveArgs,
                callId,
                useFcopOneShot: Boolean(useFcopOneShot && mcpConfig?.command),
                pythonBin:
                  useFcopOneShot && mcpConfig
                    ? resolveOneShotPythonBin(mcpConfig)
                    : undefined,
                sessionId: handle.session_id,
                taskId: pinnedTaskId ?? undefined,
                threadKey: spec.pinnedThreadKey,
                promptRoutingText,
                turnIndex: rounds,
                visionClient: client,
                visionModel: model,
                mcpClient:
                  useFcopOneShot || !mcpClient
                    ? undefined
                    : {
                        callTool: async (name, args) => {
                          await restartMcpIfNeeded();
                          if (!mcpClient) {
                            throw new Error(
                              `McpClient has not been initialized for tool: ${name}`,
                            );
                          }
                          const result = await mcpClient.call("tools/call", {
                            name,
                            arguments: args,
                          });
                          return result && result.content
                            ? result.content.map((c: any) => c.text).join("\n")
                            : JSON.stringify(result);
                        },
                      },
              });
            } catch (err: any) {
              const errMsg = err?.message || String(err);
              if (writeReportReserveKey) {
                releaseWriteReportReserve(
                  writeReportReservedKeys,
                  writeReportReserveKey,
                );
                writeReportReserveByCallId.delete(callId);
              }
              hadToolFailure = true;
              failedToolNames.add(toolName);
              toolLog.push(`${toolName}: 失败 — ${errMsg}`);
              emitToolCompleted(
                toolName,
                effectiveArgs,
                "failed",
                errMsg,
                callId,
                writeReportRepairExtra,
              );
              handle.emit("sdk.status", {
                raw: {
                  status: "failed",
                  error: `Tool call failed for [${toolName}]: ${errMsg}`,
                },
              });
              recordToolDedupeResult({
                toolName,
                args: call.args ?? {},
                output: `Error: ${errMsg}`,
                seenThisRound: seenToolKeysThisRound,
                seenGlobal: seenToolKeysGlobal,
                cache: toolResultCache,
              });
              recordGoogleContractToolResult(contractState, {
                toolName,
                output: `Error: ${errMsg}`,
                ok: false,
              });
              settledStatus = "failed";
              return {
                name: toolName,
                id: call.id,
                response: { error: errMsg },
              };
            }

            if (!routed.ok) {
              const denied = parseRouterAuthorityDenied(routed.error);
              const isAuthorityDenied =
                routed.code === "AUTHORITY_DENIED" ||
                denied?.code === "AUTHORITY_DENIED";
              const denialMessage =
                denied?.message ||
                routed.error ||
                routed.code ||
                "tool_failed";
              const denialText =
                routed.error ||
                JSON.stringify({
                  ok: false,
                  code: routed.code ?? "TOOL_FAILED",
                  message: denialMessage,
                });
              const isApprovalWait =
                routed.code === "OPERATION_APPROVAL_REQUIRED" ||
                routed.code === "APPROVAL_CREATION_PENDING";
              const isCallOnlyRejection =
                routed.code === "ROLE_CAPABILITY_DENIED" ||
                routed.code === "TOOL_REQUEST_INVALID";

              if (writeReportReserveKey) {
                releaseWriteReportReserve(
                  writeReportReservedKeys,
                  writeReportReserveKey,
                );
                writeReportReserveByCallId.delete(callId);
              }
              if (isApprovalWait) {
                operationApprovalWait = {
                  failure_code: routed.code,
                  operation_fingerprint: routed.operation_fingerprint,
                  operation_classification:
                    routed.code === "APPROVAL_CREATION_PENDING"
                      ? "approval_creation_pending"
                      : "approval_required",
                  retry_policy: "none",
                  next_safe_action:
                    routed.code === "APPROVAL_CREATION_PENDING"
                      ? "wait_for_idempotent_approval_record_creation"
                      : "wait_for_approval_decision_event",
                  report_required: false,
                  operation_outcome: routed.operation_outcome,
                };
                hadToolFailure = false;
                settledStatus = "finished";
                hadAssistantText = true;
              } else if (!isCallOnlyRejection) {
                hadToolFailure = true;
                failedToolNames.add(toolName);
                settledStatus = "failed";
              }
              toolLog.push(
                `${toolName}: ${isAuthorityDenied ? "AUTHORITY_DENIED" : routed.code ?? "TOOL_FAILED"} — ${denialMessage}`,
              );
              emitToolCompleted(
                toolName,
                effectiveArgs,
                isApprovalWait ? "waiting_approval" : "failed",
                denialText,
                callId,
                isAuthorityDenied
                  ? { code: "AUTHORITY_DENIED", tool: toolName }
                  : { code: routed.code, tool: toolName },
              );
              handle.emit("sdk.status", isApprovalWait
                ? {
                    status: routed.code === "APPROVAL_CREATION_PENDING"
                      ? "approval_creation_pending"
                      : "waiting_approval",
                    failure_code: routed.code,
                    operation_fingerprint: routed.operation_fingerprint,
                    operation_outcome: routed.operation_outcome,
                    operation_executed: false,
                    agent_notice_delivered:
                      routed.code === "OPERATION_APPROVAL_REQUIRED",
                    next_safe_action: operationApprovalWait?.next_safe_action,
                    notice: denialMessage,
                  }
                : isCallOnlyRejection
                  ? {
                      status: "tool_rejected",
                      code: routed.code,
                      operation_executed: false,
                      tool: toolName,
                      reason: denialMessage,
                    }
                  : {
                      raw: {
                        status: "failed",
                        error: denialMessage,
                        code: isAuthorityDenied
                          ? "AUTHORITY_DENIED"
                          : routed.code ?? "TOOL_FAILED",
                        tool: toolName,
                      },
                    });
              if (isAuthorityDenied) {
                ingestRuntimeAlert({
                  code: "AUTHORITY_DENIED",
                  severity: "P1",
                  category: "role_boundary",
                  title: "Google 工具权限拒绝",
                  message: denialMessage,
                });
              }
              recordToolDedupeResult({
                toolName,
                args: effectiveArgs,
                output: denialText,
                seenThisRound: seenToolKeysThisRound,
                seenGlobal: seenToolKeysGlobal,
                cache: toolResultCache,
              });
              if (!isApprovalWait && !isCallOnlyRejection) {
                recordGoogleContractToolResult(contractState, {
                  toolName,
                  output: denialText,
                  ok: false,
                });
              }
              return {
                name: toolName,
                id: call.id,
                response: isAuthorityDenied
                  ? {
                      ok: false,
                      error: denied ?? {
                        code: "AUTHORITY_DENIED",
                        message: denialMessage,
                      },
                    }
                  : isApprovalWait
                    ? {
                        ok: false,
                        status: "waiting_approval",
                        code: routed.code,
                        notice: denialMessage,
                      }
                    : { error: denialText },
              };
            }

            const toolOutput = routed.output ?? "";
            const classified = classifyGoogleContractToolOutcome(
              toolName,
              toolOutput,
            );
            recordGoogleContractToolResult(contractState, {
              toolName,
              output: toolOutput,
              ok: classified.outcome === "success",
            });
            const brief =
              toolOutput.length > 280
                ? `${toolOutput.slice(0, 280)}…`
                : toolOutput;
            toolLog.push(`${toolName}: ${classified.label} — ${brief}`);

            if (classified.outcome === "failed") {
              hadToolFailure = true;
              failedToolNames.add(toolName);
              settledStatus = "failed";
              const errSummary = `${toolName}: ${brief}`;
              emitToolCompleted(
                toolName,
                effectiveArgs,
                "failed",
                errSummary,
                callId,
                writeReportRepairExtra,
              );
              handle.emit("sdk.status", {
                raw: {
                  status: "failed",
                  error: errSummary,
                  tool: toolName,
                },
              });
              recordToolDedupeResult({
                toolName,
                args: effectiveArgs,
                output: toolOutput,
                seenThisRound: seenToolKeysThisRound,
                seenGlobal: seenToolKeysGlobal,
                cache: toolResultCache,
              });
              return {
                name: toolName,
                id: call.id,
                response: { error: toolOutput },
              };
            }

            failedToolNames.delete(toolName);
            if (failedToolNames.size === 0) {
              hadToolFailure = false;
              if (!hitMaxRounds) {
                settledStatus = "finished";
              }
            }
            emitToolCompleted(
              toolName,
              effectiveArgs,
              "success",
              toolOutput,
              callId,
              writeReportRepairExtra,
            );
            handle.emit("sdk.result", {
              raw: {
                name: toolName,
                status: "success",
                result: toolOutput,
                call_id: callId,
                id: callId,
                tool_use_id: callId,
              },
            });
            recordToolDedupeResult({
              toolName,
              args: effectiveArgs,
              output: toolOutput,
              seenThisRound: seenToolKeysThisRound,
              seenGlobal: seenToolKeysGlobal,
              cache: toolResultCache,
            });
            return {
              name: toolName,
              id: call.id,
              response: { output: toolOutput },
            };
          };

          const toolResults = await runPreparedToolSlots(
            toolSlots,
            executeGeminiToolCall,
            {
              onParallelBatch: (names, size) => {
                if (size > 1) {
                  console.log(
                    `[GoogleGenAiAdapter] parallel readonly batch (${size}): ${names.join(", ")}`,
                  );
                }
              },
            },
          );

          // Approval is a stop signal for this inference turn only.  The
          // logical Session and TASK remain live and no settlement/report
          // adapter may reinterpret the wait as a failed run.
          if (operationApprovalWait) {
            break;
          }

          chatHistory.push({
            role: "model",
            parts: partList && partList.length > 0 ? partList : [{ text: replyText || "" }]
          });

          const modelPartsForFr =
            partList && partList.length > 0 ? partList : [];
          let frTurn = buildFunctionResponseUserTurn(modelPartsForFr, toolResults);
          let frAlign = validateFunctionResponseAlignment(modelPartsForFr, frTurn);
          let frAlignmentBroken = false;

          if (!frAlign.ok) {
            recordGoogleContractFrMismatch(contractState);
            const frStrike = contractState.functionResponseMismatchStrikes;
            console.warn(
              `[GoogleGenAiAdapter] functionResponse alignment mismatch (strike ${frStrike}): calls=${frAlign.callCount} responses=${frAlign.responseCount} round=${rounds}`,
            );

            if (frStrike === 1) {
              const repaired = repairFunctionResponseUserTurn(
                modelPartsForFr,
                toolResults,
              );
              const repairedAlign = validateFunctionResponseAlignment(
                modelPartsForFr,
                repaired,
              );
              if (repairedAlign.ok) {
                frTurn = repaired;
                frAlign = repairedAlign;
                console.warn(
                  `[GoogleGenAiAdapter] functionResponse alignment repaired after mismatch round=${rounds}`,
                );
              } else {
                recordGoogleContractFrMismatch(contractState);
              }
            }

            if (contractState.functionResponseMismatchStrikes >= 2) {
              frAlignmentBroken = true;
              hadToolFailure = true;
              settledStatus = "failed";
              failedToolNames.add("functionResponse_alignment");
              const errMsg = `FunctionResponse alignment 熔断：calls=${frAlign.callCount} responses=${frAlign.responseCount}`;
              handle.emit("sdk.assistant", {
                raw: { text: `⚠️ ${errMsg}` },
              });
              handle.emit("sdk.status", {
                raw: {
                  status: "failed",
                  error: errMsg,
                  code: "FUNCTION_RESPONSE_MISALIGNED",
                },
              });
              ingestRuntimeAlert({
                code: "FUNCTION_RESPONSE_MISALIGNED",
                severity: "P0",
                category: "shell_tool",
                title: "Gemini FunctionResponse 错位",
                message: errMsg,
              });
              console.error(
                `[GoogleGenAiAdapter] FUNCTION_RESPONSE_MISALIGNED — breaking tool loop: ${errMsg}`,
              );
            }
          }

          if (!frAlignmentBroken) {
            chatHistory.push(frTurn);
            chatHistory = sanitizeGeminiContents(chatHistory);
          } else {
            break;
          }

          if (
            shouldBreakPatrolToolLoop({
              functionCallCount: functionCalls.length,
              executedCount: executedThisRound,
              reusedCount: reusedThisRound,
              roundIndex: rounds,
              cache: toolResultCache,
              adminRejectHotPath,
            })
          ) {
            const patrolSummary = buildPatrolSummaryFromCache(toolResultCache, {
              uiLang: uiLang === "en" ? "en" : "zh",
              toolLog,
              adminRejectHotPathNextSteps:
                adminRejectHotPath &&
                !hasWriteReportDoneInCache(toolResultCache),
            });
            handle.emit("sdk.assistant", { raw: { text: patrolSummary } });
            hadAssistantText = true;
            console.warn(
              `[GoogleGenAiAdapter] patrol tool loop break — executed=${executedThisRound} reused=${reusedThisRound} total=${functionCalls.length} round=${rounds}`,
            );
            break;
          }

          if (
            shouldBreakWriteReportAckLoop({
              functionCallCount: functionCalls.length,
              executedCount: executedThisRound,
              reusedCount: reusedThisRound,
              writeReportReuseCount: writeReportReuseThisRound,
              roundIndex: rounds,
              cache: toolResultCache,
            })
          ) {
            const ackSummary = buildWriteReportAckBreakSummary({
              uiLang: uiLang === "en" ? "en" : "zh",
              toolLog,
            });
            handle.emit("sdk.assistant", { raw: { text: ackSummary } });
            hadAssistantText = true;
            console.warn(
              `[GoogleGenAiAdapter] write_report ack loop break — executed=${executedThisRound} write_report_reuse=${writeReportReuseThisRound} total=${functionCalls.length} round=${rounds}`,
            );
            break;
          }

          if (shouldStopGoogleContractLoop({ state: contractState })) {
            console.warn(
              `[GoogleGenAiAdapter] contract early stop: ${contractState.stopReason ?? "blocked"}`,
            );
            if (contractState.stopReason === "write_report_submit_done") {
              failedToolNames.delete("submit_task");
              failedToolNames.delete("write_report");
              if (failedToolNames.size === 0) {
                hadToolFailure = false;
                settledStatus = "finished";
              }
            }
            break;
          }
        }

        if (rounds >= maxRounds) {
          hitMaxRounds = true;
          recordGoogleContractHitMaxRounds(contractState);
          if (failedToolNames.size > 0) {
            settledStatus = "failed";
          } else {
            settledStatus = "finished";
          }
          handle.emit("sdk.status", {
            raw: {
              status: failedToolNames.size > 0 ? "failed" : "warning",
              error: `已达工具轮次上限（${maxRounds} 轮）`,
            },
          });
        }

        const buildSessionEndHeadline = (): string | null => {
          const parts: string[] = [];
          if (failedToolNames.size > 0) {
            parts.push(
              `部分工具失败（${[...failedToolNames].join("、")}）`,
            );
          }
          if (hitMaxRounds) {
            parts.push(
              `已达到工具轮次上限（${rounds}/${maxRounds}）`,
            );
          }
          if (parts.length === 0) return null;
          const joined =
            parts.length === 2 ? parts.join("，且") : parts[0]!;
          return `⚠️ ${joined}。请查看「门铃 → 故障」与「错误日志」。`;
        };

        const sessionEndHeadline = buildSessionEndHeadline();
        if (sessionEndHeadline) {
          if (!hadAssistantText) {
            const lines = toolLog.length ? toolLog.slice(-12) : [];
            const summary =
              lines.length > 0
                ? `${sessionEndHeadline}\n\n已完成 FCoP 工具调用（${lines.length} 条记录），但模型未返回面向用户的正文：\n\n${lines.join("\n")}`
                : sessionEndHeadline;
            handle.emit("sdk.assistant", { raw: { text: summary } });
          } else {
            handle.emit("sdk.assistant", {
              raw: { text: `\n\n---\n${sessionEndHeadline}` },
            });
          }
        } else if (!hadAssistantText) {
          if (toolResultCache.size > 0 || toolLog.length > 0) {
            const summary = buildPatrolSummaryFromCache(toolResultCache, {
              uiLang: uiLang === "en" ? "en" : "zh",
              toolLog,
              variant: "no_model_text",
              adminRejectHotPathNextSteps:
                adminRejectHotPath &&
                !hasWriteReportDoneInCache(toolResultCache),
            });
            handle.emit("sdk.assistant", { raw: { text: summary } });
          } else {
            handle.emit("sdk.assistant", {
              raw: { text: "会话已结束，模型未返回文本。" },
            });
          }
        }

        if (!isTaskDispatch) {
          this._chatMemory.save(memoryKey, chatHistory);
        }

        handle.emit("sdk.result", {
          raw: buildGeminiUsageResult(tokenStats, spec.agentId),
        });

        const settlementProjectRoot = mcpConfig?.cwd || process.cwd();
        const settlementPythonBin =
          useFcopOneShot && mcpConfig
            ? resolveOneShotPythonBin(mcpConfig)
            : undefined;
        if (!operationApprovalWait) try {
          const settlement = await settleGoogleFcopRun({
            projectRoot: settlementProjectRoot,
            agentId: spec.agentId ?? sdkAgentId ?? "",
            pinnedTaskId: pinnedTaskId ?? undefined,
            toolLog,
            failedToolNames: [...failedToolNames],
            hitMaxRounds,
            hadToolFailure,
            contractState,
            model,
            pythonBin: settlementPythonBin,
          });
          if (
            contractState.stopReason === "write_report_submit_done" &&
            settlement.ok
          ) {
            settledStatus = "finished";
            hadToolFailure = false;
          } else if (settlement.status === "blocked") {
            settledStatus = "failed";
            hadToolFailure = true;
          } else if (settlement.status === "failed") {
            settledStatus = "failed";
            hadToolFailure = true;
          } else if (settlement.ok && settledStatus !== "failed") {
            settledStatus = "finished";
          }
        } catch (settleErr: any) {
          console.error(
            `[GoogleGenAiAdapter] settleGoogleFcopRun failed:`,
            settleErr,
          );
        }

        const failedTools = [...failedToolNames];
        const settleFailureCtx: GoogleSettleFailureContext | undefined =
          operationApprovalWait ?? (settledStatus === "failed"
            ? {
                failed_tool_names: failedTools.length ? failedTools : undefined,
                hit_max_rounds: hitMaxRounds || undefined,
                error_message:
                  failedTools.length > 0
                    ? `部分工具失败（${failedTools.join("、")}）`
                    : hitMaxRounds
                      ? `已达工具轮次上限（${maxRounds} 轮）`
                      : undefined,
                raw: {
                  status: "error",
                  ...(failedTools.length
                    ? { failed_tool_names: failedTools }
                    : {}),
                  ...(hitMaxRounds ? { hit_max_rounds: true } : {}),
                },
              }
            : undefined);
        handle.settle(settledStatus, rounds, settleFailureCtx);
        handle.emit("runtime.session_ended");
      } catch (err: any) {
        console.error(`[GoogleGenAiAdapter] send failed with raw error:`, err);
        const errMsg = err?.message || String(err);
        handle.emit("sdk.assistant", {
          raw: {
            text: `Gemini 调用失败：${errMsg}`,
          },
        });
        handle.emit("sdk.status", {
          status: "failed",
          error: errMsg,
        });
        handle.settle("failed", 0, {
          error_message: errMsg,
          raw: err,
        });
        handle.emit("runtime.session_ended");
      } finally {
        if (mcpClient) {
          await mcpClient.stop();
        }
      }
    });

    return handle;
  }
}

async function extractGoogleResponseText(response: any, partList: any[] | undefined): Promise<string> {
  const maybeText = response?.text;
  if (typeof maybeText === "string") {
    return maybeText;
  }
  if (typeof maybeText === "function") {
    const value = await maybeText.call(response);
    if (typeof value === "string") {
      return value;
    }
  }
  if (Array.isArray(partList)) {
    return partList
      .map((part) => (typeof part?.text === "string" ? part.text : ""))
      .join("");
  }
  return "";
}

/**
 * Google 已下线/弃用的模型 id → 当前可用替代（避免 404）。
 *
 * 下架时间线（2026-06）：
 *   gemini-1.0-pro / gemini-pro-vision  → 已下线
 *   gemini-1.5-pro / gemini-1.5-flash   → 已下线
 *   gemini-2.0-flash / 2.0-flash-lite   → 2026-06-01 下线
 *
 * 当前推荐（2026-06）：
 *   gemini-3.5-flash   → GA 2026-05-19，agentic/coding 主推
 *   gemini-3.1-pro     → Preview 2026-02，复杂推理
 *   gemini-2.5-pro/flash → 稳定兜底
 */
const DEPRECATED_GEMINI_MODEL_ALIASES: Readonly<Record<string, string>> = {
  // 1.5 系列（已下线）→ 3.5 Flash（当前主推 GA）
  "gemini-1.5-pro": "gemini-3.5-flash",
  "gemini-1.5-flash": "gemini-3.5-flash",
  "gemini-1.5-flash-8b": "gemini-3.5-flash",
  // 2.0 系列（2026-06-01 下线）→ 3.5 Flash
  "gemini-2.0-flash": "gemini-3.5-flash",
  "gemini-2.0-flash-lite": "gemini-3.5-flash",
  "gemini-2.0-flash-exp": "gemini-3.5-flash",
  // 2.5 系列（仍可用，但不再是最新）→ 保留不重定向，直接使用
  // 旧别名/错误写法修正
  "gemini-pro": "gemini-3.5-flash",
  "gemini-pro-vision": "gemini-3.5-flash",
};

function remapDeprecatedGeminiModel(modelId: string): string {
  const alias = DEPRECATED_GEMINI_MODEL_ALIASES[modelId];
  if (alias) {
    console.warn(
      `[GoogleGenAiAdapter] Gemini model ${modelId} is deprecated/unavailable; using ${alias}. ` +
        "Update codeflowmu.team.json or Panel team model settings.",
    );
    return alias;
  }
  return modelId;
}

/** Resolve Gemini model id; opaque Cursor router ids fall back to `defaultModel`. */
export function resolveGoogleModel(
  modelId: string | undefined,
  defaultModel?: string,
): string {
  const normalized = modelId?.trim();
  const isOpaque =
    !normalized || normalized === "auto" || normalized === "default";

  if (isOpaque) {
    const fallback =
      defaultModel?.trim() ||
      process.env["GOOGLE_DEFAULT_MODEL"]?.trim() ||
      process.env["GEMINI_DEFAULT_MODEL"]?.trim();
    if (fallback?.startsWith("gemini-")) {
      return remapDeprecatedGeminiModel(fallback);
    }
    throw new Error(
      `Google Gen AI requires an explicit Gemini model id; received ${JSON.stringify(modelId)}. ` +
        "Set GOOGLE_DEFAULT_MODEL or update the agent model in Team Config before sending chat.",
    );
  }
  if (!normalized.startsWith("gemini-")) {
    throw new Error(
      `Google Gen AI model must be a Gemini model id, got ${JSON.stringify(modelId)}.`,
    );
  }
  return remapDeprecatedGeminiModel(normalized);
}

function computeGeminiBackoffMs(attempt: number): number {
  const exponent = Math.max(0, attempt - 1);
  const deterministic = Math.min(
    GEMINI_BACKOFF_CAP_MS,
    GEMINI_BACKOFF_BASE_MS * Math.pow(2, exponent),
  );
  const jitter = Math.random() * deterministic;
  return Math.min(GEMINI_BACKOFF_CAP_MS, deterministic + jitter);
}

function parsePositiveInt(value: string | undefined, fallback: number): number {
  const parsed = Number.parseInt(value || "", 10);
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
}

function parseNonNegativeInt(value: string | undefined, fallback: number): number {
  const parsed = Number.parseInt(value || "", 10);
  return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
}

function mcpTimeoutMsFor(method: string): number {
  if (method === "initialize") {
    return parsePositiveInt(
      process.env.CODEFLOW_MCP_INITIALIZE_TIMEOUT_MS,
      MCP_INITIALIZE_TIMEOUT_MS,
    );
  }
  if (method === "tools/list") {
    return parsePositiveInt(
      process.env.CODEFLOW_MCP_TOOLS_LIST_TIMEOUT_MS,
      MCP_TOOLS_LIST_TIMEOUT_MS,
    );
  }
  if (method === "tools/call") {
    return parsePositiveInt(
      process.env.CODEFLOW_MCP_TOOL_TIMEOUT_MS,
      MCP_DEFAULT_CALL_TIMEOUT_MS,
    );
  }
  return 15_000;
}

function describeGeminiToolBudget(totalTools: number, selectedTools: number): string {
  const mode = resolveGeminiToolSelectionMode();
  const estimatedTokens = estimateGeminiToolTokens(selectedTools);
  const avoidedTokens = Math.max(
    0,
    estimateGeminiToolTokens(totalTools) - estimatedTokens,
  );
  return `${selectedTools}/${totalTools} tools (mode=${mode}); estimated tool schema tokens=${estimatedTokens}, avoided≈${avoidedTokens}`;
}

function estimateGeminiToolTokens(toolCount: number): number {
  return toolCount * GEMINI_TOOL_SCHEMA_TOKEN_ESTIMATE;
}

interface GeminiTokenStats {
  model: string;
  request_count: number;
  estimated_input_tokens: number;
  estimated_system_tokens: number;
  estimated_history_tokens: number;
  estimated_tool_schema_tokens: number;
  actual_input_tokens: number;
  actual_output_tokens: number;
  actual_total_tokens: number;
}

function createGeminiTokenStats(model: string): GeminiTokenStats {
  return {
    model,
    request_count: 0,
    estimated_input_tokens: 0,
    estimated_system_tokens: 0,
    estimated_history_tokens: 0,
    estimated_tool_schema_tokens: 0,
    actual_input_tokens: 0,
    actual_output_tokens: 0,
    actual_total_tokens: 0,
  };
}

function estimateGeminiRequestTokens(opts: {
  systemInstruction?: string;
  contents: any[];
  toolCount: number;
}): {
  system_tokens: number;
  history_tokens: number;
  tool_schema_tokens: number;
  total_input_tokens: number;
} {
  const systemTokens = estimateTextTokens(opts.systemInstruction || "");
  const historyTokens = estimateTextTokens(JSON.stringify(opts.contents ?? []));
  const toolSchemaTokens = estimateGeminiToolTokens(opts.toolCount);
  return {
    system_tokens: systemTokens,
    history_tokens: historyTokens,
    tool_schema_tokens: toolSchemaTokens,
    total_input_tokens: systemTokens + historyTokens + toolSchemaTokens,
  };
}

function estimateTextTokens(text: string): number {
  if (!text) return 0;
  let ascii = 0;
  let nonAscii = 0;
  for (const ch of text) {
    if (ch.charCodeAt(0) <= 0x7f) ascii += 1;
    else nonAscii += 1;
  }
  return Math.ceil(ascii / 4 + nonAscii * 1.2);
}

function addGeminiUsageMetadata(stats: GeminiTokenStats, usage: any): void {
  if (!usage || typeof usage !== "object") return;
  const prompt = toFiniteNumber(usage.promptTokenCount);
  const candidates = toFiniteNumber(usage.candidatesTokenCount);
  const total = toFiniteNumber(usage.totalTokenCount);
  stats.actual_input_tokens += prompt;
  stats.actual_output_tokens += candidates;
  stats.actual_total_tokens += total || prompt + candidates;
}

function buildGeminiUsageResult(
  stats: GeminiTokenStats,
  agentId: string,
): Record<string, unknown> {
  return {
    type: "gemini.usage",
    status: "finished",
    agent_id: agentId,
    model: stats.model,
    usage: {
      input_tokens: stats.actual_input_tokens,
      output_tokens: stats.actual_output_tokens,
      total_tokens: stats.actual_total_tokens,
      estimated_input_tokens: stats.estimated_input_tokens,
      estimated_system_tokens: stats.estimated_system_tokens,
      estimated_history_tokens: stats.estimated_history_tokens,
      estimated_tool_schema_tokens: stats.estimated_tool_schema_tokens,
      request_count: stats.request_count,
    },
    modelUsage: {
      [stats.model]: {
        inputTokens: stats.actual_input_tokens,
        outputTokens: stats.actual_output_tokens,
        estimatedInputTokens: stats.estimated_input_tokens,
        estimatedToolSchemaTokens: stats.estimated_tool_schema_tokens,
      },
    },
  };
}

function toFiniteNumber(value: unknown): number {
  const n = Number(value ?? 0);
  return Number.isFinite(n) ? n : 0;
}
