"""Jarvis agent graph orchestration."""

from __future__ import annotations

import asyncio
import json
import logging
import re
from uuid import uuid4
from typing import Any, Literal, cast

from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, SystemMessage, ToolMessage
from langgraph.graph import END, StateGraph

from app.agents.isolation import (
    WorktreeIsolationError,
    prepare_session_isolation,
    prepare_worktree_isolation,
    select_isolation_strategy,
)
from app.agents.prompts import (
    ANALYST_SYSTEM_PROMPT,
    COORDINATOR_SYSTEM_PROMPT,
    EXECUTOR_SYSTEM_PROMPT,
    JSON_ACTION_FALLBACK_PROMPT,
    LIBRARIAN_SYSTEM_PROMPT,
    MASTER_SYSTEM_PROMPT,
    SCHEDULE_PLANNER_SYSTEM_PROMPT,
)
from app.agents.orchestration.result_merge import merge_task_results
from app.agents.orchestration.scheduler import build_subtask_specs, ensure_child_links
from app.agents.orchestration.subagent_runtime import subtask_spec_to_agent_task
from app.agents.registry import load_builtin_registry_indexes
from app.agents.runtime_metrics import (
    coerce_cost_thresholds,
    estimate_token_cost,
    extract_token_usage,
    is_cost_budget_warning,
)
from app.agents.schemas.event import AgentEvent
from app.agents.schemas.message import AgentMessage
from app.agents.schemas.orchestration import (
    ExecutionDecision,
    MergeReport,
    RuntimeRequestContext,
    TaskGraph,
    VerificationReport,
    render_runtime_request_context_summary,
)
from app.agents.schemas.task import (
    AgentTask,
    CollaborationBudget,
    InterruptRecord,
    RecoveryRecord,
    TaskResult,
)
from app.agents.skill_registry import build_skill_context
from app.agents.skills.retriever import build_shortlisted_skill_context
from app.agents.state import AgentRole, AgentState
from app.agents.tools import SUB_COMMANDER_TOOLSETS
from app.agents.tools.time_reasoning import normalize_tool_time_arguments
from app.agents.verifier import (
    apply_verification_verdict,
    normalize_task_result,
    verify_task_result,
)
from app.services.llm_service import (
    create_llm_from_config,
    default_provider_capabilities,
    get_llm,
    resolve_provider_capabilities,
)

logger = logging.getLogger("jarvis.agent")

SUB_COMMANDER_PROMPTS = {
    "schedule_analysis": "你负责做日程判断与聚焦分析，只给判断和依据，不直接落库。",
    "schedule_planning": "你负责把当前目标转成明确安排；当用户要求创建提醒/任务/目标时，直接调用工具执行。",
    "executor_tasks": "你负责执行任务、提醒、目标、待办相关操作，需要时直接调用工具。",
    "executor_forum": "你只处理论坛与指令帖相关操作。",
    "librarian_retrieval": "你负责知识检索与外部搜索，基于证据回答问题。",
    "librarian_graph": "你负责知识图谱上下文与结构化沉淀。",
    "analyst_progress": "你负责进度研判，汇总当前推进情况。",
    "analyst_insights": "你负责趋势、风险和机会判断，必要时调用检索工具。",
}

ROLE_SKILL_CONTEXT = {
    AgentRole.SCHEDULE_PLANNER: "schedule_planner",
    AgentRole.EXECUTOR: "executor",
    AgentRole.LIBRARIAN: "librarian",
    AgentRole.ANALYST: "analyst",
    AgentRole.CODE_COMMANDER: None,
}

from app.agents.prompts import CODE_COMMANDER_SYSTEM_PROMPT

ROLE_SYSTEM_PROMPTS = {
    AgentRole.SCHEDULE_PLANNER: SCHEDULE_PLANNER_SYSTEM_PROMPT,
    AgentRole.EXECUTOR: EXECUTOR_SYSTEM_PROMPT,
    AgentRole.LIBRARIAN: LIBRARIAN_SYSTEM_PROMPT,
    AgentRole.ANALYST: ANALYST_SYSTEM_PROMPT,
    AgentRole.CODE_COMMANDER: CODE_COMMANDER_SYSTEM_PROMPT,
}

SCHEDULE_KEYWORDS = (
    "提醒",
    "日程",
    "安排",
    "计划",
    "排期",
    "会议",
    "开会",
    "明天",
    "今天",
    "后天",
    "下周",
    "本周",
    "周",
    "星期",
    "交付",
    "节点",
    "deadline",
)
ACCOUNTING_INTENT_KEYWORDS = (
    "记账",
    "账单",
    "花了多少钱",
    "用了多少钱",
    "支出",
    "消费",
    "花销",
    "开销",
)
KNOWLEDGE_KEYWORDS = (
    "知识",
    "搜索",
    "检索",
    "资料",
    "文档",
    "联网",
    "上网",
    "查询",
    "查一下",
    "最新",
)
GENERAL_QA_PATTERNS = (
    "介绍一下",
    "介绍下",
    "什么是",
    "是谁",
    "在哪里",
    "为什么",
    "怎么理解",
    "聊聊",
)
ANALYSIS_KEYWORDS = ("分析", "报告", "统计", "趋势", "风险", "洞察", "总结")
EXECUTION_KEYWORDS = ("创建", "更新", "修改", "执行", "发帖", "论坛", "帖子", "完成", "处理")
CODE_COMMANDER_KEYWORDS = (
    "写代码",
    "代码",
    "代码助手",
    "demo",
    "示例项目",
    "贪食蛇",
    "小游戏",
    "帮我写一个",
    "opencode",
    "codex",
    "claude code",
    "gemini code",
)
SCHEDULE_ANALYSIS_KEYWORDS = ("聚焦", "判断", "分析", "优先级", "取舍", "最近对话", "该做什么")
SCHEDULE_PLANNING_KEYWORDS = (
    "安排",
    "计划",
    "排期",
    "提醒",
    "创建",
    "新增",
    "会议",
    "交付",
    "节点",
)
IDENTITY_PATTERNS = ("你是谁", "你是誰")
CAPABILITY_PATTERNS = ("你能做什么", "你可以做什么", "你会做什么")
SHORT_CONFIRMATION_PATTERNS = ("创建", "好的创建", "确认创建", "就创建", "那就创建")
SCHEDULE_CONFIRMATION_HINTS = (
    "创建这条提醒",
    "现在创建这条提醒",
    "现在创建提醒",
    "是否需要我现在创建",
)
SCHEDULE_CONFIRMATION_QUESTION_MARKERS = ("是否", "要不要", "吗", "？", "?")
COLLABORATION_STEP_MARKERS = (
    "然后",
    "再",
    "并且",
    "同时",
    "顺便",
    "最后",
    "分别",
    "拆成",
    "协作",
    "整合",
)
COLLABORATION_ROLE_ORDER = {
    AgentRole.LIBRARIAN: 0,
    AgentRole.ANALYST: 1,
    AgentRole.SCHEDULE_PLANNER: 2,
    AgentRole.EXECUTOR: 3,
}


def _role_value(role: AgentRole | str | None) -> str:
    if isinstance(role, AgentRole):
        return role.value
    return str(role or "")


def _normalize_current_agent(value: AgentRole | str | None) -> str:
    role_value = _role_value(value)
    return role_value or AgentRole.MASTER.value


def _normalize_active_agents(values: list[AgentRole | str] | None) -> list[AgentRole]:
    normalized: list[AgentRole] = []
    for value in values or [AgentRole.MASTER]:
        role_value = _role_value(value)
        try:
            role = AgentRole(role_value)
        except ValueError:
            continue
        if role not in normalized:
            normalized.append(role)
    return normalized or [AgentRole.MASTER]


def _normalize_user_text(text: str) -> str:
    normalized = (text or "").strip().lower()
    normalized = re.sub(r"[，。！？；：,.!?;:\s]+", "", normalized)
    return normalized


def _stringify_message_content(content: Any) -> str:
    if content is None:
        return ""
    if isinstance(content, str):
        return content
    if isinstance(content, list):
        parts: list[str] = []
        for item in content:
            if isinstance(item, str):
                parts.append(item)
                continue
            if isinstance(item, dict):
                text = item.get("text")
                if text:
                    parts.append(str(text))
                    continue
                nested = item.get("content")
                if nested:
                    parts.append(_stringify_message_content(nested))
                    continue
            parts.append(str(item))
        return "".join(parts)
    if isinstance(content, dict):
        text = content.get("text")
        if text:
            return str(text)
        nested = content.get("content")
        if nested:
            return _stringify_message_content(nested)
        return json.dumps(content, ensure_ascii=False)
    return str(content)


def _get_state_int(state: AgentState, key: str) -> int:
    value = state.get(key)
    return value if isinstance(value, int) else 0


def _clear_isolation_state(state: AgentState) -> None:
    state["isolation_mode"] = "none"
    state["isolation_id"] = None
    state["isolation_workspace_path"] = None
    state["isolation_parent_conversation_id"] = None
    state["isolation_metadata"] = {}


def _apply_isolation_payload(state: AgentState, payload: dict[str, Any]) -> None:
    state["isolation_mode"] = str(payload.get("mode") or "none")
    state["isolation_id"] = str(payload.get("isolation_id") or "") or None
    state["isolation_workspace_path"] = str(payload.get("workspace_path") or "") or None
    state["isolation_parent_conversation_id"] = (
        str(payload.get("parent_conversation_id") or "") or None
    )
    state["isolation_metadata"] = dict(payload.get("metadata") or {})


def _prepare_isolation_context(
    state: AgentState,
    *,
    role: AgentRole,
    sub_commander: str,
    user_query: str,
    toolset: list[Any],
) -> None:
    tool_names = [tool.name for tool in toolset]
    decision = select_isolation_strategy(
        user_query=user_query,
        tool_names=tool_names,
        role_value=role.value,
        execution_mode=str(state.get("execution_mode") or "direct"),
    )
    if decision.mode == "none":
        _clear_isolation_state(state)
        _append_event_trace(
            state,
            "agent.isolation.selected",
            payload={"mode": "none", "reason": decision.reason, "tool_names": tool_names},
        )
        return

    if decision.mode == "session":
        isolation_payload = prepare_session_isolation(
            state=state,
            decision=decision,
            role_value=role.value,
            sub_commander=sub_commander,
        )
        _apply_isolation_payload(state, isolation_payload)
        _append_event_trace(
            state,
            "agent.isolation.selected",
            payload=isolation_payload,
        )
        return

    try:
        isolation_payload = prepare_worktree_isolation(
            state=state,
            decision=decision,
            role_value=role.value,
            sub_commander=sub_commander,
        )
    except WorktreeIsolationError as exc:
        isolation_payload = prepare_session_isolation(
            state=state,
            decision=decision,
            role_value=role.value,
            sub_commander=sub_commander,
        )
        isolation_payload["metadata"] = {
            **dict(isolation_payload.get("metadata") or {}),
            "fallback_reason": str(exc),
            "fallback_from": "worktree",
        }
        _append_event_trace(
            state,
            "agent.isolation.fallback",
            payload={
                "requested_mode": "worktree",
                "fallback_mode": "session",
                "reason": str(exc),
                "tool_names": tool_names,
            },
            severity="warning",
        )

    _apply_isolation_payload(state, isolation_payload)
    _append_event_trace(
        state,
        "agent.isolation.selected",
        payload=isolation_payload,
    )


def _record_response_usage(state: AgentState, response: Any) -> None:
    input_tokens, output_tokens = extract_token_usage(response)
    if not input_tokens and not output_tokens:
        return

    current_input_tokens = int(state.get("input_tokens") or 0)
    current_output_tokens = int(state.get("output_tokens") or 0)
    total_input_tokens = current_input_tokens + input_tokens
    total_output_tokens = current_output_tokens + output_tokens
    state["input_tokens"] = total_input_tokens
    state["output_tokens"] = total_output_tokens
    state["estimated_cost"] = estimate_token_cost(total_input_tokens, total_output_tokens)

    thresholds = coerce_cost_thresholds(state.get("cost_thresholds"))
    state["cost_thresholds"] = thresholds
    budget_warning = is_cost_budget_warning(
        total_input_tokens,
        total_output_tokens,
        state.get("estimated_cost"),
        thresholds,
    )
    previous_budget_warning = bool(state.get("budget_warning") or False)
    state["budget_warning"] = budget_warning

    agent_id = str(state.get("agent_id") or state.get("current_agent") or AgentRole.MASTER.value)
    cost_by_agent = {
        key: dict(value) for key, value in dict(state.get("cost_by_agent") or {}).items()
    }
    agent_totals = dict(cost_by_agent.get(agent_id) or {})
    agent_input_tokens = int(agent_totals.get("input_tokens") or 0) + input_tokens
    agent_output_tokens = int(agent_totals.get("output_tokens") or 0) + output_tokens
    agent_estimated_cost = estimate_token_cost(agent_input_tokens, agent_output_tokens)
    cost_by_agent[agent_id] = {
        "agent_id": agent_id,
        "input_tokens": agent_input_tokens,
        "output_tokens": agent_output_tokens,
        "total_tokens": agent_input_tokens + agent_output_tokens,
        "estimated_cost": agent_estimated_cost,
        "budget_warning": is_cost_budget_warning(
            agent_input_tokens,
            agent_output_tokens,
            agent_estimated_cost,
            thresholds,
        ),
    }
    state["cost_by_agent"] = cost_by_agent

    _append_event_trace(
        state,
        "agent.cost.updated",
        payload={
            "agent_id": agent_id,
            "input_tokens_delta": input_tokens,
            "output_tokens_delta": output_tokens,
            "input_tokens": total_input_tokens,
            "output_tokens": total_output_tokens,
            "estimated_cost": state.get("estimated_cost"),
            "budget_warning": budget_warning,
        },
    )
    if budget_warning and not previous_budget_warning:
        _append_event_trace(
            state,
            "agent.cost.warning",
            payload={
                "thresholds": thresholds,
                "input_tokens": total_input_tokens,
                "output_tokens": total_output_tokens,
                "estimated_cost": state.get("estimated_cost"),
            },
            severity="warning",
        )


def _role_values() -> set[str]:
    return {role.value for role in AgentRole}


def _summary_state_key(
    target: str,
) -> Literal["schedule_context_summary", "knowledge_context", "analysis_report"]:
    if target not in {"schedule_context_summary", "knowledge_context", "analysis_report"}:
        raise ValueError(f"unsupported summary target: {target}")
    return cast(Literal["schedule_context_summary", "knowledge_context", "analysis_report"], target)


def _ensure_message_thread(state: AgentState) -> str:
    thread_id = str(state.get("thread_id") or "").strip()
    if thread_id:
        return thread_id
    thread_id = f"thread-{uuid4()}"
    state["thread_id"] = thread_id
    return thread_id


def _bump_message_sequence(state: AgentState) -> tuple[str, int]:
    sequence = int(state.get("message_sequence") or 0) + 1
    state["message_sequence"] = sequence
    message_id = f"msg-{sequence}"
    state["last_message_id"] = message_id
    return message_id, sequence


def _task_payload(task: AgentTask | dict[str, Any] | None) -> dict[str, Any]:
    if isinstance(task, AgentTask):
        return task.model_dump(mode="json")
    return dict(task or {})


def _budget_snapshot_from_state(
    state: AgentState,
    *,
    mode: Literal["direct", "collaboration"],
    remaining_parallel_tasks: int | None = None,
    metadata: dict[str, Any] | None = None,
) -> dict[str, Any]:
    metadata_payload = {
        "max_spawn_depth": 1,
        "max_child_agents": remaining_parallel_tasks,
        "max_messages_per_thread": 24,
        "max_messages_per_turn": 8,
        "max_parallel_collaborators": remaining_parallel_tasks,
        "recovery_attempt_limit": _get_state_int(state, "max_retries") or 1,
        "current_depth": _get_state_int(state, "collaboration_depth"),
        **(metadata or {}),
    }
    budget = CollaborationBudget(
        mode=mode,
        max_parallel_tasks=remaining_parallel_tasks,
        remaining_parallel_tasks=remaining_parallel_tasks,
        max_tool_calls=_get_state_int(state, "max_tool_rounds") or None,
        remaining_tool_calls=max(
            _get_state_int(state, "max_tool_rounds") - _get_state_int(state, "tool_round_count"), 0
        )
        if _get_state_int(state, "max_tool_rounds")
        else None,
        max_iterations=_get_state_int(state, "max_iterations") or None,
        remaining_iterations=max(
            _get_state_int(state, "max_iterations") - _get_state_int(state, "iteration_count"), 0
        )
        if _get_state_int(state, "max_iterations")
        else None,
        metadata=metadata_payload,
    )
    return budget.model_dump(mode="json")


def _store_budget_snapshot(state: AgentState, budget_snapshot: dict[str, Any]) -> None:
    state["budget_state"] = budget_snapshot
    state["collaboration_budget_history"] = [
        *(state.get("collaboration_budget_history") or []),
        budget_snapshot,
    ]


def _budget_metadata(state: AgentState) -> dict[str, Any]:
    budget_state = state.get("budget_state") or {}
    if isinstance(budget_state, CollaborationBudget):
        return dict(budget_state.metadata or {})
    if isinstance(budget_state, dict):
        return dict(budget_state.get("metadata") or {})
    return {}


def _budget_limit(state: AgentState, key: str, default: int) -> int:
    value = _budget_metadata(state).get(key)
    return value if isinstance(value, int) and value > 0 else default


def _append_message_trace(
    state: AgentState,
    *,
    from_agent_id: str,
    to_agent_id: str,
    message_type: str,
    content_summary: str,
    task_id: str | None = None,
    reply_to_message_id: str | None = None,
    payload: dict[str, Any] | None = None,
    message_id: str | None = None,
    message_index: int | None = None,
) -> dict[str, Any]:
    thread_id = _ensure_message_thread(state)
    if message_id is None or message_index is None:
        message_id, message_index = _bump_message_sequence(state)
    message = AgentMessage(
        message_id=message_id,
        thread_id=thread_id,
        from_agent_id=from_agent_id,
        to_agent_id=to_agent_id,
        task_id=task_id,
        reply_to_message_id=reply_to_message_id,
        message_type=cast(Any, message_type),
        content_summary=content_summary[:400],
        payload=payload or {},
    )
    serialized = message.model_dump(mode="json")
    state["message_trace"] = [*(state.get("message_trace") or []), serialized]
    return serialized


def _create_child_agent(
    state: AgentState,
    *,
    role: AgentRole,
    task: AgentTask,
) -> str | None:
    parent_agent_id = str(state.get("agent_id") or AgentRole.MASTER.value)
    if not _spawn_permission_for_role(state, role):
        _append_event_trace(
            state,
            "agent.spawn.blocked",
            payload={"reason": "role_policy_blocked", "target_role": role.value},
            severity="warning",
            task_id=task.task_id,
            parent_task_id=task.parent_task_id,
            child_task_id=task.task_id,
        )
        return None
    current_depth = _get_state_int(state, "collaboration_depth")
    if current_depth >= _budget_limit(state, "max_spawn_depth", 1):
        _append_event_trace(
            state,
            "agent.spawn.blocked",
            payload={
                "reason": "max_spawn_depth_exceeded",
                "target_role": role.value,
                "depth": current_depth,
            },
            severity="warning",
            task_id=task.task_id,
            parent_task_id=task.parent_task_id,
            child_task_id=task.task_id,
        )
        return None
    spawned_agent_ids = list(state.get("spawned_agent_ids") or [])
    if len(spawned_agent_ids) >= _budget_limit(state, "max_child_agents", 4):
        _append_event_trace(
            state,
            "agent.spawn.blocked",
            payload={"reason": "max_child_agents_exceeded", "target_role": role.value},
            severity="warning",
            task_id=task.task_id,
            parent_task_id=task.parent_task_id,
            child_task_id=task.task_id,
        )
        return None
    child_agent_id = f"{role.value}-{uuid4().hex[:8]}"
    state["spawned_agent_ids"] = [*spawned_agent_ids, child_agent_id]
    _append_event_trace(
        state,
        "agent.created",
        payload={
            "parent_agent_id": parent_agent_id,
            "child_agent_id": child_agent_id,
            "target_role": role.value,
        },
        task_id=task.task_id,
        parent_task_id=task.parent_task_id,
        child_task_id=task.task_id,
    )
    return child_agent_id


def _collaboration_task_from_state(state: AgentState) -> dict[str, Any]:
    return dict((state.get("turn_context") or {}).get("collaboration_task") or {})


def _record_interrupt(
    state: AgentState,
    *,
    reason: str,
    task: AgentTask | dict[str, Any] | None = None,
    payload: dict[str, Any] | None = None,
) -> InterruptRecord:
    task_payload = (
        _task_payload(task) if task is not None else _collaboration_task_from_state(state)
    )
    interrupt = InterruptRecord(
        interrupt_id=f"interrupt-{uuid4()}",
        reason=reason,
        requested_by=_role_value(state.get("current_agent")),
        payload=payload or {},
    )
    interrupt_payload = interrupt.model_dump(mode="json")
    state["interrupted_tasks"] = [*(state.get("interrupted_tasks") or []), interrupt_payload]
    state["recovery_points"] = [
        *(state.get("recovery_points") or []),
        {
            "interrupt_id": interrupt.interrupt_id,
            "task_id": task_payload.get("task_id"),
            "thread_id": state.get("thread_id"),
            "message_id": state.get("last_message_id"),
        },
    ]
    _append_event_trace(
        state,
        "agent.interrupt.requested",
        payload={"reason": reason, **(payload or {})},
        severity="warning",
        task_id=task_payload.get("task_id"),
        parent_task_id=task_payload.get("parent_task_id"),
        child_task_id=(task_payload.get("child_task_ids") or [None])[0],
        interrupt_id=interrupt.interrupt_id,
    )
    _append_event_trace(
        state,
        "agent.task.interrupted",
        payload={"reason": reason, **(payload or {})},
        severity="warning",
        task_id=task_payload.get("task_id"),
        parent_task_id=task_payload.get("parent_task_id"),
        child_task_id=(task_payload.get("child_task_ids") or [None])[0],
        interrupt_id=interrupt.interrupt_id,
    )
    _append_event_trace(
        state,
        "agent.interrupt.completed",
        payload={"reason": reason, **(payload or {})},
        severity="warning",
        task_id=task_payload.get("task_id"),
        parent_task_id=task_payload.get("parent_task_id"),
        child_task_id=(task_payload.get("child_task_ids") or [None])[0],
        interrupt_id=interrupt.interrupt_id,
    )
    return interrupt


def _record_recovery(
    state: AgentState,
    *,
    interrupt: InterruptRecord,
    strategy: str,
    task: AgentTask | dict[str, Any] | None = None,
    payload: dict[str, Any] | None = None,
) -> RecoveryRecord:
    task_payload = (
        _task_payload(task) if task is not None else _collaboration_task_from_state(state)
    )
    recovery = RecoveryRecord(
        recovery_id=f"recovery-{uuid4()}",
        source_interrupt_id=interrupt.interrupt_id,
        strategy=strategy,
        resumed_from_task_id=task_payload.get("task_id"),
        resumed_from_thread_id=str(state.get("thread_id") or "") or None,
        payload=payload or {},
    )
    recovery_payload = recovery.model_dump(mode="json")
    state["recovery_trace"] = [*(state.get("recovery_trace") or []), recovery_payload]
    _append_event_trace(
        state,
        "agent.recovery.started",
        payload={"strategy": strategy, **(payload or {})},
        task_id=task_payload.get("task_id"),
        parent_task_id=task_payload.get("parent_task_id"),
        child_task_id=(task_payload.get("child_task_ids") or [None])[0],
        recovery_id=recovery.recovery_id,
        interrupt_id=interrupt.interrupt_id,
    )
    _append_event_trace(
        state,
        "agent.task.recovered",
        payload={"strategy": strategy, **(payload or {})},
        task_id=task_payload.get("task_id"),
        parent_task_id=task_payload.get("parent_task_id"),
        child_task_id=(task_payload.get("child_task_ids") or [None])[0],
        recovery_id=recovery.recovery_id,
        interrupt_id=interrupt.interrupt_id,
    )
    _append_event_trace(
        state,
        "agent.recovery.completed",
        payload={"strategy": strategy, **(payload or {})},
        task_id=task_payload.get("task_id"),
        parent_task_id=task_payload.get("parent_task_id"),
        child_task_id=(task_payload.get("child_task_ids") or [None])[0],
        recovery_id=recovery.recovery_id,
        interrupt_id=interrupt.interrupt_id,
    )
    return recovery


def _spawn_permission_for_role(state: AgentState, role: AgentRole) -> bool:
    indexes = load_builtin_registry_indexes()
    current_role_value = _normalize_current_agent(state.get("current_agent"))
    current_manifest = indexes.agent_by_role_value.get(current_role_value)
    if current_manifest is None or not current_manifest.can_spawn_children:
        return False
    allowed_roles = indexes.spawnable_role_values_by_agent_id.get(current_manifest.agent_id, ())
    return role.value in allowed_roles


def _get_llm_for_state(state: AgentState):
    user_llm_config = state.get("user_llm_config")
    llm = create_llm_from_config(user_llm_config) if user_llm_config else get_llm()
    capabilities = getattr(llm, "_jarvis_provider_capabilities", None)
    if capabilities is None:
        capabilities = (
            resolve_provider_capabilities(user_llm_config)
            if user_llm_config
            else default_provider_capabilities()
        )
    state["provider_capabilities"] = {
        "provider": capabilities.provider,
        "supports_native_tools": capabilities.supports_native_tools,
        "preferred_tool_strategy": capabilities.preferred_tool_strategy,
    }
    return llm


def _resolve_capabilities(state: AgentState, llm) -> Any:
    capabilities = getattr(llm, "_jarvis_provider_capabilities", None)
    if capabilities is None:
        config = state.get("user_llm_config")
        capabilities = (
            resolve_provider_capabilities(config) if config else default_provider_capabilities()
        )
    state["provider_capabilities"] = {
        "provider": capabilities.provider,
        "supports_native_tools": capabilities.supports_native_tools,
        "preferred_tool_strategy": capabilities.preferred_tool_strategy,
    }
    return capabilities


def _filter_user_messages(messages: list[BaseMessage]) -> list[BaseMessage]:
    return [message for message in messages if getattr(message, "type", "") in {"human", "user"}]


def _coalesce_system_messages(messages: list[BaseMessage]) -> list[BaseMessage]:
    system_parts: list[str] = []
    non_system_messages: list[BaseMessage] = []

    for message in messages:
        if getattr(message, "type", "") == "system":
            text = _stringify_message_content(getattr(message, "content", ""))
            if text:
                system_parts.append(text)
            continue
        non_system_messages.append(message)

    if not system_parts:
        return non_system_messages

    return [SystemMessage(content="\n\n".join(system_parts)), *non_system_messages]


def _is_simple_greeting(text: str) -> bool:
    return _normalize_user_text(text) in {
        "你好",
        "您好",
        "早",
        "早上好",
        "在吗",
        "嗨",
        "hi",
        "hello",
    }


def _is_identity_question(text: str) -> bool:
    normalized = _normalize_user_text(text)
    return any(normalized.startswith(pattern) for pattern in IDENTITY_PATTERNS)


def _is_capability_question(text: str) -> bool:
    normalized = _normalize_user_text(text)
    return any(normalized.startswith(pattern) for pattern in CAPABILITY_PATTERNS)


def _is_short_confirmation(text: str) -> bool:
    normalized = _normalize_user_text(text)
    return normalized in SHORT_CONFIRMATION_PATTERNS


def _tool_result_indicates_failure(result: Any) -> bool:
    text = _stringify_message_content(result)
    return "失败" in text


def _latest_assistant_message_content(messages: list[BaseMessage]) -> str:
    previous_assistant_message = next(
        (message for message in reversed(messages[:-1]) if getattr(message, "type", "") == "ai"),
        None,
    )
    if previous_assistant_message is None:
        return ""
    return _stringify_message_content(getattr(previous_assistant_message, "content", ""))


def _previous_turn_proposed_schedule_creation(messages: list[BaseMessage]) -> bool:
    if len(messages) < 2:
        return False
    content = _latest_assistant_message_content(messages)
    has_schedule_confirmation_hint = any(hint in content for hint in SCHEDULE_CONFIRMATION_HINTS)
    has_question_marker = any(
        marker in content for marker in SCHEDULE_CONFIRMATION_QUESTION_MARKERS
    )
    return has_schedule_confirmation_hint and has_question_marker


def _previous_turn_completed_reminder_creation(state: AgentState) -> bool:
    created_entities = state.get("created_entities") or []
    if not created_entities:
        return False
    latest_entity = created_entities[-1]
    if latest_entity.get("type") != "reminder":
        return False
    tool_calls = state.get("tool_calls") or []
    if not tool_calls:
        return False
    latest_tool_call = tool_calls[-1]
    if latest_tool_call.get("name") != "create_reminder":
        return False
    messages = state.get("messages") or []
    if len(messages) < 2:
        return False
    if getattr(messages[-1], "type", "") not in {"human", "user"}:
        return False
    previous_message = messages[-2]
    if getattr(previous_message, "type", "") != "ai":
        return False
    previous_assistant_content = _stringify_message_content(
        getattr(previous_message, "content", "")
    )
    completion_markers = ("已创建提醒", "提醒已经创建好了", "帮你设好了这条提醒", "创建成功")
    return any(marker in previous_assistant_content for marker in completion_markers)


def _latest_non_confirmation_user_request(messages: list[BaseMessage]) -> str | None:
    user_messages = [
        _stringify_message_content(getattr(message, "content", "")).strip()
        for message in messages
        if getattr(message, "type", "") in {"human", "user"}
    ]
    for content in reversed(user_messages[:-1]):
        if content and not _is_short_confirmation(content):
            return content
    return None


def _expand_schedule_confirmation_query(user_query: str, messages: list[BaseMessage]) -> str:
    previous_request = _latest_non_confirmation_user_request(messages)
    if not previous_request:
        return user_query
    return f"用户确认继续创建上一条提醒安排：{previous_request}"


def _is_schedule_creation_confirmation_response(response_text: str) -> bool:
    content = _stringify_message_content(response_text)
    has_schedule_confirmation_hint = any(hint in content for hint in SCHEDULE_CONFIRMATION_HINTS)
    has_question_marker = any(
        marker in content for marker in SCHEDULE_CONFIRMATION_QUESTION_MARKERS
    )
    return has_schedule_confirmation_hint and has_question_marker


def _write_schedule_creation_continuity(state: AgentState, user_query: str) -> None:
    summary = user_query.strip()
    if not summary:
        return
    state["pending_action"] = {
        "type": "schedule_creation",
        "summary": summary,
        "status": "pending",
    }
    state["routing_decision"] = {
        "target_agent": AgentRole.SCHEDULE_PLANNER.value,
        "reason": "continue_pending_action",
    }
    state["continuity_state"] = {"status": "fresh"}


def _clear_structured_continuity(state: AgentState) -> None:
    state["pending_action"] = None
    state["routing_decision"] = None
    state["continuity_state"] = None


def _should_clear_schedule_creation_continuity(
    state: AgentState, created_entities: list[dict[str, Any]]
) -> bool:
    if not _has_active_structured_continuation(state):
        return False
    pending_action = state.get("pending_action") or {}
    if pending_action.get("type") != "schedule_creation":
        return False
    return any(entity.get("type") == "reminder" for entity in created_entities)


def _classify_request_signals(user_query: str) -> dict[str, Any]:
    text = (user_query or "").strip().lower()
    has_accounting_signal = any(keyword in text for keyword in ACCOUNTING_INTENT_KEYWORDS)
    has_schedule_signal = not has_accounting_signal and (
        bool(re.search(r"\d{1,2}月\d{1,2}日", text))
        or any(keyword in text for keyword in SCHEDULE_KEYWORDS)
    )
    has_analysis_signal = any(keyword in text for keyword in ANALYSIS_KEYWORDS)
    has_knowledge_signal = any(keyword in text for keyword in KNOWLEDGE_KEYWORDS)
    has_execution_signal = has_accounting_signal or any(
        keyword in text for keyword in EXECUTION_KEYWORDS
    )

    roles: list[AgentRole] = []
    if has_knowledge_signal:
        roles.append(AgentRole.LIBRARIAN)
    if has_analysis_signal:
        roles.append(AgentRole.ANALYST)
    if has_schedule_signal:
        roles.append(AgentRole.SCHEDULE_PLANNER)
    if has_execution_signal:
        roles.append(AgentRole.EXECUTOR)

    return {
        "text": text,
        "has_accounting_signal": has_accounting_signal,
        "has_schedule_signal": has_schedule_signal,
        "has_analysis_signal": has_analysis_signal,
        "has_knowledge_signal": has_knowledge_signal,
        "has_execution_signal": has_execution_signal,
        "roles": roles,
    }


def _select_request_mode(
    user_query: str,
) -> tuple[Literal["direct", "collaboration"], dict[str, Any]]:
    signals = _classify_request_signals(user_query)
    roles: list[AgentRole] = signals["roles"]
    text = signals["text"]
    has_multi_step_signal = any(marker in text for marker in COLLABORATION_STEP_MARKERS)
    is_explicit_collaboration_request = "协作" in text or ("拆" in text and "任务" in text)
    is_long_request = len((user_query or "").strip()) >= 24

    should_collaborate = len(roles) >= 3 or (
        len(roles) >= 2
        and (has_multi_step_signal or is_explicit_collaboration_request or is_long_request)
    )
    selected_mode: Literal["direct", "collaboration"] = (
        "collaboration" if should_collaborate else "direct"
    )
    metadata = {
        "mode": selected_mode,
        "reason": "multi_role_request" if should_collaborate else "single_role_or_simple_request",
        "roles": [role.value for role in roles],
        "multi_step": has_multi_step_signal,
    }
    return selected_mode, metadata


def _route_agent_from_user_query(user_query: str) -> AgentRole:
    signals = _classify_request_signals(user_query)
    text = signals["text"]

    # Code Commander routing - check first as it has specific keywords
    if any(keyword in text for keyword in CODE_COMMANDER_KEYWORDS):
        return AgentRole.CODE_COMMANDER

    if signals["has_accounting_signal"]:
        return AgentRole.EXECUTOR

    if signals["has_schedule_signal"]:
        return AgentRole.SCHEDULE_PLANNER

    if signals["has_analysis_signal"]:
        return AgentRole.ANALYST

    if signals["has_knowledge_signal"]:
        return AgentRole.LIBRARIAN

    if any(pattern in text for pattern in GENERAL_QA_PATTERNS):
        return AgentRole.MASTER

    if signals["has_execution_signal"]:
        return AgentRole.EXECUTOR
    return AgentRole.MASTER


def _choose_sub_commander(role: AgentRole, user_query: str) -> str:
    text = (user_query or "").strip().lower()

    if role == AgentRole.SCHEDULE_PLANNER:
        if re.search(r"\d{1,2}月\d{1,2}日", text) or any(
            keyword in text for keyword in SCHEDULE_PLANNING_KEYWORDS
        ):
            return "schedule_planning"
        return "schedule_analysis"
    if role == AgentRole.EXECUTOR:
        if any(keyword in text for keyword in ("论坛", "帖子", "发帖", "指令")):
            return "executor_forum"
        return "executor_tasks"
    if role == AgentRole.LIBRARIAN:
        if any(keyword in text for keyword in ("图谱", "关系", "沉淀", "graph")):
            return "librarian_graph"
        return "librarian_retrieval"
    if role == AgentRole.ANALYST:
        if any(keyword in text for keyword in ("趋势", "风险", "洞察", "建议", "机会")):
            return "analyst_insights"
        return "analyst_progress"
    raise ValueError(f"unsupported role: {role}")


def _is_missing_knowledge_result(tool_result: str | None) -> bool:
    text = (tool_result or "").strip()
    if not text:
        return True
    markers = (
        "未找到相关知识",
        "知识库可能为空",
        "未找到相关网页结果",
        "暂无相关记录",
        "没有找到",
    )
    return any(marker in text for marker in markers)


def _extract_json_object(content: str) -> str | None:
    text = (content or "").strip()
    if not text:
        return None

    fenced_match = re.search(r"```(?:json)?\s*(\{.*?\})\s*```", text, re.DOTALL | re.IGNORECASE)
    if fenced_match:
        return fenced_match.group(1)

    decoder = json.JSONDecoder()
    for index, char in enumerate(text):
        if char != "{":
            continue
        try:
            _, end = decoder.raw_decode(text[index:])
            return text[index : index + end]
        except json.JSONDecodeError:
            continue
    return None


def _parse_json_action(content: str, allowed_tools: list[str]) -> dict[str, Any] | None:
    json_text = _extract_json_object(_stringify_message_content(content))
    if not json_text:
        return None

    try:
        payload = json.loads(json_text)
    except json.JSONDecodeError:
        return None

    if not isinstance(payload, dict):
        return None

    mode = payload.get("mode")
    if mode == "tool_call":
        tool_calls: list[dict[str, Any]] = []
        raw_tool_calls = payload.get("tool_calls", [])
        if not isinstance(raw_tool_calls, list):
            return None
        for item in raw_tool_calls:
            if not isinstance(item, dict):
                return None
            name = item.get("name")
            if name not in allowed_tools:
                return None
            args = item.get("arguments")
            if args is None:
                args = item.get("parameters")
            if not isinstance(args, dict):
                return None
            tool_calls.append(
                {
                    "name": name,
                    "args": args,
                    "reason": item.get("reason"),
                }
            )
        return {"mode": mode, "tool_calls": tool_calls}

    if mode == "final" and isinstance(payload.get("final_response"), str):
        return {"mode": mode, "final_response": payload["final_response"]}

    if mode == "clarification" and isinstance(payload.get("clarification_question"), str):
        return {"mode": mode, "clarification_question": payload["clarification_question"]}

    return None


def _looks_like_internal_tool_markup(content: str) -> bool:
    text = (content or "").strip()
    if not text:
        return False

    lowered = text.lower()
    xml_markers = (
        "<minimax:tool_call",
        "</minimax:tool_call>",
        "<invoke name=",
        "</invoke>",
        "<parameter name=",
        "</parameter>",
    )
    if any(marker in lowered for marker in xml_markers):
        return True

    return "分发说明" in text and ("<invoke name=" in lowered or "tool_call" in lowered)


def _clean_tool_result_for_user(tool_result: str | None) -> str:
    text = (tool_result or "").strip()
    if not text:
        return ""

    cleaned_lines = [
        re.sub(r"^\[[^\]]+\]\s*", "", line).strip()
        for line in text.splitlines()
        if line.strip()
    ]
    return "\n".join(line for line in cleaned_lines if line).strip()


def _build_internal_markup_fallback_response(
    state: AgentState,
    *,
    sub_commander: str,
) -> str | None:
    tool_result = state.get("last_tool_result")
    cleaned_tool_result = _clean_tool_result_for_user(tool_result)
    if not cleaned_tool_result or _tool_result_indicates_failure(cleaned_tool_result):
        return None

    if sub_commander == "librarian_retrieval":
        if _is_missing_knowledge_result(tool_result):
            return "这次检索没有拿到有效证据。我先不展示内部调度过程；如果您愿意，我可以直接基于常识回答，或改为联网搜索后再整理。"
        return f"我已经完成检索，直接给您可用信息：\n\n{cleaned_tool_result}"

    return cleaned_tool_result


def _has_active_structured_continuation(state: AgentState) -> bool:
    pending_action = state.get("pending_action") or {}
    routing_decision = state.get("routing_decision") or {}
    continuity_state = state.get("continuity_state") or {}

    if continuity_state.get("status") != "fresh":
        return False
    if pending_action.get("status") != "pending":
        return False
    if routing_decision.get("reason") != "continue_pending_action":
        return False
    target_agent = routing_decision.get("target_agent")
    return target_agent in _role_values()


def _route_from_structured_continuity(state: AgentState, user_query: str) -> AgentRole | None:
    if not _is_short_confirmation(user_query):
        return None
    if not _has_active_structured_continuation(state):
        return None

    target_agent = (state.get("routing_decision") or {}).get("target_agent")

    try:
        return AgentRole(str(target_agent))
    except ValueError:
        return None


def _build_structured_continuity_summary(state: AgentState) -> str | None:
    pending_action = state.get("pending_action")
    routing_decision = state.get("routing_decision")

    if not pending_action or not routing_decision:
        return None
    if not _has_active_structured_continuation(state):
        return None

    action_type = str(pending_action.get("type") or "unknown")
    action_summary = str(pending_action.get("summary") or "")
    routing_reason = str(routing_decision.get("reason") or "")
    target_agent = str(routing_decision.get("target_agent") or "")

    lines = [
        "structured_continuity:",
        f"- pending_action.type: {action_type}",
    ]
    if action_summary:
        lines.append(f"- pending_action.summary: {action_summary}")
    if target_agent:
        lines.append(f"- routing_decision.target_agent: {target_agent}")
    if routing_reason:
        lines.append(f"- routing_decision.reason: {routing_reason}")
    lines.append("- instruction: continue_pending_action")
    return "\n".join(lines)


def _build_retrospective_context_summary(state: AgentState) -> str | None:
    retrospectives = list(state.get("recalled_retrospectives") or [])
    if not retrospectives:
        return None

    lines = ["【相关历史复盘】"]
    for item in retrospectives[:2]:
        if not isinstance(item, dict):
            continue
        request_summary = str(item.get("request_summary") or item.get("task_type") or "").strip()
        execution_mode = str(item.get("execution_mode") or "").strip()
        success_score = float(item.get("success_score") or 0.0)
        reusable_patterns = list(item.get("reusable_patterns") or [])
        avoid_patterns = list(item.get("avoid_patterns") or [])

        summary_parts = [request_summary[:80] or execution_mode or "历史任务"]
        if execution_mode:
            summary_parts.append(f"mode={execution_mode}")
        summary_parts.append(f"score={success_score:.2f}")
        if reusable_patterns:
            summary_parts.append(f"可复用={','.join(reusable_patterns[:2])}")
        elif avoid_patterns:
            summary_parts.append(f"避坑={','.join(avoid_patterns[:2])}")

        lines.append(f"- {'；'.join(summary_parts)}")

    return "\n".join(lines) if len(lines) > 1 else None


def _estimate_request_complexity(user_query: str, selected_roles: list[str]) -> float:
    text = (user_query or "").strip()
    base = min(len(text) / 120.0, 1.0)
    role_boost = min(len(selected_roles) * 0.2, 0.6)
    return round(min(base + role_boost, 1.0), 2)


def _record_execution_decision(
    state: AgentState,
    *,
    user_query: str,
    mode: Literal["direct", "collaboration", "parallel"],
    reason: str,
    selected_roles: list[str] | None = None,
    parallel_worthiness_score: float | None = None,
) -> None:
    runtime_request_context = state.get("runtime_request_context") or {}
    request_id = str(runtime_request_context.get("request_id") or state.get("conversation_id") or "")
    roles = list(selected_roles or [])
    decision = ExecutionDecision(
        request_id=request_id or f"request-{uuid4().hex[:8]}",
        mode=mode,
        reason=reason,
        complexity_score=_estimate_request_complexity(user_query, roles),
        parallel_worthiness_score=parallel_worthiness_score,
        selected_roles=roles,
    )
    state["execution_decision"] = decision.model_dump(mode="json")
    _append_event_trace(
        state,
        "agent.execution.decided",
        payload=state["execution_decision"],
    )


def _build_system_messages(
    state: AgentState, system_prompt: str, role: AgentRole, sub_commander: str
) -> list[BaseMessage]:
    messages: list[BaseMessage] = [SystemMessage(content=system_prompt)]

    current_datetime_context = state.get("current_datetime_context")
    if current_datetime_context:
        messages.append(SystemMessage(content=current_datetime_context))

    runtime_request_context = state.get("runtime_request_context")
    if isinstance(runtime_request_context, dict) and runtime_request_context:
        try:
            runtime_context_model = RuntimeRequestContext.model_validate(runtime_request_context)
        except Exception:
            runtime_context_model = None
        if runtime_context_model is not None:
            messages.append(
                SystemMessage(
                    content=render_runtime_request_context_summary(runtime_context_model)
                )
            )

    continuity_summary = _build_structured_continuity_summary(state)
    if continuity_summary:
        messages.append(SystemMessage(content=continuity_summary))

    clarification_summary = _build_clarification_summary(state)
    if clarification_summary:
        messages.append(SystemMessage(content=clarification_summary))

    collaboration_summary = _build_collaboration_context_summary(state)
    if collaboration_summary:
        messages.append(SystemMessage(content=collaboration_summary))

    retrospective_summary = _build_retrospective_context_summary(state)
    if retrospective_summary:
        messages.append(SystemMessage(content=retrospective_summary))

    role_context_map = {
        AgentRole.SCHEDULE_PLANNER: state.get("schedule_context_summary"),
        AgentRole.LIBRARIAN: state.get("knowledge_context"),
        AgentRole.ANALYST: state.get("analysis_report"),
    }
    role_context = role_context_map.get(role)
    if role_context:
        messages.append(SystemMessage(content=f"角色上下文：\n{role_context}"))

    role_skill_key = ROLE_SKILL_CONTEXT.get(role)
    if role_skill_key:
        shortlisted_context = build_shortlisted_skill_context(
            state.get("skill_shortlist"),
            agent_type=role_skill_key,
        )
        skill_context = shortlisted_context or build_skill_context(role_skill_key)
        if skill_context:
            messages.append(SystemMessage(content=skill_context))

    messages.append(SystemMessage(content=f"本次应由子指挥官 `{sub_commander}` 接手。"))
    messages.append(SystemMessage(content=SUB_COMMANDER_PROMPTS[sub_commander]))
    return messages


def _assign_agent_for_task_role(role_value: str | None) -> AgentRole:
    try:
        return AgentRole(str(role_value or AgentRole.MASTER.value))
    except ValueError:
        return AgentRole.MASTER


def _build_task_title(role: AgentRole) -> str:
    titles = {
        AgentRole.LIBRARIAN: "补齐事实与证据",
        AgentRole.ANALYST: "给出分析与判断",
        AgentRole.SCHEDULE_PLANNER: "形成安排与计划",
        AgentRole.EXECUTOR: "执行必要动作",
    }
    return titles.get(role, "处理协作任务")


def _build_task_goal(role: AgentRole, user_query: str) -> str:
    goals = {
        AgentRole.LIBRARIAN: f"围绕用户请求检索并整理能直接支撑结论的证据：{user_query}",
        AgentRole.ANALYST: f"基于已有上下文与证据，对用户请求给出结论、风险和建议：{user_query}",
        AgentRole.SCHEDULE_PLANNER: f"把用户请求收束为明确安排、节奏或日程建议：{user_query}",
        AgentRole.EXECUTOR: f"基于用户请求和前序结论执行必要动作，并回收结果：{user_query}",
    }
    return goals.get(role, user_query)


def _build_expected_evidence(role: AgentRole) -> list[dict[str, Any]]:
    evidence_map = {
        AgentRole.LIBRARIAN: [
            {"type": "evidence", "detail": "retrieval findings or source-backed context"}
        ],
        AgentRole.ANALYST: [{"type": "analysis", "detail": "judgment with supporting rationale"}],
        AgentRole.SCHEDULE_PLANNER: [
            {"type": "plan", "detail": "explicit schedule or next-step proposal"}
        ],
        AgentRole.EXECUTOR: [
            {"type": "execution", "detail": "tool output or execution confirmation"}
        ],
    }
    return evidence_map.get(role, [{"type": "summary", "detail": "task evidence"}])


def _build_collaboration_tasks(user_query: str) -> list[AgentTask]:
    _, metadata = _select_request_mode(user_query)
    raw_roles = metadata.get("roles") or []
    roles = sorted(
        (_assign_agent_for_task_role(role_value) for role_value in raw_roles),
        key=lambda role: COLLABORATION_ROLE_ORDER.get(role, 99),
    )
    unique_roles: list[AgentRole] = []
    for role in roles:
        if role not in unique_roles and role != AgentRole.MASTER:
            unique_roles.append(role)

    parent_task_id = f"task-root-{uuid4().hex[:8]}"
    task_ids = [
        f"task-{index}-{uuid4().hex[:8]}" for index, _ in enumerate(unique_roles[:4], start=1)
    ]
    tasks: list[AgentTask] = []
    for index, role in enumerate(unique_roles[:4]):
        tasks.append(
            AgentTask(
                task_id=task_ids[index],
                title=_build_task_title(role),
                owner_agent_id=role.value,
                role=role.value,
                goal=_build_task_goal(role, user_query),
                parent_task_id=parent_task_id,
                child_task_ids=[task_ids[index + 1]] if index + 1 < len(task_ids) else [],
                expected_evidence=_build_expected_evidence(role),
            )
        )
    return tasks


def _build_collaboration_plan_from_task_graph(
    state: AgentState,
    user_query: str,
) -> tuple[list[AgentTask], list[dict[str, Any]]]:
    raw_task_graph = state.get("task_graph")
    if not isinstance(raw_task_graph, dict) or not raw_task_graph.get("nodes"):
        return _build_collaboration_tasks(user_query), []

    task_graph = TaskGraph.model_validate(raw_task_graph)
    specs = [
        spec
        for spec in build_subtask_specs(task_graph, query_text=user_query)
        if spec.role != "master"
    ]
    child_links = ensure_child_links(specs)
    tasks: list[AgentTask] = []
    for spec in specs:
        task = subtask_spec_to_agent_task(spec)
        task.child_task_ids = child_links.get(spec.subtask_id, [])
        tasks.append(task)
    return tasks, [spec.model_dump(mode="json") for spec in specs]


def _build_collaboration_context_summary(state: AgentState) -> str | None:
    if state.get("execution_mode") != "collaboration":
        return None

    lines = [COORDINATOR_SYSTEM_PROMPT]
    current_task = (state.get("turn_context") or {}).get("collaboration_task") or {}
    if current_task:
        lines.extend(
            [
                "current_collaboration_task:",
                f"- task_id: {current_task.get('task_id')}",
                f"- title: {current_task.get('title')}",
                f"- role: {current_task.get('role')}",
                f"- goal: {current_task.get('goal')}",
            ]
        )

    task_results = state.get("task_results") or []
    if task_results:
        lines.append("completed_collaboration_results:")
        for item in task_results[-4:]:
            normalized = (
                item.model_dump(mode="json") if isinstance(item, TaskResult) else dict(item)
            )
            lines.append(
                f"- {normalized.get('task_id')} | {normalized.get('status')} | {normalized.get('summary') or ''}"
            )

    return "\n".join(lines)


def _maybe_reset_turn_budgets(state: AgentState) -> None:
    messages = state.get("messages") or []
    if not messages:
        state["routing_hops"] = 0
        state["terminated_due_to_loop_guard"] = False
        state["iteration_count"] = 0
        state["tool_round_count"] = 0
        state["retry_count"] = 0
        state["stop_reason"] = None
        state["clarification_needed"] = False
        state["clarification_question"] = None
        state["final_response"] = None
        return

    last_message_type = getattr(messages[-1], "type", "")
    has_prior_assistant_turn = any(
        getattr(message, "type", "") == "ai" for message in messages[:-1]
    )
    if last_message_type in {"human", "user"} and has_prior_assistant_turn:
        state["routing_hops"] = 0
        state["terminated_due_to_loop_guard"] = False
        state["iteration_count"] = 0
        state["tool_round_count"] = 0
        state["retry_count"] = 0
        state["stop_reason"] = None
        state["clarification_needed"] = False
        state["clarification_question"] = None
        state["final_response"] = None


def _conversation_history_messages(state: AgentState) -> list[BaseMessage]:
    history = list(state.get("messages", []))
    return [message for message in history if getattr(message, "type", "") != "system"]


def _append_event_trace(
    state: AgentState,
    event_type: str,
    *,
    payload: dict[str, Any] | None = None,
    severity: str = "info",
    task_id: str | None = None,
    parent_task_id: str | None = None,
    child_task_id: str | None = None,
    interrupt_id: str | None = None,
    recovery_id: str | None = None,
    message_id: str | None = None,
) -> None:
    thread_id = _ensure_message_thread(state)
    event = AgentEvent(
        event_id=f"evt-{uuid4()}",
        event_type=cast(Any, event_type),
        conversation_id=str(state.get("conversation_id") or "") or None,
        agent_id=_role_value(state.get("current_agent")),
        sub_commander_id=state.get("current_sub_commander"),
        task_id=task_id,
        parent_task_id=parent_task_id,
        child_task_id=child_task_id,
        thread_id=thread_id,
        message_id=message_id or (str(state.get("last_message_id") or "") or None),
        interrupt_id=interrupt_id,
        recovery_id=recovery_id,
        payload=payload or {},
        severity=cast(Any, severity),
    )
    state["event_trace"] = [
        *(state.get("event_trace") or []),
        event.model_dump(mode="json"),
    ]


def _set_phase(
    state: AgentState, phase: str, *, reason: str, payload: dict[str, Any] | None = None
) -> None:
    if state.get("current_phase") == phase:
        return
    state["current_phase"] = phase
    state["phase_history"] = [
        *(state.get("phase_history") or []),
        {
            "phase": phase,
            "reason": reason,
            **({"payload": payload} if payload else {}),
        },
    ]
    _append_event_trace(
        state,
        "agent.phase.changed",
        payload={"phase": phase, "reason": reason, **(payload or {})},
    )


def _record_checkpoint(
    state: AgentState, checkpoint: str, *, reason: str, payload: dict[str, Any] | None = None
) -> None:
    state["current_checkpoint"] = checkpoint
    state["checkpoint_history"] = [
        *(state.get("checkpoint_history") or []),
        {
            "checkpoint": checkpoint,
            "phase": state.get("current_phase"),
            "reason": reason,
            **({"payload": payload} if payload else {}),
        },
    ]
    _append_event_trace(
        state,
        "agent.checkpoint.recorded",
        payload={
            "checkpoint": checkpoint,
            "phase": state.get("current_phase"),
            "reason": reason,
            **(payload or {}),
        },
    )


def _capability_manifest_for_tool(tool_name: str):
    indexes = load_builtin_registry_indexes()
    capability_id = indexes.capability_id_by_tool_name.get(tool_name)
    if capability_id is None:
        return None
    return indexes.capability_by_id.get(capability_id)


def _build_verifier_hints(state: AgentState, tool_name: str, result: Any) -> dict[str, Any]:
    capability = _capability_manifest_for_tool(tool_name)
    permission_class = getattr(capability, "permission_class", None)
    side_effect_scope = getattr(capability, "side_effect_scope", None)
    return {
        "tool_name": tool_name,
        "permission_class": getattr(permission_class, "value", None),
        "side_effect_scope": getattr(side_effect_scope, "value", None),
        "requires_confirmation": bool(getattr(capability, "requires_confirmation", False)),
        "supports_retry": bool(getattr(capability, "supports_retry", False)),
        "safe_for_parallel_use": bool(getattr(capability, "safe_for_parallel_use", False)),
        "result_preview": _stringify_message_content(result)[:200],
    }


def _update_task_result_summary(state: AgentState, tool_summaries: list[dict[str, Any]]) -> None:
    if not tool_summaries:
        return

    previous_summary = state.get("task_result_summary") or {}
    previous_tools = previous_summary.get("tools") or []
    merged_tools = [*previous_tools, *tool_summaries]
    summary = {
        "tool_count": len(merged_tools),
        "tools": merged_tools,
        "created_count": sum(int(item.get("created_count") or 0) for item in merged_tools),
        "created_entity_types": [
            entity_type
            for item in merged_tools
            for entity_type in item.get("created_entity_types") or []
            if entity_type
        ],
        "stop_reason": state.get("stop_reason"),
    }
    state["task_result_summary"] = summary
    state["action_results"] = [*(state.get("action_results") or []), summary]


def _record_sub_commander(
    state: AgentState, role: AgentRole, sub_commander: str, user_query: str
) -> None:
    thread_id = _ensure_message_thread(state)
    message_id, message_index = _bump_message_sequence(state)
    current_agent_id = str(state.get("agent_id") or AgentRole.MASTER.value)
    state["current_agent"] = role.value
    state["current_sub_commander"] = sub_commander
    state["active_agents"] = _normalize_active_agents(state.get("active_agents"))
    if role not in state["active_agents"]:
        state["active_agents"] = [*state["active_agents"], role]
    state["active_sub_commanders"] = [*(state.get("active_sub_commanders") or []), sub_commander]
    state["agent_trace"] = [*(state.get("agent_trace") or []), role.value]
    trace_entry = {
        "agent": _role_value(role),
        "agent_id": current_agent_id,
        "sub_commander": sub_commander,
        "query": user_query,
        "thread_id": thread_id,
        "message_id": message_id,
        "message_index": message_index,
    }
    state["sub_commander_trace"] = [*(state.get("sub_commander_trace") or []), trace_entry]
    state["retrieval_trace"] = [*(state.get("retrieval_trace") or []), trace_entry]
    _append_message_trace(
        state,
        from_agent_id=str(state.get("parent_agent_id") or AgentRole.MASTER.value),
        to_agent_id=current_agent_id,
        message_type="task_request",
        content_summary=user_query,
        task_id=((state.get("turn_context") or {}).get("collaboration_task") or {}).get("task_id"),
        payload={"sub_commander": sub_commander},
        message_id=message_id,
        message_index=message_index,
    )
    _append_event_trace(
        state,
        "agent.message.received",
        payload={"sub_commander": sub_commander, "summary": user_query[:200]},
        task_id=((state.get("turn_context") or {}).get("collaboration_task") or {}).get("task_id"),
        message_id=message_id,
    )


def _stop_sub_commander_due_to_budget(state: AgentState, reason: str) -> None:
    state["stop_reason"] = reason
    state["final_response"] = (
        "这次需要处理的步骤有点多，我先停在这里。您可以把目标再明确一点，或让我先只完成其中一步。"
    )


def _guard_sub_commander_budget(
    state: AgentState, counter_key: str, max_key: str, reason: str
) -> bool:
    max_value = _get_state_int(state, max_key)
    current_value = _get_state_int(state, counter_key)
    if max_value > 0 and current_value >= max_value:
        _stop_sub_commander_due_to_budget(state, reason)
        return False
    return True


def _classify_created_entity(tool_name: str) -> dict[str, str] | None:
    mapping = {
        "create_reminder": "reminder",
        "create_goal": "goal",
        "create_todo": "todo",
        "create_schedule_task": "task",
        "create_task": "task",
        "create_forum_post": "forum_post",
    }
    entity_type = mapping.get(tool_name)
    if not entity_type:
        return None
    return {"type": entity_type, "tool": tool_name}


def _build_reminder_time_clarification(args: dict[str, Any]) -> dict[str, Any] | None:
    if not args.get("date") or args.get("reminder_at"):
        return None
    title = str(args.get("title") or args.get("content") or "这件事").strip() or "这件事"
    return {
        "question": f"要把“{title}”提醒在几点？如果您不想特地指定，我也可以默认按当天早上 9 点给您设置。",
        "missing_fields": ["reminder_at"],
        "partial_args": args,
    }


def _has_active_clarification_context(state: AgentState) -> bool:
    clarification_context = state.get("clarification_context") or {}
    if not clarification_context:
        return False
    if not clarification_context.get("question"):
        return False
    owning_agent = clarification_context.get("owning_agent")
    return isinstance(owning_agent, str) and owning_agent in _role_values()


def _clear_clarification_context(state: AgentState) -> None:
    state["clarification_context"] = None


def _write_clarification_context(
    state: AgentState,
    *,
    role: AgentRole,
    sub_commander: str,
    tool_name: str,
    question: str,
    partial_args: dict[str, Any] | None = None,
    missing_fields: list[str] | None = None,
) -> None:
    state["clarification_context"] = {
        "owning_agent": role.value,
        "owning_sub_commander": sub_commander,
        "target_action": tool_name,
        "question": question,
        "partial_args": dict(partial_args or {}),
        "missing_fields": list(missing_fields or []),
        "status": "pending",
    }
    pending_action = state.get("pending_action") or {}
    if pending_action.get("status") == "pending":
        state["continuity_state"] = {"status": "fresh", "mode": "resume_after_clarification"}


def _route_from_clarification_context(state: AgentState, user_query: str) -> AgentRole | None:
    if not _has_active_clarification_context(state):
        return None
    if _route_agent_from_user_query(user_query) != AgentRole.MASTER:
        return None
    owning_agent = str((state.get("clarification_context") or {}).get("owning_agent") or "")
    try:
        return AgentRole(owning_agent)
    except ValueError:
        return None


def _build_clarification_summary(state: AgentState) -> str | None:
    clarification_context = state.get("clarification_context") or {}
    if not _has_active_clarification_context(state):
        return None
    lines = [
        "clarification_context:",
        f"- owning_agent: {clarification_context.get('owning_agent')}",
        f"- owning_sub_commander: {clarification_context.get('owning_sub_commander')}",
        f"- target_action: {clarification_context.get('target_action')}",
        f"- question: {clarification_context.get('question')}",
    ]
    missing_fields = clarification_context.get("missing_fields") or []
    partial_args = clarification_context.get("partial_args") or {}
    if missing_fields:
        lines.append(f"- missing_fields: {', '.join(str(field) for field in missing_fields)}")
    if partial_args:
        lines.append(f"- partial_args: {json.dumps(partial_args, ensure_ascii=False)}")
    lines.append(
        "- instruction: merge the user's latest answer into the missing fields and continue the same action"
    )
    return "\n".join(lines)


def _build_resumed_clarification_query(state: AgentState, user_query: str) -> str:
    clarification_context = state.get("clarification_context") or {}
    partial_args = clarification_context.get("partial_args") or {}
    target_action = clarification_context.get("target_action") or ""
    if not partial_args or target_action != "create_reminder":
        return user_query

    parts: list[str] = []
    title = partial_args.get("title") or partial_args.get("content")
    if title:
        parts.append(f"title={title}")
    date = partial_args.get("date")
    if date:
        parts.append(f"date={date}")
    parts.append(f"reminder_at={user_query}")
    return f"继续完成提醒创建，请合并参数：{'；'.join(parts)}"


def _prepare_tool_calls_for_execution(
    tool_calls: list[dict[str, Any]],
    state: AgentState,
) -> tuple[list[dict[str, Any]], dict[str, Any] | None]:
    prepared_calls: list[dict[str, Any]] = []
    for call in tool_calls:
        tool_name, args = _canonicalize_tool_call(call["name"], dict(call.get("args") or {}))
        normalized_args = normalize_tool_time_arguments(
            tool_name,
            args,
            state.get("current_datetime_context"),
        )
        clarification = None
        if tool_name == "create_reminder":
            clarification = _build_reminder_time_clarification(normalized_args)
        if clarification:
            return [], {
                "tool_name": tool_name,
                "question": clarification["question"],
                "partial_args": clarification.get("partial_args") or normalized_args,
                "missing_fields": clarification.get("missing_fields") or [],
            }
        prepared_calls.append(
            {
                "id": call.get("id"),
                "name": tool_name,
                "args": normalized_args,
                "reason": call.get("reason"),
            }
        )
    return prepared_calls, None


def _canonicalize_tool_call(tool_name: str, args: dict[str, Any]) -> tuple[str, dict[str, Any]]:
    normalized_name = tool_name
    normalized_args = dict(args)

    if normalized_name == "create_reminder":
        if normalized_args.get("description") and not (
            normalized_args.get("title") or normalized_args.get("content")
        ):
            normalized_args["content"] = normalized_args["description"]
        if normalized_args.get("reminder_content") and not (
            normalized_args.get("title") or normalized_args.get("content")
        ):
            normalized_args["content"] = normalized_args["reminder_content"]
        if normalized_args.get("reminder_time") and not (
            normalized_args.get("reminder_at")
            or normalized_args.get("datetime")
            or normalized_args.get("at")
            or normalized_args.get("remind_at")
            or normalized_args.get("time")
        ):
            normalized_args["time"] = normalized_args["reminder_time"]

    if normalized_name in {"create_schedule_task", "create_task"}:
        if normalized_args.get("task") and not (
            normalized_args.get("title") or normalized_args.get("content")
        ):
            normalized_args["title"] = normalized_args["task"]
        if normalized_args.get("due_datetime") and not (
            normalized_args.get("due_date") or normalized_args.get("date")
        ):
            normalized_args["due_date"] = normalized_args["due_datetime"]
        if normalized_args.get("due_time") and not normalized_args.get("due_date"):
            normalized_args["due_date"] = normalized_args["due_time"]

    if normalized_name == "create_todo":
        if normalized_args.get("task") and not (
            normalized_args.get("title") or normalized_args.get("content")
        ):
            normalized_args["title"] = normalized_args["task"]
        if normalized_args.get("date") and not normalized_args.get("todo_date"):
            normalized_args["todo_date"] = normalized_args["date"]
        if normalized_args.get("due_date") and not normalized_args.get("todo_date"):
            normalized_args["todo_date"] = normalized_args["due_date"]
        if normalized_args.get("due_datetime") and not normalized_args.get("todo_date"):
            normalized_args["todo_date"] = normalized_args["due_datetime"]
        if any(
            normalized_args.get(key)
            for key in ("due_datetime", "start_time", "end_time", "due_time")
        ):
            normalized_name = "create_schedule_task"
            if normalized_args.get("due_time") and not normalized_args.get("due_date"):
                normalized_args["due_date"] = normalized_args["due_time"]
            elif normalized_args.get("todo_date") and not normalized_args.get("due_date"):
                normalized_args["due_date"] = normalized_args["todo_date"]

    if normalized_name == "create_goal":
        if normalized_args.get("task") and not (
            normalized_args.get("title") or normalized_args.get("content")
        ):
            normalized_args["title"] = normalized_args["task"]
        if normalized_args.get("date") and not normalized_args.get("goal_date"):
            normalized_args["goal_date"] = normalized_args["date"]
        if normalized_args.get("target_date") and not normalized_args.get("goal_date"):
            normalized_args["goal_date"] = normalized_args["target_date"]

    return normalized_name, normalized_args


async def _invoke_llm(llm, messages: list[BaseMessage], tools: list[Any] | None = None):
    messages = _coalesce_system_messages(messages)
    if tools:
        llm = llm.bind_tools(tools)
    return await llm.ainvoke(messages)


async def _execute_tool_calls(
    tool_calls: list[dict[str, Any]],
    toolset: list[Any],
    state: AgentState,
) -> tuple[list[dict[str, Any]], str, list[dict[str, str]], list[ToolMessage]]:
    tool_map = {tool.name: tool for tool in toolset}
    normalized_calls: list[dict[str, Any]] = []
    result_lines: list[str] = []
    created_entities: list[dict[str, str]] = []
    tool_messages: list[ToolMessage] = []
    verifier_hints_by_tool: list[dict[str, Any]] = []
    tool_summaries: list[dict[str, Any]] = []

    for call in tool_calls:
        tool_name = call["name"]
        normalized_args = dict(call.get("args") or {})
        tool = tool_map.get(tool_name)
        if tool is None:
            raise ValueError(f"Tool not found: {tool_name}")

        _append_event_trace(
            state,
            "agent.tool.start",
            payload={"tool_name": tool_name, "args": normalized_args},
            task_id=str(call.get("id") or "") or None,
        )

        try:
            if hasattr(tool, "ainvoke"):
                result = await tool.ainvoke(normalized_args)
            else:
                result = await asyncio.to_thread(tool.invoke, normalized_args)
        except Exception as exc:
            logger.exception("Tool execution failed: %s args=%s", tool_name, normalized_args)
            result = f"工具执行失败: {exc}"
            _append_event_trace(
                state,
                "agent.error",
                payload={"tool_name": tool_name, "args": normalized_args, "error": str(exc)},
                severity="error",
                task_id=str(call.get("id") or "") or None,
            )

        normalized_call = {
            "id": call.get("id"),
            "name": tool_name,
            "args": normalized_args,
            "reason": call.get("reason"),
        }
        normalized_calls.append(normalized_call)
        result_lines.append(f"[{tool_name}] {result}")
        verifier_hints = _build_verifier_hints(state, tool_name, result)
        verifier_hints_by_tool.append(verifier_hints)
        tool_outcome = {
            "tool_name": tool_name,
            "args": normalized_args,
            "result_preview": _stringify_message_content(result)[:200],
            "verifier_hints": verifier_hints,
            "isolation": {
                "mode": state.get("isolation_mode"),
                "workspace_path": state.get("isolation_workspace_path"),
            },
        }
        state["tool_outcomes"] = [*(state.get("tool_outcomes") or []), tool_outcome]
        _append_event_trace(
            state,
            "agent.tool.result",
            payload={
                "tool_name": tool_name,
                "args": normalized_args,
                "result_preview": _stringify_message_content(result)[:200],
                "verification": verifier_hints,
            },
            severity="error" if _tool_result_indicates_failure(result) else "info",
            task_id=str(call.get("id") or "") or None,
        )
        tool_messages.append(
            ToolMessage(
                content=_stringify_message_content(result),
                tool_call_id=str(call.get("id") or tool_name),
                name=tool_name,
            )
        )
        entity = _classify_created_entity(tool_name)
        call_created_entities: list[dict[str, str]] = []
        if entity and not _tool_result_indicates_failure(result):
            created_entities.append(entity)
            call_created_entities.append(entity)
        tool_summaries.append(
            {
                "tool_name": tool_name,
                "result_preview": _stringify_message_content(result)[:200],
                "created_entity_types": [
                    entity.get("type") for entity in call_created_entities if entity.get("type")
                ],
                "created_count": len(call_created_entities),
            }
        )

    state["verifier_hints"] = {"tools": verifier_hints_by_tool}
    _update_task_result_summary(state, tool_summaries)
    return normalized_calls, "\n".join(result_lines), created_entities, tool_messages


async def _run_sub_commander(
    state: AgentState,
    role: AgentRole,
    manager_prompt: str,
    user_query: str,
    *,
    use_tools: bool,
    summary_target: str | None = None,
):
    state["clarification_needed"] = False
    state["clarification_question"] = None
    state["stop_reason"] = None

    llm = _get_llm_for_state(state)
    capabilities = _resolve_capabilities(state, llm)
    if _has_active_clarification_context(state) and role.value == str(
        (state.get("clarification_context") or {}).get("owning_agent") or ""
    ):
        user_query = _build_resumed_clarification_query(state, user_query)
    sub_commander = _choose_sub_commander(role, user_query)
    _record_sub_commander(state, role, sub_commander, user_query)

    toolset = SUB_COMMANDER_TOOLSETS.get(sub_commander, []) if use_tools else []
    _prepare_isolation_context(
        state,
        role=role,
        sub_commander=sub_commander,
        user_query=user_query,
        toolset=toolset,
    )
    if (
        role == AgentRole.EXECUTOR
        and _is_short_confirmation(user_query)
        and _previous_turn_completed_reminder_creation(state)
    ):
        state["tool_calls"] = []
        state["last_tool_result"] = None
        state["final_response"] = (
            "上一条提醒已经创建好了。若您现在要新建别的内容，请直接告诉我要创建什么。"
        )
        history_messages = list(state.get("messages", []))
        history_messages.append(AIMessage(content=state["final_response"]))
        state["messages"] = history_messages
        state["should_respond"] = True
        return state
    base_messages = _build_system_messages(state, manager_prompt, role, sub_commander)
    conversation_history = _conversation_history_messages(state)
    if conversation_history and getattr(conversation_history[-1], "type", "") in {"human", "user"}:
        conversation_history = conversation_history[:-1]
    user_message = HumanMessage(content=f"用户请求: {user_query}")
    working_messages = [*base_messages, *conversation_history, user_message]

    state["tool_calls"] = []
    state["last_tool_result"] = None
    state["tool_strategy_used"] = None
    state["fallback_parse_error"] = None
    start_tool_index = len(state.get("tool_outcomes") or [])

    if not _guard_sub_commander_budget(
        state, "tool_round_count", "max_tool_rounds", "max_tool_rounds_exceeded"
    ):
        pass
    elif not _guard_sub_commander_budget(
        state, "retry_count", "max_retries", "max_retries_exceeded"
    ):
        pass
    elif not toolset:
        if _guard_sub_commander_budget(
            state, "iteration_count", "max_iterations", "max_iterations_exceeded"
        ):
            state["iteration_count"] = int(state.get("iteration_count") or 0) + 1
            response = await _invoke_llm(llm, working_messages)
            _record_response_usage(state, response)
            state["final_response"] = _stringify_message_content(response.content)
    elif capabilities.supports_native_tools:
        state["tool_strategy_used"] = "native"
        bound_llm = llm.bind_tools(toolset)
        while state.get("final_response") is None and not state.get("clarification_needed"):
            if not _guard_sub_commander_budget(
                state, "iteration_count", "max_iterations", "max_iterations_exceeded"
            ):
                break
            state["iteration_count"] = int(state.get("iteration_count") or 0) + 1
            response = await _invoke_llm(bound_llm, working_messages)
            _record_response_usage(state, response)
            tool_calls = getattr(response, "tool_calls", None) or []
            if tool_calls:
                if not _guard_sub_commander_budget(
                    state, "tool_round_count", "max_tool_rounds", "max_tool_rounds_exceeded"
                ):
                    break
                prepared_calls, clarification = _prepare_tool_calls_for_execution(tool_calls, state)
                if clarification:
                    state["clarification_needed"] = True
                    state["clarification_question"] = clarification["question"]
                    _write_clarification_context(
                        state,
                        role=role,
                        sub_commander=sub_commander,
                        tool_name=clarification["tool_name"],
                        question=clarification["question"],
                        partial_args=clarification.get("partial_args"),
                        missing_fields=clarification.get("missing_fields"),
                    )
                    state["stop_reason"] = "clarification_needed"
                    state["final_response"] = clarification["question"]
                    break
                state["tool_round_count"] = int(state.get("tool_round_count") or 0) + 1
                assistant_tool_message = AIMessage(
                    content=_stringify_message_content(getattr(response, "content", "")),
                    tool_calls=tool_calls,
                )
                (
                    normalized_calls,
                    tool_result,
                    created_entities,
                    tool_messages,
                ) = await _execute_tool_calls(
                    prepared_calls,
                    toolset,
                    state,
                )
                state["tool_calls"] = normalized_calls
                state["last_tool_result"] = tool_result
                state["created_entities"] = [
                    *(state.get("created_entities") or []),
                    *created_entities,
                ]
                if created_entities:
                    _clear_clarification_context(state)
                if (
                    role == AgentRole.SCHEDULE_PLANNER
                    and _should_clear_schedule_creation_continuity(state, created_entities)
                ):
                    _clear_structured_continuity(state)
                working_messages = [*working_messages, assistant_tool_message, *tool_messages]
                if sub_commander == "librarian_retrieval" and _is_missing_knowledge_result(
                    tool_result
                ):
                    working_messages.append(
                        SystemMessage(
                            content="如果检索工具没有找到证据，可以直接基于你的常识给出清晰回答，不要机械地说不知道。"
                        )
                    )
                continue
            state["final_response"] = _stringify_message_content(response.content)
    else:
        state["tool_strategy_used"] = "json_fallback"
        allowed_tools = [tool.name for tool in toolset]
        while state.get("final_response") is None and not state.get("clarification_needed"):
            if not _guard_sub_commander_budget(
                state, "iteration_count", "max_iterations", "max_iterations_exceeded"
            ):
                break
            state["iteration_count"] = int(state.get("iteration_count") or 0) + 1
            parsed = None
            retry_instruction: BaseMessage | None = None
            while parsed is None:
                response = await _invoke_llm(
                    llm,
                    [
                        *working_messages,
                        SystemMessage(content=JSON_ACTION_FALLBACK_PROMPT),
                        SystemMessage(content=f"本次可用工具列表: {', '.join(allowed_tools)}"),
                        *([retry_instruction] if retry_instruction else []),
                    ],
                )
                _record_response_usage(state, response)
                response_text = _stringify_message_content(response.content)
                response_text_stripped = response_text.strip()
                parsed = _parse_json_action(response_text, allowed_tools)
                if parsed is None and response_text_stripped and _looks_like_internal_tool_markup(
                    response_text_stripped
                ):
                    if int(state.get("retry_count") or 0) >= int(state.get("max_retries") or 0):
                        state["fallback_parse_error"] = "internal_tool_markup"
                        state["final_response"] = _build_internal_markup_fallback_response(
                            state,
                            sub_commander=sub_commander,
                        ) or (
                            "这次内部调度没有正确收束成最终答复。我先不展示内部调用过程；您重试一次，我会直接用自然语言回答。"
                        )
                        break
                    if not _guard_sub_commander_budget(
                        state, "iteration_count", "max_iterations", "max_iterations_exceeded"
                    ):
                        parsed = None
                        break
                    state["iteration_count"] = int(state.get("iteration_count") or 0) + 1
                    state["retry_count"] = int(state.get("retry_count") or 0) + 1
                    retry_instruction = SystemMessage(
                        content=(
                            "上一轮输出了内部调度或工具调用标记，这是协议错误。"
                            "不要再输出分发说明、XML 标签、<invoke>、<parameter>、JSON 或 tool_call。"
                            "请直接面向用户给出最终自然语言答复；如果已有工具结果，就基于结果整理；"
                            "如果工具没有找到证据，可以基于常识直接回答。"
                        )
                    )
                    continue
                if parsed is None and response_text.strip() and state.get("tool_round_count"):
                    state["fallback_parse_error"] = None
                    state["final_response"] = response_text.strip()
                    break
                if parsed is not None:
                    state["fallback_parse_error"] = None
                    break
                if not _guard_sub_commander_budget(
                    state, "iteration_count", "max_iterations", "max_iterations_exceeded"
                ):
                    parsed = None
                    break
                if int(state.get("retry_count") or 0) >= int(state.get("max_retries") or 0):
                    state["fallback_parse_error"] = "invalid_json_action"
                    state["final_response"] = (
                        "这次内部动作解析没整理好，不过您的意思我接住了。您再说一遍要我执行的内容，我只回结果，不展示内部调用细节。"
                    )
                    break
                state["iteration_count"] = int(state.get("iteration_count") or 0) + 1
                state["retry_count"] = int(state.get("retry_count") or 0) + 1
                retry_instruction = SystemMessage(
                    content="上一次输出不是有效 JSON。请严格只返回合法 JSON，不要加解释。"
                )
            if state.get("final_response") is not None:
                break
            if parsed is None:
                break
            if parsed["mode"] == "final":
                state["final_response"] = parsed["final_response"]
                break
            if parsed["mode"] == "clarification":
                state["clarification_needed"] = True
                state["clarification_question"] = parsed["clarification_question"]
                _write_clarification_context(
                    state,
                    role=role,
                    sub_commander=sub_commander,
                    tool_name="clarification",
                    question=parsed["clarification_question"],
                )
                state["stop_reason"] = "clarification_needed"
                state["final_response"] = parsed["clarification_question"]
                break
            if not _guard_sub_commander_budget(
                state, "tool_round_count", "max_tool_rounds", "max_tool_rounds_exceeded"
            ):
                break
            prepared_calls, clarification = _prepare_tool_calls_for_execution(
                parsed["tool_calls"], state
            )
            if clarification:
                state["clarification_needed"] = True
                state["clarification_question"] = clarification["question"]
                _write_clarification_context(
                    state,
                    role=role,
                    sub_commander=sub_commander,
                    tool_name=clarification["tool_name"],
                    question=clarification["question"],
                    partial_args=clarification.get("partial_args"),
                    missing_fields=clarification.get("missing_fields"),
                )
                state["stop_reason"] = "clarification_needed"
                state["final_response"] = clarification["question"]
                break
            state["tool_round_count"] = int(state.get("tool_round_count") or 0) + 1
            (
                normalized_calls,
                tool_result,
                created_entities,
                tool_messages,
            ) = await _execute_tool_calls(
                prepared_calls,
                toolset,
                state,
            )
            state["tool_calls"] = normalized_calls
            state["last_tool_result"] = tool_result
            state["created_entities"] = [*(state.get("created_entities") or []), *created_entities]
            if role == AgentRole.SCHEDULE_PLANNER and _should_clear_schedule_creation_continuity(
                state, created_entities
            ):
                _clear_structured_continuity(state)
            working_messages = [*working_messages, *tool_messages]
            if sub_commander == "librarian_retrieval" and _is_missing_knowledge_result(tool_result):
                working_messages.append(
                    SystemMessage(
                        content="如果检索工具没有找到证据，可以直接基于你的常识给出清晰回答，不要机械地说不知道。"
                    )
                )

    if summary_target:
        state[_summary_state_key(summary_target)] = state.get("final_response")

    task_result_summary = state.get("task_result_summary")
    tool_outcomes = list(state.get("tool_outcomes") or [])[start_tool_index:]
    has_tool_failure = any(
        _tool_result_indicates_failure(outcome.get("result_preview")) for outcome in tool_outcomes
    )
    verifier_input = {
        "summary": state.get("final_response") or (task_result_summary or {}).get("tools"),
        "evidence": tool_outcomes,
        "success": bool(tool_outcomes or state.get("final_response")) and not has_tool_failure,
    }
    _append_event_trace(
        state,
        "agent.verify.started",
        payload={
            "summary_present": bool(verifier_input["summary"]),
            "evidence_count": len(verifier_input["evidence"]),
        },
    )
    verdict = verify_task_result(
        summary=state.get("final_response"),
        evidence=tool_outcomes,
        result=verifier_input,
    )
    updated_state = apply_verification_verdict(state, verdict)
    state.update(updated_state)
    _append_event_trace(
        state,
        "agent.verify.completed",
        payload={
            "status": verdict.status,
            "summary": verdict.summary,
            "evidence_count": len(verdict.evidence),
        },
        severity="error" if verdict.status == "failed" else "info",
    )

    final_response_text = state.get("final_response")
    if not state.get("clarification_needed") and final_response_text:
        _clear_clarification_context(state)
    if (
        role == AgentRole.SCHEDULE_PLANNER
        and isinstance(final_response_text, str)
        and _is_schedule_creation_confirmation_response(final_response_text)
    ):
        _write_schedule_creation_continuity(state, user_query)

    history_messages = list(state.get("messages", []))
    final_response = state.get("final_response")
    if isinstance(final_response, str):
        history_messages.append(AIMessage(content=final_response))

    state["messages"] = history_messages
    state["should_respond"] = True
    return state


def _derive_task_status(state: AgentState) -> Literal["completed", "failed", "blocked"]:
    if state.get("clarification_needed") or state.get("stop_reason") == "clarification_needed":
        return "blocked"
    if state.get("verification_status") == "failed":
        return "failed"
    if state.get("stop_reason") and state.get("stop_reason") != "clarification_needed":
        return "blocked"
    if state.get("final_response"):
        return "completed"
    return "failed"


def _build_task_evidence(state: AgentState, start_index: int) -> list[dict[str, Any]]:
    tool_outcomes = [dict(item) for item in list(state.get("tool_outcomes") or [])[start_index:]]
    if tool_outcomes:
        evidence = tool_outcomes
    else:
        evidence = []

    if state.get("isolation_mode") and state.get("isolation_mode") != "none":
        evidence.append(
            {
                "type": "isolation",
                "mode": state.get("isolation_mode"),
                "workspace_path": state.get("isolation_workspace_path"),
                "metadata": dict(state.get("isolation_metadata") or {}),
            }
        )

    if state.get("input_tokens") or state.get("output_tokens"):
        evidence.append(
            {
                "type": "cost",
                "input_tokens": int(state.get("input_tokens") or 0),
                "output_tokens": int(state.get("output_tokens") or 0),
                "estimated_cost": state.get("estimated_cost"),
                "budget_warning": bool(state.get("budget_warning") or False),
            }
        )

    if state.get("verification_status") or state.get("verification_summary"):
        evidence.append(
            {
                "type": "verification",
                "status": state.get("verification_status"),
                "summary": state.get("verification_summary"),
            }
        )
    if not evidence and state.get("final_response"):
        evidence.append(
            {
                "type": "response_summary",
                "content": _stringify_message_content(state.get("final_response"))[:400],
            }
        )
    return evidence


def _collect_task_result(task: AgentTask, state: AgentState, start_tool_index: int) -> TaskResult:
    status = _derive_task_status(state)
    next_action = state.get("clarification_question")
    if not next_action and status != "completed" and state.get("stop_reason"):
        next_action = f"resolve_{state['stop_reason']}"
    return TaskResult(
        task_id=task.task_id,
        status=status,
        summary=_stringify_message_content(
            state.get("final_response") or state.get("verification_summary")
        ),
        evidence=_build_task_evidence(state, start_tool_index),
        owner_agent_id=task.owner_agent_id,
        parent_task_id=task.parent_task_id,
        child_task_ids=list(task.child_task_ids),
        thread_id=str(state.get("thread_id") or "") or None,
        message_id=str(state.get("last_message_id") or "") or None,
        message_index=int(state.get("message_sequence") or 0) or None,
        interrupt_records=list(state.get("interrupted_tasks") or []),
        recovery_records=list(state.get("recovery_trace") or []),
        budget_snapshot=state.get("budget_state"),
        next_action=next_action,
        output_data={
            "role": task.role,
            "sub_commander": state.get("current_sub_commander"),
            "verification_status": state.get("verification_status"),
            "isolation_mode": state.get("isolation_mode"),
            "isolation_workspace_path": state.get("isolation_workspace_path"),
            "estimated_cost": state.get("estimated_cost"),
            "budget_warning": bool(state.get("budget_warning") or False),
        },
    )


def _apply_task_result_to_state(
    state: AgentState, task: AgentTask, task_result: TaskResult
) -> None:
    normalized_result = normalize_task_result(task_result, default_task_id=task.task_id)
    serialized_result = normalized_result.model_dump(mode="json")
    state["task_results"] = [*(state.get("task_results") or []), serialized_result]

    updated_tasks: list[dict[str, Any]] = []
    completed_entry: dict[str, Any] | None = None
    pending_tasks: list[dict[str, Any]] = []
    for existing_task in state.get("active_tasks") or []:
        normalized_task = (
            existing_task.model_dump(mode="json")
            if isinstance(existing_task, AgentTask)
            else dict(existing_task)
        )
        if normalized_task.get("task_id") == task.task_id:
            normalized_task["status"] = normalized_result.status
            normalized_task["evidence"] = list(normalized_result.evidence)
            normalized_task["result_summary"] = normalized_result.summary
            if normalized_result.next_action:
                normalized_task["next_action"] = normalized_result.next_action
            completed_entry = {
                "task_id": task.task_id,
                "role": task.role,
                "owner_agent_id": task.owner_agent_id,
                "status": normalized_result.status,
                "summary": normalized_result.summary,
                "next_action": normalized_result.next_action,
            }
        updated_tasks.append(normalized_task)
        if normalized_task.get("status") != "completed":
            pending_tasks.append(normalized_task)
    state["active_tasks"] = updated_tasks
    state["pending_tasks"] = pending_tasks

    if completed_entry is not None:
        state["completed_tasks"] = [*(state.get("completed_tasks") or []), completed_entry]


def _build_collaboration_final_response(task_results: list[TaskResult | dict[str, Any]]) -> str:
    normalized_results = [normalize_task_result(item) for item in task_results]
    lines = [f"已按协作模式回收 {len(normalized_results)} 个子任务结果："]
    final_completed_summary: str | None = None
    for index, result in enumerate(normalized_results, start=1):
        owner = result.owner_agent_id or "unknown"
        summary = result.summary or "未返回摘要。"
        lines.append(f"{index}. [{owner}] ({result.status}) {summary}")
        if result.status == "completed" and result.summary:
            final_completed_summary = result.summary
    if final_completed_summary:
        lines.append(f"汇总结论：{final_completed_summary}")
    elif normalized_results:
        blocked_result = next(
            (item for item in reversed(normalized_results) if item.next_action), None
        )
        if blocked_result and blocked_result.next_action:
            lines.append(f"下一步：{blocked_result.next_action}")
        else:
            lines.append("汇总结论：协作任务已执行，但最终摘要为空。")
    return "\n".join(lines)


def _build_serial_fallback_response(
    user_query: str,
    task_results: list[TaskResult | dict[str, Any]],
    merge_report: MergeReport | dict[str, Any] | None,
) -> str:
    normalized_results = [normalize_task_result(item) for item in task_results]
    completed = [item for item in normalized_results if item.status == "completed"]
    merge_payload = (
        merge_report.model_dump(mode="json")
        if isinstance(merge_report, MergeReport)
        else dict(merge_report or {})
    )
    lines = [
        "并行/协作结果出现冲突或失败，我已切回保守收敛路径。",
        f"原始请求：{user_query}",
    ]
    if completed:
        lines.append("当前仍可确认的结果：")
        for item in completed[:3]:
            lines.append(f"- [{item.owner_agent_id or 'unknown'}] {item.summary or '已完成'}")
    if merge_payload.get("conflict_flags"):
        lines.append("冲突/回退原因：")
        for flag in list(merge_payload.get("conflict_flags") or [])[:3]:
            lines.append(f"- {flag}")
    if not completed:
        lines.append("目前没有足够稳定的子任务结果，建议改走 direct 或更小范围的 collaboration。")
    return "\n".join(lines)


def _verify_collaboration_results(
    state: AgentState,
    tasks: list[AgentTask],
    task_results: list[TaskResult | dict[str, Any]] | None = None,
) -> None:
    expected_task_ids = {task.task_id for task in tasks}
    normalized_results = [
        normalize_task_result(item)
        for item in (
            task_results if task_results is not None else (state.get("task_results") or [])
        )
        if normalize_task_result(item).task_id in expected_task_ids
    ]
    result_by_task_id = {item.task_id: item for item in normalized_results}
    missing_task_ids = [task.task_id for task in tasks if task.task_id not in result_by_task_id]
    failed_or_blocked = [
        item.task_id for item in normalized_results if item.status in {"failed", "blocked"}
    ]
    missing_evidence = [item.task_id for item in normalized_results if not item.evidence]

    verification_evidence = [
        {
            "task_id": item.task_id,
            "owner_agent_id": item.owner_agent_id,
            "status": item.status,
            "evidence_count": len(item.evidence or []),
        }
        for item in normalized_results
    ]
    merge_report = merge_task_results([item.model_dump(mode="json") for item in normalized_results])
    state["merge_report"] = merge_report.model_dump(mode="json")
    _append_event_trace(
        state,
        "agent.merge.completed",
        payload=state["merge_report"],
    )

    if missing_task_ids:
        summary = f"协作结果不完整，缺少任务结果: {', '.join(missing_task_ids)}"
        verdict = verify_task_result(
            status="failed", summary=summary, evidence=verification_evidence
        )
    elif failed_or_blocked:
        summary = f"协作结果未闭环，存在失败或阻塞任务: {', '.join(failed_or_blocked)}"
        verdict = verify_task_result(
            status="failed", summary=summary, evidence=verification_evidence
        )
    elif missing_evidence:
        summary = f"协作结果证据不足，缺少 evidence 的任务: {', '.join(missing_evidence)}"
        verdict = verify_task_result(
            status="failed", summary=summary, evidence=verification_evidence
        )
    elif merge_report.status == "conflicted":
        verdict = verify_task_result(
            status="failed",
            summary=merge_report.summary,
            evidence=[
                *verification_evidence,
                {"type": "merge_conflict", "conflict_flags": merge_report.conflict_flags},
            ],
        )
    elif merge_report.status == "fallback":
        verdict = verify_task_result(
            status="failed",
            summary=merge_report.summary,
            evidence=[
                *verification_evidence,
                {"type": "merge_fallback", "conflict_flags": merge_report.conflict_flags},
            ],
        )
    else:
        summary = (
            merge_report.summary
            or f"协作模式已完成 {len(normalized_results)}/{len(tasks)} 个子任务，并为每个子任务回收了结果与 evidence。"
        )
        verdict = verify_task_result(
            status="passed", summary=summary, evidence=verification_evidence
        )

    updated_state = apply_verification_verdict(state, verdict)
    state.update(updated_state)
    state["verification_report"] = VerificationReport(
        status=state.get("verification_status") or "skipped",
        summary=state.get("verification_summary"),
        evidence=list(state.get("verification_evidence") or []),
    ).model_dump(mode="json")
    _append_event_trace(
        state,
        "agent.verify.completed",
        payload=state["verification_report"],
    )


async def _run_collaboration_flow(state: AgentState, user_query: str) -> AgentState:
    _set_phase(state, "phase_2_controlled_collaboration", reason="collaboration_flow_started")
    _record_checkpoint(state, "collaboration.tasks_planning", reason="collaboration_flow_started")
    tasks, scheduled_subtasks = _build_collaboration_plan_from_task_graph(state, user_query)
    if len(tasks) < 2:
        state["execution_mode"] = "direct"
        state["routing_decision"] = {"mode": "direct", "reason": "collaboration_plan_fell_back"}
        _record_checkpoint(
            state,
            "collaboration.fallback_to_direct",
            reason="insufficient_tasks",
            payload={"task_count": len(tasks)},
        )
        _set_phase(
            state,
            "phase_1_routing",
            reason="collaboration_flow_abandoned",
            payload={"task_count": len(tasks)},
        )
        _record_checkpoint(
            state,
            "routing.direct_resumed",
            reason="collaboration_flow_abandoned",
            payload={"task_count": len(tasks)},
        )
        return state

    base_history = list(state.get("messages", []))
    root_agent_id = str(
        state.get("root_agent_id") or state.get("agent_id") or AgentRole.MASTER.value
    )
    coordinator_agent_id = str(state.get("agent_id") or AgentRole.MASTER.value)
    state["execution_mode"] = "collaboration"
    state["routing_decision"] = {
        "mode": "collaboration",
        "reason": "multi_role_request",
        "task_count": len(tasks),
        "roles": [task.role for task in tasks],
    }
    budget_snapshot = _budget_snapshot_from_state(
        state,
        mode="collaboration",
        remaining_parallel_tasks=len(tasks),
        metadata={"task_count": len(tasks)},
    )
    _store_budget_snapshot(state, budget_snapshot)
    _append_event_trace(
        state,
        "agent.collaboration.budget.updated",
        payload=budget_snapshot,
    )
    state["scheduled_subtasks"] = scheduled_subtasks
    state["active_tasks"] = [task.model_dump(mode="json") for task in tasks]
    _record_checkpoint(
        state, "collaboration.tasks_ready", reason="tasks_built", payload={"task_count": len(tasks)}
    )
    parent_task_id = (
        next((task.parent_task_id for task in tasks if task.parent_task_id), None) or "root"
    )
    state["task_hierarchy"] = {parent_task_id: [task.task_id for task in tasks]}
    state["task_results"] = []
    state["next_step"] = None

    _set_phase(state, "phase_3_dynamic_collaboration", reason="collaboration_workers_dispatch")
    for task in tasks:
        scheduled_subtask = next(
            (item for item in scheduled_subtasks if item.get("subtask_id") == task.task_id),
            None,
        )
        if scheduled_subtask is not None:
            _append_event_trace(
                state,
                "agent.subtask.started",
                payload={
                    "subtask_id": scheduled_subtask.get("subtask_id"),
                    "role": scheduled_subtask.get("role"),
                    "dependencies": scheduled_subtask.get("dependencies") or [],
                },
                task_id=task.task_id,
            )
        _record_checkpoint(
            state,
            "collaboration.task_dispatch",
            reason="dispatch_task",
            payload={"task_id": task.task_id, "role": task.role},
        )
        state["current_agent"] = AgentRole.MASTER.value
        state["agent_id"] = coordinator_agent_id
        state["parent_agent_id"] = None
        state["root_agent_id"] = root_agent_id
        state["collaboration_depth"] = 0
        assigned_role = _assign_agent_for_task_role(task.role)
        child_agent_id = _create_child_agent(state, role=assigned_role, task=task)
        if child_agent_id is None:
            interrupt = _record_interrupt(
                state,
                reason="spawn_blocked",
                task=task,
                payload={"target_role": assigned_role.value},
            )
            _record_recovery(
                state,
                interrupt=interrupt,
                strategy="fallback_to_direct_role_execution",
                task=task,
                payload={"target_role": assigned_role.value},
            )
            child_agent_id = f"blocked-{assigned_role.value}"
        state["messages"] = list(base_history)
        state["current_agent"] = assigned_role.value
        state["agent_id"] = child_agent_id
        state["parent_agent_id"] = coordinator_agent_id
        state["root_agent_id"] = root_agent_id
        state["collaboration_depth"] = 1
        task.thread_id = str(state.get("thread_id") or "") or None
        task.message_id = str(state.get("last_message_id") or "") or None
        task.message_index = int(state.get("message_sequence") or 0) or None
        task.collaboration_budget = state.get("budget_state")
        state["turn_context"] = {"collaboration_task": task.model_dump(mode="json")}
        state["final_response"] = None
        state["verification_status"] = None
        state["verification_summary"] = None
        state["verification_evidence"] = []
        state["clarification_needed"] = False
        state["clarification_question"] = None
        state["stop_reason"] = None
        start_tool_index = len(state.get("tool_outcomes") or [])

        await _run_sub_commander(
            state,
            assigned_role,
            ROLE_SYSTEM_PROMPTS[assigned_role],
            task.goal or user_query,
            use_tools=True,
            summary_target=None,
        )

        task_result = _collect_task_result(task, state, start_tool_index)
        _record_checkpoint(
            state,
            "collaboration.task_result_collected",
            reason="task_finished",
            payload={"task_id": task.task_id, "status": task_result.status},
        )
        _append_message_trace(
            state,
            from_agent_id=child_agent_id,
            to_agent_id=coordinator_agent_id,
            message_type="task_update",
            content_summary=task_result.summary or task.title,
            task_id=task.task_id,
            reply_to_message_id=task.message_id,
            payload={"status": task_result.status, "owner_agent_id": task_result.owner_agent_id},
        )
        _append_event_trace(
            state,
            "agent.message.sent",
            payload={"status": task_result.status, "summary": (task_result.summary or "")[:200]},
            task_id=task.task_id,
            parent_task_id=task.parent_task_id,
            child_task_id=(task.child_task_ids or [None])[0],
            message_id=str(state.get("last_message_id") or "") or None,
        )
        if scheduled_subtask is not None:
            _append_event_trace(
                state,
                "agent.subtask.completed",
                payload={
                    "subtask_id": scheduled_subtask.get("subtask_id"),
                    "status": task_result.status,
                    "summary": task_result.summary,
                },
                task_id=task.task_id,
            )
        _apply_task_result_to_state(state, task, task_result)

        if task_result.status != "completed":
            break

    state["turn_context"] = None
    state["current_agent"] = AgentRole.MASTER.value
    state["agent_id"] = coordinator_agent_id
    state["parent_agent_id"] = None
    state["root_agent_id"] = root_agent_id
    state["collaboration_depth"] = 0
    state["final_response"] = _build_collaboration_final_response(state.get("task_results") or [])
    _set_phase(
        state, "phase_4_visibility_and_verification", reason="collaboration_verification_started"
    )
    _record_checkpoint(state, "collaboration.verification_started", reason="before_verify")
    _append_event_trace(
        state,
        "agent.verify.started",
        payload={
            "summary_present": bool(state["final_response"]),
            "evidence_count": len(state.get("task_results") or []),
        },
    )
    _verify_collaboration_results(state, tasks, state.get("task_results") or [])
    _append_event_trace(
        state,
        "agent.verify.completed",
        payload={
            "status": state.get("verification_status"),
            "summary": state.get("verification_summary"),
            "evidence_count": len(state.get("verification_evidence") or []),
        },
        severity="error" if state.get("verification_status") == "failed" else "info",
    )
    merge_report = state.get("merge_report") or {}
    if state.get("verification_status") == "failed" and merge_report.get("fallback_used"):
        state["final_response"] = _build_serial_fallback_response(
            user_query,
            state.get("task_results") or [],
            merge_report,
        )
        state["routing_decision"] = {
            "mode": "direct",
            "reason": "fallback_to_serial_recovery",
        }
        _append_event_trace(
            state,
            "agent.rollback.triggered",
            payload={
                "layer": "collaboration_runtime",
                "reason": "merge_fallback_used",
            },
            severity="warning",
        )
    _record_checkpoint(
        state,
        "collaboration.completed",
        reason="collaboration_flow_finished",
        payload={"verification_status": state.get("verification_status")},
    )
    state["messages"] = [*base_history, AIMessage(content=state["final_response"])]
    state["should_respond"] = True
    return state


def _can_delegate_within_hop_budget(state: AgentState) -> bool:
    return _get_state_int(state, "routing_hops") < _get_state_int(state, "max_routing_hops")


def _stop_due_to_loop_guard(state: AgentState) -> AgentState:
    state["terminated_due_to_loop_guard"] = True
    state["final_response"] = (
        "这次需要处理的步骤有点多，我先停在这里。您可以把目标再明确一点，或让我先只完成其中一步。"
    )
    state["messages"] = [*state.get("messages", []), AIMessage(content=state["final_response"])]
    return state


async def master_node(state: AgentState) -> AgentState:
    _maybe_reset_turn_budgets(state)
    _set_phase(state, "phase_1_routing", reason="master_node_entered")
    _record_checkpoint(state, "routing.master_entered", reason="master_node_entered")
    user_messages = _filter_user_messages(state["messages"])
    user_query = (
        _stringify_message_content(user_messages[-1].content).strip() if user_messages else ""
    )

    state["current_agent"] = state.get("current_agent") or AgentRole.MASTER
    state["active_agents"] = _normalize_active_agents(state.get("active_agents"))

    if _is_simple_greeting(user_query):
        state["final_response"] = "您好。我在。\n\n您把问题给我，我先帮您收束重点，再往下推。"
        state["messages"] = [*state.get("messages", []), AIMessage(content=state["final_response"])]
        return state

    if _is_identity_question(user_query):
        state["final_response"] = (
            "我是 Jarvis。\n\n比起做一个泛泛的助手，我更像您的判断型协作伙伴："
            "帮您看清问题、压缩路径、把事情往前推进。"
        )
        state["messages"] = [*state.get("messages", []), AIMessage(content=state["final_response"])]
        return state

    if _is_capability_question(user_query):
        state["final_response"] = (
            "主要做三件事。\n"
            "- 帮您判断：看问题本质、梳理取舍、给出方向\n"
            "- 帮您收束：把复杂内容理顺，把重点拎出来\n"
            "- 帮您推进：拆任务、定步骤、把下一步变清楚\n\n"
            "如果您现在有具体目标，我可以直接进入处理。"
        )
        state["messages"] = [*state.get("messages", []), AIMessage(content=state["final_response"])]
        return state

    state["current_agent"] = _normalize_current_agent(state.get("current_agent"))
    parallel_worthiness = state.get("parallel_worthiness")
    if not isinstance(parallel_worthiness, dict):
        runtime_request_context = state.get("runtime_request_context") or {}
        if isinstance(runtime_request_context, dict):
            candidate_parallel = runtime_request_context.get("parallel_worthiness")
            if isinstance(candidate_parallel, dict):
                parallel_worthiness = candidate_parallel
    if isinstance(parallel_worthiness, dict) and parallel_worthiness:
        _append_event_trace(
            state,
            "agent.parallel.assessed",
            payload=parallel_worthiness,
        )
    skill_shortlist = list(state.get("skill_shortlist") or [])
    if skill_shortlist:
        _append_event_trace(
            state,
            "agent.skill.shortlisted",
            payload={
                "count": len(skill_shortlist),
                "skills": [str(item.get("skill_name") or "") for item in skill_shortlist[:4]],
            },
        )
    task_graph = state.get("task_graph")
    if isinstance(task_graph, dict) and task_graph.get("nodes"):
        _append_event_trace(
            state,
            "agent.task_graph.built",
            payload={
                "graph_id": task_graph.get("graph_id"),
                "node_count": len(task_graph.get("nodes") or []),
                "entry_node_ids": task_graph.get("entry_node_ids") or [],
                "max_parallelism": task_graph.get("max_parallelism"),
            },
        )
    elif (
        isinstance(parallel_worthiness, dict)
        and parallel_worthiness.get("preferred_mode") in {"collaboration", "parallel"}
        and not (state.get("feature_flags") or {}).get("ENABLE_PARALLEL_TASK_GRAPH", True)
    ):
        _append_event_trace(
            state,
            "agent.rollback.triggered",
            payload={
                "layer": "parallel_task_graph",
                "reason": "feature_flag_disabled",
            },
        )

    structured_continuity_route = _route_from_structured_continuity(state, user_query)
    clarification_route = _route_from_clarification_context(state, user_query)
    if structured_continuity_route is not None:
        state["execution_mode"] = "direct"
        routed_agent = structured_continuity_route
        _record_execution_decision(
            state,
            user_query=user_query,
            mode="direct",
            reason="continue_pending_action",
            selected_roles=[routed_agent.value],
        )
    elif clarification_route is not None:
        state["execution_mode"] = "direct"
        routed_agent = clarification_route
        _record_execution_decision(
            state,
            user_query=user_query,
            mode="direct",
            reason="clarification_follow_up",
            selected_roles=[routed_agent.value],
        )
    elif _is_short_confirmation(user_query) and _previous_turn_proposed_schedule_creation(
        state.get("messages", [])
    ):
        state["execution_mode"] = "direct"
        routed_agent = AgentRole.SCHEDULE_PLANNER
        _record_execution_decision(
            state,
            user_query=user_query,
            mode="direct",
            reason="schedule_confirmation_follow_up",
            selected_roles=[routed_agent.value],
        )
    else:
        request_mode, routing_metadata = _select_request_mode(user_query)
        state["routing_decision"] = routing_metadata
        _record_execution_decision(
            state,
            user_query=user_query,
            mode=request_mode,
            reason=str(routing_metadata.get("reason") or request_mode),
            selected_roles=list(routing_metadata.get("roles") or []),
        )
        if request_mode == "collaboration":
            collaboration_state = await _run_collaboration_flow(state, user_query)
            if collaboration_state.get(
                "execution_mode"
            ) == "collaboration" or collaboration_state.get("final_response"):
                return collaboration_state
        state["execution_mode"] = "direct"
        routed_agent = _route_agent_from_user_query(user_query)
    if routed_agent != AgentRole.MASTER:
        if not _can_delegate_within_hop_budget(state):
            return _stop_due_to_loop_guard(state)
        state["routing_hops"] = int(state.get("routing_hops") or 0) + 1
        state["current_agent"] = routed_agent.value
        state["next_step"] = routed_agent.value
        if routed_agent not in state["active_agents"]:
            state["active_agents"] = [*state["active_agents"], routed_agent]
        state["agent_trace"] = [
            *(state.get("agent_trace") or [AgentRole.MASTER.value]),
            routed_agent.value,
        ]
        return state

    llm = _get_llm_for_state(state)
    response = await _invoke_llm(
        llm, [SystemMessage(content=MASTER_SYSTEM_PROMPT), *state["messages"]]
    )
    _record_response_usage(state, response)
    content = _stringify_message_content(response.content).strip()

    routed_agent = _route_agent_from_user_query(content)
    if routed_agent != AgentRole.MASTER and len(content) <= 64:
        if not _can_delegate_within_hop_budget(state):
            return _stop_due_to_loop_guard(state)
        state["routing_hops"] = int(state.get("routing_hops") or 0) + 1
        state["current_agent"] = routed_agent.value
        state["next_step"] = routed_agent.value
        if routed_agent not in state["active_agents"]:
            state["active_agents"] = [*state["active_agents"], routed_agent]
        state["agent_trace"] = [
            *(state.get("agent_trace") or [AgentRole.MASTER.value]),
            routed_agent.value,
        ]
        return state

    state["final_response"] = content
    state["messages"] = [*state.get("messages", []), AIMessage(content=content)]
    return state


async def planner_node(state: AgentState) -> AgentState:
    state["next_step"] = None
    user_messages = _filter_user_messages(state["messages"])
    user_query = _stringify_message_content(user_messages[-1].content) if user_messages else ""
    if _has_active_clarification_context(state):
        user_query = _build_resumed_clarification_query(state, user_query)
    elif _is_short_confirmation(user_query) and _previous_turn_proposed_schedule_creation(
        state.get("messages", [])
    ):
        user_query = _expand_schedule_confirmation_query(user_query, state.get("messages", []))
    return await _run_sub_commander(
        state,
        AgentRole.SCHEDULE_PLANNER,
        ROLE_SYSTEM_PROMPTS[AgentRole.SCHEDULE_PLANNER],
        user_query,
        use_tools=True,
        summary_target="schedule_context_summary",
    )


async def executor_node(state: AgentState) -> AgentState:
    user_messages = _filter_user_messages(state["messages"])
    user_query = _stringify_message_content(user_messages[-1].content) if user_messages else ""
    return await _run_sub_commander(
        state,
        AgentRole.EXECUTOR,
        ROLE_SYSTEM_PROMPTS[AgentRole.EXECUTOR],
        user_query,
        use_tools=True,
    )


async def librarian_node(state: AgentState) -> AgentState:
    user_messages = _filter_user_messages(state["messages"])
    user_query = _stringify_message_content(user_messages[-1].content) if user_messages else ""
    return await _run_sub_commander(
        state,
        AgentRole.LIBRARIAN,
        ROLE_SYSTEM_PROMPTS[AgentRole.LIBRARIAN],
        user_query,
        use_tools=True,
        summary_target="knowledge_context",
    )


async def analyst_node(state: AgentState) -> AgentState:
    user_messages = _filter_user_messages(state["messages"])
    user_query = _stringify_message_content(user_messages[-1].content) if user_messages else ""
    return await _run_sub_commander(
        state,
        AgentRole.ANALYST,
        ROLE_SYSTEM_PROMPTS[AgentRole.ANALYST],
        user_query,
        use_tools=True,
        summary_target="analysis_report",
    )


async def code_commander_node(state: AgentState) -> AgentState:
    """
    Code Commander node - handles code execution tasks
    Routes to DirectExecutor (LOW risk) or SandboxExecutor (HIGH risk)
    """
    from app.agents.prompts import CODE_COMMANDER_SYSTEM_PROMPT
    from app.agents.tools.ai_adapter import get_adapter
    from app.agents.tools.direct_executor import DirectExecutor
    from app.agents.tools.sandbox_executor import SandboxExecutor
    from app.agents.tools.security_classifier import RiskLevel, SecurityClassifier

    user_messages = _filter_user_messages(state["messages"])
    user_query = _stringify_message_content(user_messages[-1].content) if user_messages else ""

    # Get AI provider from state (default: claude)
    ai_provider = str(state.get("code_ai_provider") or "claude")

    # Classify risk level
    classifier = SecurityClassifier()
    risk_level = classifier.classify(user_query)

    # Select executor based on risk
    adapter = get_adapter(ai_provider)
    if risk_level == RiskLevel.LOW:
        executor = DirectExecutor(adapter)
        result_parts = []
        async for line in executor.execute(user_query):
            result_parts.append(line)
        result_output = "".join(result_parts)
    else:
        sandbox_exec = SandboxExecutor(adapter)
        result_parts = []
        async for line in sandbox_exec.execute(user_query):
            result_parts.append(line)
        result_output = "".join(result_parts)

    state["final_response"] = result_output
    state["code_execution_result"] = {
        "ai_provider": ai_provider,
        "risk_level": risk_level.value,
        "output": result_output,
    }
    state["messages"] = [*state.get("messages", []), AIMessage(content=result_output)]
    state["should_respond"] = True
    return state


def route_agent(state: AgentState) -> str:
    if state.get("final_response"):
        return END
    next_step = _role_value(state.get("next_step"))
    if next_step in _role_values():
        return next_step
    return _role_value(state.get("current_agent") or AgentRole.MASTER)


def _compile_graph(graph: StateGraph, callbacks: list | None = None):
    if callbacks:
        try:
            return graph.compile(callbacks=callbacks)
        except TypeError as exc:
            if "callbacks" not in str(exc):
                raise
    return graph.compile()


def create_agent_graph(callbacks: list | None = None):
    graph = StateGraph(AgentState)

    graph.add_node(AgentRole.MASTER.value, master_node)
    graph.add_node(AgentRole.SCHEDULE_PLANNER.value, planner_node)
    graph.add_node(AgentRole.EXECUTOR.value, executor_node)
    graph.add_node(AgentRole.LIBRARIAN.value, librarian_node)
    graph.add_node(AgentRole.ANALYST.value, analyst_node)
    graph.add_node(AgentRole.CODE_COMMANDER.value, code_commander_node)

    graph.set_entry_point(AgentRole.MASTER.value)
    graph.add_conditional_edges(
        AgentRole.MASTER.value,
        route_agent,
        {
            AgentRole.SCHEDULE_PLANNER.value: AgentRole.SCHEDULE_PLANNER.value,
            AgentRole.EXECUTOR.value: AgentRole.EXECUTOR.value,
            AgentRole.LIBRARIAN.value: AgentRole.LIBRARIAN.value,
            AgentRole.ANALYST.value: AgentRole.ANALYST.value,
            AgentRole.CODE_COMMANDER.value: AgentRole.CODE_COMMANDER.value,
            END: END,
        },
    )

    for role in (
        AgentRole.SCHEDULE_PLANNER,
        AgentRole.EXECUTOR,
        AgentRole.LIBRARIAN,
        AgentRole.ANALYST,
        AgentRole.CODE_COMMANDER,
    ):
        graph.add_edge(role.value, END)

    return _compile_graph(graph, callbacks=callbacks)


_agent_graph = None


def get_agent_graph(callbacks: list | None = None):
    global _agent_graph
    if _agent_graph is None:
        from app.config_tracing import get_langsmith_callbacks

        langsmith_callbacks = get_langsmith_callbacks()
        all_callbacks = (callbacks or []) + langsmith_callbacks
        _agent_graph = create_agent_graph(callbacks=all_callbacks or None)
    return _agent_graph


__all__ = [
    "_build_collaboration_tasks",
    "_build_verifier_hints",
    "_choose_sub_commander",
    "_parse_json_action",
    "_route_agent_from_user_query",
    "_select_request_mode",
    "_run_collaboration_flow",
    "_run_sub_commander",
    "create_agent_graph",
    "get_agent_graph",
    "master_node",
]
