"""
Klynx Agent - Graph nodes mixin

Contains node-level logic used by KlynxAgent StateGraph.
"""

import os
import re
import json
from typing import Any, Dict

from langchain_core.messages import AIMessage, HumanMessage, RemoveMessage

from klynx.agent.context_manager import TokenCounter
from klynx.model.adapter import LiteLLMResponse

from .state import AgentState


class NodesMixin:
    """Graph nodes for KlynxAgent."""

    AUTH_BLOCK_KEYWORDS = (
        "login",
        "log in",
        "sign in",
        "sign-in",
        "authenticate",
        "authentication",
        "unauthorized",
        "forbidden",
        "403",
        "captcha",
        "verification",
        "please sign in",
        "请登录",
        "登录",
        "未登录",
        "需要登录",
        "需登录",
        "认证",
        "鉴权",
        "权限不足",
        "验证码",
    )

    def _init_klynx_dir(self):
        """
        初始化 .klynx 配置目录
        
        在 memory_dir 下创建 .klynx 文件夹，并生成模板 .rules 和 .memory 文件。
        如果 memory_dir 为空则跳过。
        """
        if not self.memory_dir:
            return
        
        klynx_dir = os.path.join(self.memory_dir, ".klynx")
        
        if not os.path.isdir(klynx_dir):
            os.makedirs(klynx_dir, exist_ok=True)
            self._emit("info", f"[配置] 已创建 .klynx/ 目录: {klynx_dir}")
        
        # 初始化 .rules 文件
        rules_path = os.path.join(klynx_dir, ".rules")
        if not os.path.isfile(rules_path):
            template = '<rules>\n</rules>'
            with open(rules_path, 'w', encoding='utf-8') as f:
                f.write(template)
            self._emit("info", "[配置] 已创建 .klynx/.rules 模板")
        
        # 初始化 .memory 文件
        memory_path = os.path.join(klynx_dir, ".memory")
        if not os.path.isfile(memory_path):
            template = '<memory>\n</memory>'
            with open(memory_path, 'w', encoding='utf-8') as f:
                f.write(template)
            self._emit("info", "[配置] 已创建 .klynx/.memory 模板")

        # 初始化 skills 目录（用于存放用户安装的本地技能包）
        skills_dir = os.path.join(klynx_dir, "skills")
        if not os.path.isdir(skills_dir):
            os.makedirs(skills_dir, exist_ok=True)
            self._emit("info", "[配置] 已创建 .klynx/skills 目录")


    def _load_rules(self) -> str:
        """
        加载 .klynx/.rules 文件内容（从 memory_dir 读取）
        
        Returns:
            .rules 文件的内容字符串
        """
        if not self.memory_dir:
            return ""
        
        rules_path = os.path.join(self.memory_dir, ".klynx", ".rules")
        
        if not os.path.isfile(rules_path):
            self._emit("info", "[配置] .klynx/.rules 文件不存在")
            return ""
        
        try:
            with open(rules_path, 'r', encoding='utf-8', errors='ignore') as f:
                content = f.read().strip()
            if content:
                self._emit("info", f"[配置] 已加载 .klynx/.rules ({len(content)} 字符)")
                return content
            return ""
        except Exception as e:
            self._emit("error", f"[配置] 读取 .klynx/.rules 失败: {e}")
            return ""


    def _find_klynx_docs(self) -> str:
        """
        递归搜索工作目录下的所有 KLYNX.md 文件
        
        KLYNX.md 描述对应文件夹的项目内容，在 agent 节点前加载。
        
        Returns:
            所有 KLYNX.md 的路径和内容（XML格式）
        """
        skip_dirs = {'.git', '__pycache__', 'node_modules', '.venv', 'venv', 
                     '.idea', '.vscode', '.klynx'}
        docs_parts = []
        
        for root, dirs, files in os.walk(self.working_dir):
            # 跳过忽略目录
            dirs[:] = [d for d in dirs if d not in skip_dirs and not d.startswith('.')]
            
            if 'KLYNX.md' in files:
                filepath = os.path.join(root, 'KLYNX.md')
                try:
                    with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
                        content = f.read().strip()
                    if content:
                        rel_dir = os.path.relpath(root, self.working_dir)
                        if rel_dir == '.':
                            rel_dir = '(根目录)'
                        docs_parts.append(
                            f'  <doc path="{self._escape_xml(rel_dir)}">\n'
                            f'    {self._escape_xml(content)}\n'
                            f'  </doc>'
                        )
                        self._emit("info", f"[配置] 已加载 {os.path.relpath(filepath, self.working_dir)}")
                except Exception as e:
                    self._emit("error", f"[配置] 读取 {filepath} 失败: {e}")
        
        if not docs_parts:
            return ""
        
        return '<project_docs description="KLYNX.md 描述了对应文件夹下的项目内容">\n' + '\n'.join(docs_parts) + '\n</project_docs>'


    def _init_node(self, state: AgentState) -> Dict[str, Any]:
        """
        初始化节点 - 在每次运行最开始（分类节点之前）执行
        
        职责：
        1. 如果 memory_dir 非空，检查/创建 .klynx 文件夹及 .rules/.memory 文件
        2. 加载 .klynx/.rules 内容到 state
        """
        if self.memory_dir:
            self._emit("info", f"[初始化] 检查 .klynx 配置 ({self.memory_dir})...")
            self._init_klynx_dir()
            rules_content = self._load_rules()
            # 自动加载 memory_dir/.klynx/skills 下的技能（若存在）
            if hasattr(self, "_load_memory_skills"):
                try:
                    self._load_memory_skills()
                except Exception as e:
                    self._emit("warning", f"[Skills] 自动加载 .klynx/skills 失败: {e}")
        else:
            self._emit("info", "[初始化] 未设置 memory_dir，跳过 .klynx 配置加载")
            rules_content = ""
        
        return {
            "project_rules": rules_content
        }


    def _summarize_context(self, state: AgentState) -> Dict[str, Any]:
        """
        上下文总结节点 - 当上下文接近爆满时调用LLM总结对话历史
        
        保留：当前任务目标、正在进行的操作
        删除：已完成任务的详细内容
        
        注意：不直接修改 messages（因为使用 reducer），而是将总结存入 context_summary
        
        Returns:
            更新后的状态，包含总结
        """
        current_task = state.get("current_task", "")
        messages = state.get("messages", [])
        
        if not messages or not self.model:
            return {}
        
        self._emit("info", f"[上下文总结] 对话历史过长（{len(messages)} 条消息），正在总结...")
        
        # 构建总结提示词
        history_text = self._format_conversation_history(messages)
        
        summarize_prompt = f"""请总结以下对话历史，生成一个简洁的摘要。

<current_task>
{current_task}
</current_task>

<conversation_history>
{history_text}
</conversation_history>

<requirements>
1. 保留当前任务的关键信息和进度
2. 保留最近执行的操作和结果
3. 删除已完成子任务的详细内容
4. 保留任何重要的发现或错误信息
5. 摘要应该让LLM能够继续执行当前任务
6. 输出结构化的摘要，包含：任务进度、已完成操作、待处理事项
</requirements>

请直接输出总结内容，不需要额外标签。"""
        
        try:
            response = self.model.invoke([HumanMessage(content=summarize_prompt)])
            summary = response.content.strip()
            
            # Token 用量对比
            if hasattr(response, 'usage') and response.usage:
                actual_usage = response.usage
                self._emit("token_usage", f"  [总结Token用量] Prompt: {actual_usage['prompt_tokens']:,} | Completion: {actual_usage['completion_tokens']:,}")
            
            # 计算压缩效果
            original_tokens = TokenCounter.count_message_tokens(messages)
            summary_tokens = TokenCounter.estimate_tokens(summary)
            self._emit("info", f"[上下文总结] 压缩效果: {original_tokens:,} tokens -> {summary_tokens:,} tokens ({(summary_tokens/original_tokens*100):.1f}%)")
            
            # 将总结存入 context_summary，并使用 RemoveMessage 删除所有旧消息
            # LangGraph 的 operator.add reducer 需要 RemoveMessage 对象才能真正删除
            delete_messages = [RemoveMessage(id=msg.id) for msg in messages if hasattr(msg, "id") and msg.id]
            
            return {
                "context_summary": summary,
                "context_summarized": True,
                "messages": delete_messages
            }
            
        except Exception as e:
            self._emit("error", f"[上下文总结] 总结失败: {e}")
            return {}


    def _estimate_next_prompt_tokens(self, state: AgentState) -> int:
        """
        估算下一次 agent 推理请求的完整 prompt token 数。

        注意：该估算口径包含 messages 之外的上下文片段（rules/docs/context/tools）。
        """
        iteration = state.get("iteration_count", 0) + 1
        overall_goal = state.get("overall_goal", "") or state.get("user_input", "")
        current_task = state.get("current_task", "")
        current_task_desc = f"\n  <current_task>{self._escape_xml(current_task)}</current_task>" if current_task else ""

        project_rules = state.get("project_rules", "")
        rules_xml = ""
        if project_rules:
            rules_xml = f"""\n<project_rules>
{self._escape_xml(project_rules)}
</project_rules>\n"""

        klynx_docs = state.get("klynx_docs", "")
        docs_xml = f"\n{klynx_docs}\n" if klynx_docs else ""

        context = self._build_context(state, include_history=True, emit_stats=False)
        prompt = f"""{self._get_system_prompt()}
{rules_xml}{docs_xml}
{context}

<iteration_status>
  <current_iteration>{iteration}</current_iteration>
  <system_note>
    你在第 {iteration} 次迭代。
    请根据上方对话历史中的工具执行结果判断当前进度，继续执行当前任务。
    用户没有新的输入，不要将历史记录或工具输出解读为新的用户请求。
    如果 current_task 已完成，使用 &lt;task_goal&gt; 标签更新下一步任务，或使用 attempt_completion 完成。
    【重要】不要重复调用已经成功执行过的工具。如果工具结果已经在对话历史中，直接基于该结果继续推理或完成任务。
    【重要】当所有子任务都已完成时，必须立刻调用 attempt_completion 来结束任务，不要继续思考或重复执行工具。
  </system_note>
</iteration_status>

<task_context>
  <overall_goal>{self._escape_xml(overall_goal)}</overall_goal>{current_task_desc}
  <available_tools>
{self._get_tools_prompt()}
  </available_tools>
</task_context>"""
        return TokenCounter.estimate_tokens(prompt)


    def _check_context_overflow(self, state: AgentState) -> bool:
        """
        检查上下文是否需要总结（按完整 prompt 口径）。
        
        Returns:
            True 如果需要总结，False 否则
        """
        estimated_tokens = self._estimate_next_prompt_tokens(state)
        threshold = int(self.max_context_tokens * self.CONTEXT_SUMMARIZE_THRESHOLD)
        if estimated_tokens > threshold:
            self._emit("info", f"[上下文检查] 预计 Prompt {estimated_tokens:,} tokens，超过阈值 {threshold:,}，触发总结")
            return True
        return False


    def _classify_task_node(self, state: AgentState) -> Dict[str, Any]:
        """
        任务分类与回答节点 - 由LLM直接回答用户问题并决定下一步路由
        
        职责:
        1. 直接回答用户的问题
        2. 判断是否需要进入Agent工具执行流程
        3. 给出下一步建议
        
        路由结果:
        - end: 简单问题已直接回答，无需工具，结束
        - agent: 需要使用工具执行的任务，进入Agent流程
        """
        user_input = state.get("user_input", "")
        
        self._emit("info", "[分类与回答] 由LLM分析并回答用户问题...")
        
        if self.model is None:
            self._emit("info", "[分类与回答] 未设置模型，默认进入Agent流程")
            return {"task_type": "agent"}
        
        # 获取已有的对话历史（优先使用 messages，其次使用 context_summary）
        history_msgs = state.get("messages", [])
        context_summary = state.get("context_summary", "")
        history_text = ""
        if history_msgs:
            # classify 阶段使用轻量摘要，降低噪声和 token 成本
            history_text = self._quick_summarize_messages(history_msgs, max_entries=8)
        elif context_summary:
            # 新一轮对话：旧消息已被总结为 context_summary
            history_text = context_summary
        
        # 构建提示词：包含对话历史，要求LLM回答问题并决定路由
        history_section = ""
        if history_text:
            history_section = f"""
以下是之前的对话历史：
{history_text}

"""
        
        # 获取 .rules 规则
        project_rules = state.get("project_rules", "")
        rules_section = ""
        if project_rules:
            rules_section = f"\n<project_rules>\n{project_rules}\n</project_rules>\n"
        
        # 生成已加载工具列表提示
        tools_hint = ""
        if self.tools:
            tool_names = ", ".join(self.tools.keys())
            tools_hint = f"\n你可以使用的工具包括: {tool_names}\n"
        
        classify_prompt = f"""你是 Klynx Agent，一个智能编程助手。
{rules_section}{history_section}{tools_hint}用户说: {user_input}

请你完成以下任务：

1. **直接回答用户的问题**：结合对话历史，给出清晰、有帮助的回复。

2. **判断下一步路由**：在回答末尾，使用以下标签标记你的路由决策：
   - [end]: 如果这是一个简单的问候、闲聊、常识性问答等，你已经完整回答，不需要使用任何工具。
   - [agent]: 如果用户的请求涉及以下任何操作：查看文件、执行命令、编写代码、联网搜索、启动交互式环境等，需要进入Agent执行流程。

3. **如果路由为 [agent]**：请额外输出以下两个标签：
   - <overall_goal>从用户需求中提炼的总任务目标（简明扼要，一句话概括）</overall_goal>
   - <current_task>当前应该执行的第一步具体操作</current_task>

【重要】当用户提到"搜索"、"查找最新"、"联网"等关键词时，必须路由到 [agent]，让工具 web_search 执行实际搜索，严禁自行编造搜索结果。

注意：路由标签必须放在回答的最后一行，格式为 [end] 或 [agent]。"""
        
        try:
            messages = [HumanMessage(content=classify_prompt)]
            
            # 流式调用模型
            has_stream = hasattr(self.model, 'stream') and callable(self.model.stream)
            full_content = ""
            full_reasoning = ""
            
            if has_stream:
                for chunk in self.model.stream(messages):
                    content_delta = chunk.get("content", "")
                    reasoning_delta = chunk.get("reasoning_content", "")
                    if reasoning_delta:
                        full_reasoning += reasoning_delta
                        self._emit("reasoning_token", reasoning_delta)
                    if content_delta:
                        full_content += content_delta
                        self._emit("token", content_delta)
            else:
                response = self.model.invoke(messages)
                full_content = response.content.strip()
                full_reasoning = self._extract_reasoning_content(response) or ""
                if full_reasoning:
                    self._emit("reasoning", full_reasoning[:500] + "..." if len(full_reasoning) > 500 else full_reasoning)
                self._emit("answer", full_content)
            
            content = full_content.strip()
            
            # 解析路由决策
            route_match = re.search(r'\[(end|agent)\]', content, re.IGNORECASE)
            
            if route_match:
                task_type = route_match.group(1).lower()
                self._emit("routing", f"[路由决策] {task_type} (由LLM判断)")
            else:
                # 降级策略：如果没有显式标签，检查是否有工具调用意图
                if self.xml_parser.parse(content):
                    task_type = "agent"
                    self._emit("routing", "[路由决策] agent (检测到工具调用)")
                else:
                    task_type = "end"
                    self._emit("routing", "[路由决策] end (无工具调用，默认结束)")
            
            # 将回答转换为 AIMessage
            kwargs = {}
            if full_reasoning:
                kwargs["reasoning_content"] = full_reasoning
            ai_message = AIMessage(content=content, additional_kwargs=kwargs)
            
            # 提取 overall_goal 和 current_task（仅 agent 路由时）
            result = {
                "messages": [ai_message],
                "task_type": task_type,
                "iteration_count": 1
            }
            
            if task_type == "agent":
                goal_match = re.search(r'<overall_goal>(.*?)</overall_goal>', content, re.DOTALL)
                task_match = re.search(r'<current_task>(.*?)</current_task>', content, re.DOTALL)
                
                if goal_match:
                    extracted_goal = goal_match.group(1).strip()
                    self._emit("info", f"[分类] 总目标: {extracted_goal}")
                    result["overall_goal"] = extracted_goal
                else:
                    stripped_input = user_input.strip()
                    # 降级：如果用户明确输入了新任务且非无意义的短语，才作为新的总目标
                    if stripped_input and stripped_input.lower() not in ('', '继续', '好', '好的', 'ok', 'yes', '易', '嗯', '确认', 'continue', 'go', 'y'):
                        result["overall_goal"] = stripped_input
                
                if task_match:
                    extracted_task = task_match.group(1).strip()
                    self._emit("info", f"[分类] 当前任务: {extracted_task}")
                    result["current_task"] = extracted_task
            
            return result
                    
        except Exception as e:
            self._emit("error", f"[分类与回答] LLM调用失败: {e}")
            self._emit("info", "[分类与回答] 默认进入Agent流程")
            return {"task_type": "agent"}


    def _route_after_classify(self, state: AgentState) -> str:
        """
        分类后的路由决策
        - end: 已直接回答，结束
        - load_context: 需要工具，先加载项目上下文再进入Agent
        """
        task_type = state.get("task_type", "agent")
        
        if task_type == "end":
            self._emit("routing", "[路由] 简单问题已回答，结束执行")
            return "end"
        else:
            self._emit("routing", "[路由] 需要工具执行，加载项目上下文后进入Agent")
            return "load_context"


    def _direct_answer_node(self, state: AgentState) -> Dict[str, Any]:
        """
        Direct answer node for simple questions without tools.
        This node does not consume injected agent-node system prompt.
        """
        if self.model is None:
            raise ValueError("Model is not configured.")

        task = state.get("current_task", "") or state.get("user_input", "")
        self._emit("info", "[direct-answer] handling simple question...")

        ask_system_prompt = (getattr(self, "ASK_SYSTEM_PROMPT", "") or "").strip()
        if ask_system_prompt:
            prompt_content = f"""{ask_system_prompt}

<direct_answer_task>
  <user_input>{self._escape_xml(task)}</user_input>
  <note>Please answer directly without calling any tools.</note>
</direct_answer_task>"""
        else:
            prompt_content = task

        messages = [HumanMessage(content=prompt_content)]

        try:
            has_stream = hasattr(self.model, "stream") and callable(self.model.stream)
            full_content = ""
            full_reasoning = ""

            if has_stream:
                for chunk in self.model.stream(messages):
                    content_delta = chunk.get("content", "")
                    reasoning_delta = chunk.get("reasoning_content", "")
                    if reasoning_delta:
                        full_reasoning += reasoning_delta
                        self._emit("reasoning_token", reasoning_delta)
                    if content_delta:
                        full_content += content_delta
                        self._emit("token", content_delta)
            else:
                response = self.model.invoke(messages)
                full_content = response.content or ""
                full_reasoning = self._extract_reasoning_content(response) or ""
                if full_reasoning:
                    preview = full_reasoning[:1000] + "..." if len(full_reasoning) > 1000 else full_reasoning
                    self._emit("reasoning", preview)
                self._emit("answer", full_content)

            kwargs = {}
            if full_reasoning:
                kwargs["reasoning_content"] = full_reasoning
            ai_message = AIMessage(content=full_content, additional_kwargs=kwargs)

            return {
                "messages": [ai_message],
                "task_completed": True,
                "iteration_count": 1,
            }
        except Exception as e:
            self._emit("error", f"[error] {e}")
            return {
                "task_completed": True,
                "iteration_count": 1,
            }


    def _load_context_node(self, state: AgentState) -> Dict[str, Any]:
        """
        加载项目上下文节点 - 在 agent 前执行
        
        递归搜索工作目录下所有 KLYNX.md 文件，将路径和内容加载到 state 中。
        可通过 load_project_docs=False 跳过。
        """
        if not self.load_project_docs:
            self._emit("info", "[加载上下文] 已禁用项目文档加载，跳过 KLYNX.md 搜索")
            return {"klynx_docs": ""}
        
        self._emit("info", "[加载上下文] 搜索 KLYNX.md 文件...")
        
        klynx_docs = self._find_klynx_docs()
        
        if klynx_docs:
            doc_count = klynx_docs.count('<doc ')
            self._emit("info", f"[加载上下文] 找到 {doc_count} 个 KLYNX.md 文件")
        else:
            self._emit("info", "[加载上下文] 未找到 KLYNX.md 文件")
        
        return {
            "klynx_docs": klynx_docs
        }


    def _summary_node(self, state: AgentState) -> Dict[str, Any]:
        """
        总结节点 - 在任务结束前生成一次最终总结。

        约束：
        1. 只发出一次 summary 事件，避免 token/summary 双重输出。
        2. 不将总结再次写入 messages，避免后续轮次历史重复。
        """
        self._emit("info", "[总结] 正在生成工作总结...")

        progress = (state.get("progress_summary", "") or "").strip()
        user_input = (state.get("user_input", "") or "").strip()
        current_task = (state.get("current_task", "") or "").strip()
        messages = state.get("messages", [])

        # 优先使用结构化进度，缺失时回退到轻量历史摘要，避免重复信息堆叠
        evidence_text = progress or self._quick_summarize_messages(messages, max_entries=10)

        summary_prompt = f"""你是 Klynx Agent。任务已经执行完毕，请输出最终总结。

用户原始请求:
{user_input}

当前任务目标:
{current_task or "（未显式设置）"}

执行证据:
{evidence_text or "（无可用执行记录）"}

请严格按以下格式输出：

## 工作总结

### 完成的工作
- 按时间顺序列出已完成事项

### 修改的文件
- 列出修改/新增/删除的文件及关键变更

### 存在的问题
- 如有未解决项写明原因；若无则写“无”
"""

        summary_content = ""
        if self.model:
            try:
                # 使用非流式调用，避免 token 级事件与 summary 事件重复输出
                response = self.model.invoke([HumanMessage(content=summary_prompt)])
                summary_content = (response.content or "").strip()
            except Exception as e:
                self._emit("error", f"[总结] LLM调用失败: {e}")
                summary_content = (
                    "## 工作总结\n\n"
                    "### 完成的工作\n"
                    "- 已完成任务执行，详见执行记录\n\n"
                    "### 修改的文件\n"
                    "- 详见执行记录\n\n"
                    "### 存在的问题\n"
                    f"- 总结生成失败: {e}"
                )
        else:
            summary_content = (
                "## 工作总结\n\n"
                "### 完成的工作\n"
                f"- {progress if progress else '无可用执行记录'}\n\n"
                "### 修改的文件\n"
                "- 无\n\n"
                "### 存在的问题\n"
                "- 无"
            )

        if not summary_content:
            summary_content = (
                "## 工作总结\n\n"
                "### 完成的工作\n"
                "- 无可用执行记录\n\n"
                "### 修改的文件\n"
                "- 无\n\n"
                "### 存在的问题\n"
                "- 无"
            )

        self._emit("summary", summary_content)

        return {
            "task_completed": True,
            "summary_content": summary_content
        }


    def _extract_attempt_completion_result(self, content: str) -> str:
        """从 <attempt_completion> 中提取 result 文本。"""
        if not content:
            return ""

        # 首选结构化 result 子标签
        m = re.search(
            r"<attempt_completion>\s*<result>(.*?)</result>\s*</attempt_completion>",
            content,
            re.DOTALL | re.IGNORECASE,
        )
        if m:
            return m.group(1).strip()

        # 回退：提取整个 attempt_completion 内容
        m = re.search(
            r"<attempt_completion>(.*?)</attempt_completion>",
            content,
            re.DOTALL | re.IGNORECASE,
        )
        if not m:
            return ""

        inner = m.group(1).strip()
        inner = re.sub(r"</?result>", "", inner, flags=re.IGNORECASE).strip()
        return inner


    def _parse_verify_completion_json(self, raw_text: str) -> Dict[str, Any]:
        """解析 verify_completion 的 JSON 输出。"""
        text = (raw_text or "").strip()
        if not text:
            return {}

        # 兼容 markdown code fence
        if text.startswith("```"):
            text = re.sub(r"^```(?:json)?\s*", "", text, flags=re.IGNORECASE)
            text = re.sub(r"\s*```$", "", text, flags=re.IGNORECASE)
            text = text.strip()

        data = None
        try:
            data = json.loads(text)
        except Exception:
            json_match = re.search(r"\{.*\}", text, re.DOTALL)
            if json_match:
                try:
                    data = json.loads(json_match.group(0))
                except Exception:
                    data = None

        if not isinstance(data, dict):
            return {}

        decision = str(data.get("decision", "retry")).strip().lower()
        if decision not in ("accept", "retry", "clarify"):
            decision = "retry"

        need_summary_raw = data.get("need_summary", False)
        need_summary = bool(need_summary_raw)
        if isinstance(need_summary_raw, str):
            need_summary = need_summary_raw.strip().lower() in ("true", "1", "yes", "y")

        missing_items = data.get("missing_items", [])
        if isinstance(missing_items, str):
            missing_items = [missing_items.strip()] if missing_items.strip() else []
        elif not isinstance(missing_items, list):
            missing_items = []
        missing_items = [str(item).strip() for item in missing_items if str(item).strip()]

        return {
            "decision": decision,
            "need_summary": need_summary,
            "summary": str(data.get("summary", "") or "").strip(),
            "final_response": str(data.get("final_response", "") or "").strip(),
            "clarification_question": str(data.get("clarification_question", "") or "").strip(),
            "next_task": str(data.get("next_task", "") or "").strip(),
            "missing_items": missing_items,
        }


    def _verify_completion_node(self, state: AgentState) -> Dict[str, Any]:
        """
        完成校验节点：对 attempt_completion 进行最终裁决，并在 accept 时可直接产出总结。
        """
        self._emit("info", "[完成校验] 正在校验任务是否真正完成...")

        user_input = (state.get("user_input", "") or "").strip()
        current_task = (state.get("current_task", "") or "").strip()
        progress = (state.get("progress_summary", "") or "").strip()
        candidate_result = (state.get("completion_candidate_result", "") or "").strip()
        history_text = self._quick_summarize_messages(state.get("messages", []), max_entries=10)
        has_tool_activity = bool(progress)

        parsed: Dict[str, Any] = {}
        if self.model:
            verify_prompt = f"""你是一个严格的任务验收器。请判断任务是否应结束。

用户原始请求:
{user_input}

当前任务目标:
{current_task or "（未显式设置）"}

attempt_completion 提交结果:
{candidate_result or "（无）"}

执行进度记录:
{progress or "（无）"}

最近对话摘要:
{history_text or "（无）"}

判断规则：
1. decision=accept：用户目标已满足，可以结束。
2. decision=retry：尚未满足，需要继续执行，并给出缺失项。
3. decision=clarify：需求不明确或信息不足，需要向用户提问。
4. 如果 decision=accept 且存在实质执行记录（如文件修改/命令执行等），need_summary=true 并填写 summary。
5. 如果是简单问答/问候且无实质执行记录，need_summary=false。

只输出一个 JSON 对象，不要输出其他文本。格式如下：
{{
  "decision": "accept|retry|clarify",
  "need_summary": true,
  "summary": "当 need_summary=true 时填写",
  "final_response": "给用户的最终回复（可为空）",
  "missing_items": ["当 retry 时填写缺失项"],
  "next_task": "当 retry 时建议下一步",
  "clarification_question": "当 clarify 时给用户的问题"
}}"""
            try:
                verify_resp = self.model.invoke([HumanMessage(content=verify_prompt)])
                parsed = self._parse_verify_completion_json(getattr(verify_resp, "content", "") or "")
            except Exception as e:
                self._emit("error", f"[完成校验] LLM调用失败，使用降级策略: {e}")

        if not parsed:
            # 模型不可用或输出不规范时的降级：有执行记录则 accept+summary，否则 accept 无 summary
            parsed = {
                "decision": "accept",
                "need_summary": has_tool_activity,
                "summary": "",
                "final_response": candidate_result,
                "missing_items": [],
                "next_task": "",
                "clarification_question": "",
            }

        decision = parsed.get("decision", "retry")
        need_summary = bool(parsed.get("need_summary", False))
        summary_text = (parsed.get("summary", "") or "").strip()
        final_response = (parsed.get("final_response", "") or "").strip()
        missing_items = parsed.get("missing_items", []) or []
        next_task = (parsed.get("next_task", "") or "").strip()
        clarification_question = (parsed.get("clarification_question", "") or "").strip()

        # 如果模型判定 accept 但 need_summary 缺省，按是否有执行记录兜底
        if decision == "accept" and not need_summary and has_tool_activity:
            need_summary = True

        if decision == "accept":
            if need_summary:
                if not summary_text:
                    summary_text = (
                        "## 工作总结\n\n"
                        "### 完成的工作\n"
                        f"- {candidate_result or '任务已完成'}\n\n"
                        "### 修改的文件\n"
                        f"- {progress if progress else '无'}\n\n"
                        "### 存在的问题\n"
                        "- 无"
                    )
                self._emit("summary", summary_text)
            else:
                # 无总结时直接给出最终文本答复
                reply_text = final_response or candidate_result
                if reply_text:
                    self._emit("answer", reply_text)

            return {
                "task_completed": True,
                "completion_candidate": False,
                "completion_candidate_result": "",
                "verification_decision": "accept",
                "summary_content": summary_text if need_summary else "",
                "ended_without_tools": False,
                "last_action": "complete",
            }

        if decision == "clarify":
            question = clarification_question or "为了继续完成任务，请补充更具体的需求或约束。"
            self._emit("answer", question)
            return {
                "task_completed": False,
                "completion_candidate": False,
                "completion_candidate_result": "",
                "verification_decision": "clarify",
                "summary_content": "",
                "ended_without_tools": False,
                "last_action": "clarify",
            }

        # retry: 回到 agent 继续执行
        retry_note = "；".join(missing_items) if missing_items else "未提供明确缺失项"
        self._emit("warning", f"[完成校验] 验收未通过，继续执行。缺失项: {retry_note}")
        feedback_content = (
            "<verification_feedback>\n"
            f"decision=retry\n"
            f"missing_items={retry_note}\n"
            f"next_task={next_task or '请根据缺失项继续执行并在完成后再次调用 attempt_completion'}\n"
            "</verification_feedback>"
        )
        return {
            "messages": [HumanMessage(content=feedback_content)],
            "task_completed": False,
            "completion_candidate": False,
            "completion_candidate_result": "",
            "verification_decision": "retry",
            "ended_without_tools": False,
            "last_action": "think_more",
            "current_task": next_task or current_task,
        }


    def _route_after_verify_completion(self, state: AgentState) -> str:
        """根据完成校验结果路由下一步。"""
        decision = (state.get("verification_decision", "") or "").strip().lower()
        if decision == "retry":
            self._emit("routing", "[路由] 完成校验未通过，回到 agent 继续执行")
            return "agent"

        if decision == "clarify":
            self._emit("routing", "[路由] 完成校验要求用户补充信息，结束本轮")
            return "end_direct"

        self._emit("routing", "[路由] 完成校验通过，结束执行")
        return "end_direct"


    def _is_auth_blocker_result(self, content: str) -> bool:
        """检测工具结果是否命中登录墙/认证阻碍。"""
        text = (content or "").lower()
        if not text:
            return False

        # 限定到浏览器工具结果或显式错误文本，减少误报
        browser_scope = any(
            token in text for token in ('tool="browser_', "browser_open", "browser_view", "browser_act")
        ) or "<error>" in text
        if not browser_scope:
            return False

        return any(keyword in text for keyword in self.AUTH_BLOCK_KEYWORDS)


    def _observe_env_node(self, state: AgentState) -> Dict[str, Any]:
        """
        观察环境节点 - 获取环境信息后再进入思考
        """
        self._emit("info", "[观察环境] 获取工作目录信息...")
        
        # 获取环境快照
        env_snapshot = self._get_env_snapshot()
        
        # 打印简要信息
        tree_match = re.search(r'<file_tree>(.*?)</file_tree>', env_snapshot, re.DOTALL)
        if tree_match:
            tree_content = tree_match.group(1).strip()
            lines = tree_content.split('\n')[:]
            self._emit("info", f"[目录结构] {len(lines)} 项目录/文件")
            for line in lines[:5]:
                self._emit("info", f"  {line}")
            if len(lines) > 5:
                self._emit("info", f"  ... (还有 {len(lines)-5} 项)")
        
        return {
            "env_snapshot": env_snapshot,
            "last_action": "observe"
        }


    def _model_inference_node(self, state: AgentState) -> Dict[str, Any]:
        """
        模型推理节点 - Agent思维环路中的思考
        
        职责：
        1. 根据上一步工具执行的反馈结果，做出新的思考和判断
        2. 判断任务目标是否需要更新
        3. 使用 context 变量构建提示词
        """
        if self.model is None:
            raise ValueError("未设置模型，无法执行推理")
        
        iteration = state.get("iteration_count", 0) + 1
        self._emit("iteration", f"[迭代 {iteration}]")
        
        # 检查上下文是否需要总结
        if self._check_context_overflow(state):
            summarize_result = self._summarize_context(state)
            if summarize_result:
                # 更新 state 中的 messages
                state = {**state, **summarize_result}
        
        # 获取结构化目标
        overall_goal = state.get("overall_goal", "") or state.get("user_input", "")
        current_task = state.get("current_task", "")
        
        # 构建上下文（包含历史对话）
        context = self._build_context(state, include_history=True)
        
        # 构建 .rules 规则部分（从 state 中读取，由 init 节点加载，始终保留，不压缩）
        project_rules = state.get("project_rules", "")
        rules_xml = ""
        if project_rules:
            rules_xml = f"""\n<project_rules>
{self._escape_xml(project_rules)}
</project_rules>\n"""
        
        # 构建 KLYNX.md 文档部分（从 state 中读取，由 load_context 节点填充）
        klynx_docs = state.get("klynx_docs", "")
        docs_xml = ""
        if klynx_docs:
            docs_xml = f"\n{klynx_docs}\n"
        
        # 构建当前任务描述
        current_task_desc = f"\n  <current_task>{self._escape_xml(current_task)}</current_task>" if current_task else ""
        
        prompt = f"""{self._get_system_prompt()}
{rules_xml}{docs_xml}
{context}

<iteration_status>
  <current_iteration>{iteration}</current_iteration>
  <system_note>
    你在第 {iteration} 次迭代。
    请根据上方对话历史中的工具执行结果判断当前进度，继续执行当前任务。
    用户没有新的输入，不要将历史记录或工具输出解读为新的用户请求。
    如果 current_task 已完成，使用 &lt;task_goal&gt; 标签更新下一步任务，或使用 attempt_completion 完成。
    【重要】不要重复调用已经成功执行过的工具。如果工具结果已经在对话历史中，直接基于该结果继续推理或完成任务。
    【重要】当所有子任务都已完成时，必须立刻调用 attempt_completion 来结束任务，不要继续思考或重复执行工具。
  </system_note>
</iteration_status>

<task_context>
  <overall_goal>{self._escape_xml(overall_goal)}</overall_goal>{current_task_desc}
  <available_tools>
{self._get_tools_prompt()}
  </available_tools>
</task_context>"""
        
        messages = [HumanMessage(content=prompt)]
        
        # 调用模型
        # 根据 tool_call_mode 决定是否传递 tools 参数
        stream_tools = self._json_schemas if (self.tool_call_mode == "native" and self._json_schemas) else None
        
        try:
            # 使用流式调用
            stream = self.model.stream(messages, tools=stream_tools)
            
            full_content = ""
            full_reasoning = ""
            usage_info = {}
            native_tool_calls = []  # 原生 FC 返回的工具调用
            
            for chunk in stream:
                content_delta = chunk.get("content", "")
                reasoning_delta = chunk.get("reasoning_content", "")
                usage_delta = chunk.get("usage", {})
                
                # 原生 Function Calling: 流结束时的 tool_calls 组装结果
                if "tool_calls" in chunk:
                    native_tool_calls = chunk["tool_calls"]
                
                if usage_delta:
                    usage_info = usage_delta
                
                if content_delta:
                    full_content += content_delta
                    self._emit("token", content_delta)
                    if self.streaming_callback:
                        self.streaming_callback({"type": "token", "content": content_delta})
                    
                if reasoning_delta:
                    full_reasoning += reasoning_delta
                    self._emit("reasoning_token", reasoning_delta)
                    if self.streaming_callback:
                        self.streaming_callback({"type": "reasoning_token", "content": reasoning_delta})
            
            # 构造完整响应对象，保持原有逻辑兼容性
            response = LiteLLMResponse(content=full_content, reasoning_content=full_reasoning)
            if usage_info:
                response.usage = usage_info
            if native_tool_calls:
                response.tool_calls = native_tool_calls
            
        except Exception as e:
            self._emit("error", f"[Error] 模型调用失败: {e}")
            raise
        
        content = response.content
        reasoning_content = response.reasoning_content
        
        if reasoning_content:
            pass 
        
        # 修复: DeepSeek 有时将工具调用 XML 放在 reasoning_content 而非 content 中
        # 当 content 为空但 reasoning_content 包含工具调用XML时，使用 reasoning_content 作为 content
        if (not content or len(content.strip()) == 0) and reasoning_content:
            # 检查 reasoning_content 中是否有工具调用或 attempt_completion
            if self.xml_parser.parse(reasoning_content, extra_tool_names=list(self.tools.keys())) or self.xml_parser.has_attempt_completion(reasoning_content):
                self._emit("info", "[修复] 从 reasoning_content 中提取工具调用")
                content = reasoning_content
        
        # 如果开启了 thinking_context，将思考内容添加到 content 中
        if state.get("thinking_context", False) and reasoning_content:
            content = f"<thinking>\n{reasoning_content}\n</thinking>\n\n{content}"

        # 打印回答内容
        if content:
            self._emit("answer", content)
        
        # 提取任务目标更新（如果有）
        task_goal_match = re.search(r'<task_goal>(.*?)</task_goal>', content, re.DOTALL)
        if task_goal_match:
            current_task = task_goal_match.group(1).strip()
            self._emit("info", f"[任务目标更新] {current_task[:100]}..." if len(current_task) > 100 else f"[任务目标更新] {current_task}")
        
        # 提取并打印思考内容
        thinking_match = re.search(r'<thinking>(.*?)</thinking>', content, re.DOTALL)
        if thinking_match:
            thinking_text = thinking_match.group(1).strip()
            self._emit("info", f"[Thinking] {thinking_text[:200]}..." if len(thinking_text) > 200 else f"[Thinking] {thinking_text}")
        
        # ============ 双模式提取工具调用 ============
        tool_calls = []
        if self.tool_call_mode == "native" and response.tool_calls:
            # Native Function Calling 模式：直接使用 API 返回的 tool_calls
            tool_calls = response.tool_calls
            tools_str = ", ".join([tc.get('tool', 'unknown') for tc in tool_calls])
            self._emit("tool_calls", f"[Native Function Calling] 触发了系统工具: {tools_str}")
        else:
            # XML 回退模式（或 native 模式下模型选择了纯文本回复）：使用 XML 解析
            tool_calls = self.xml_parser.parse(content, extra_tool_names=list(self.tools.keys()))
            if tool_calls:
                tools_str = ", ".join([tc.get('tool', 'unknown') for tc in tool_calls])
                self._emit("tool_calls", f"[XML 格式] 触发了系统工具: {tools_str}")
        
        # 检测是否触发了 attempt_completion（候选完成，需进入 verify_completion 校验）
        completion_candidate = False
        completion_candidate_result = ""

        if self.xml_parser.has_attempt_completion(content):
            completion_candidate = True
            completion_candidate_result = self._extract_attempt_completion_result(content)

        if tool_calls:
            for tc in tool_calls:
                if tc.get("tool", "") == "attempt_completion":
                    completion_candidate = True
                    params = tc.get("params", {}) or {}
                    if isinstance(params, dict):
                        result_text = str(params.get("result", "") or "").strip()
                        if result_text:
                            completion_candidate_result = result_text
                    elif not completion_candidate_result:
                        completion_candidate_result = str(params).strip()
                    break

        if completion_candidate:
            result_preview = completion_candidate_result or "（未提供结果文本）"
            self._emit("info", f"[完成申请] 检测到 attempt_completion: {result_preview}")
        
        # 处理空回复（Native FC 模式下，content 为空但有 tool_calls 是正常行为，不算空回复）
        empty_count = state.get("empty_response_count", 0)
        has_native_tool_calls = bool(tool_calls and self.tool_call_mode == "native")
        if (not content or len(content.strip()) == 0) and not has_native_tool_calls:
            empty_count += 1
            self._emit("warning", f"[Warning] 空回复 ({empty_count}/3)")
            if empty_count >= 3:
                kwargs = {}
                if reasoning_content:
                    kwargs["reasoning_content"] = reasoning_content
                ai_message = AIMessage(content=content, additional_kwargs=kwargs) if not isinstance(response, AIMessage) else response
                return {
                    "messages": [ai_message],
                    "iteration_count": iteration,
                    "task_completed": True,
                    "completion_candidate": False,
                    "completion_candidate_result": "",
                    "empty_response_count": empty_count,
                    "current_task": current_task
                }
        else:
            empty_count = 0
        
        # 将响应转换为 AIMessage（使用可能已修正的 content）
        kwargs = {}
        if reasoning_content:
            kwargs["reasoning_content"] = reasoning_content
        # Native FC 模式：将 tool_calls 存入 additional_kwargs，供 _tool_parser_node 读取
        if tool_calls and self.tool_call_mode == "native":
            kwargs["tool_calls"] = tool_calls
        ai_message = AIMessage(content=content, additional_kwargs=kwargs)
        
        # 更新 Token 用量
        prev_total = state.get("total_tokens", 0)
        prev_completion = state.get("completion_tokens", 0)
        
        curr_prompt = 0
        curr_completion = 0
        curr_total = 0
        
        if hasattr(response, 'usage') and response.usage:
            curr_prompt = response.usage.get('prompt_tokens', 0)
            curr_completion = response.usage.get('completion_tokens', 0)
            curr_total = response.usage.get('total_tokens', 0)

        # If usage is missing or total_tokens is 0, use fallback
        if curr_total == 0:
            curr_completion = TokenCounter.estimate_tokens(content)
            curr_prompt = TokenCounter.estimate_tokens(prompt)
            curr_total = curr_prompt + curr_completion

        return {
            "messages": [ai_message],
            "iteration_count": iteration,
            "task_completed": False,
            "completion_candidate": completion_candidate,
            "completion_candidate_result": completion_candidate_result,
            "empty_response_count": empty_count,
            "current_task": current_task,
            "total_tokens": prev_total + curr_total,
            "prompt_tokens": curr_prompt,
            "completion_tokens": prev_completion + curr_completion
        }


    def _feedback_node(self, state: AgentState) -> Dict[str, Any]:
        """
        反馈节点 - 评估工具执行结果并决定是否继续执行
        """
        self._emit("info", "[反馈节点] 评估工具执行结果...")
        
        # 获取最新的消息（包含工具执行结果）
        messages = state.get("messages", [])
        if not messages:
            self._emit("info", "[反馈节点] 无消息可评估")
            return {"task_completed": False, "last_action": "think_more"}
        
        # 获取最新的工具执行结果
        last_message = messages[-1]
        content = getattr(last_message, 'content', '') if hasattr(last_message, 'content') else str(last_message)
        
        # 简化输出
        self._emit("info", f"[反馈节点] 工具执行结果: {content[:100]}..." if len(content) > 100 else f"[反馈节点] 工具执行结果: {content}")

        # 登录墙/认证阻碍：必须停下向用户确认下一步，不允许 agent 自行切换方案
        if self._is_auth_blocker_result(content):
            question = (
                "检测到目标站点需要登录/认证，当前流程可能无法直接完成。"
                "是否改用 web_search 获取公开信息，还是由你提供登录方式后继续浏览器操作？"
            )
            self._emit("warning", "[反馈节点] 检测到登录墙/认证阻碍，暂停并请求用户确认")
            self._emit("answer", question)
            return {
                "task_completed": False,
                "last_action": "clarify",
                "needs_user_confirmation": True,
                "clarification_question": question,
                "ended_without_tools": False,
            }
        
        if state.get("completion_candidate", False):
            self._emit("info", "[反馈节点] 检测到完成申请，进入完成校验")
            return {"last_action": "complete_candidate"}

        task_completed = state.get("task_completed", False)
        if task_completed:
            self._emit("info", "[反馈节点] 任务已完成，结束执行")
            return {"task_completed": True, "last_action": "complete"}

        self._emit("info", "[反馈节点] 任务未完成，回到思考节点")
        return {"task_completed": False, "last_action": "think_more"}


    def _tool_parser_node(self, state: AgentState) -> Dict[str, Any]:
        """
        工具解析节点 - 从 LLM 回复中提取工具调用
        支持双模式：Native Function Calling 和 XML 解析
        """
        messages = state.get("messages", [])
        if not messages:
            self._emit("info", "[工具解析] 无消息可解析")
            return {
                "pending_tool_calls": [],
                "ended_without_tools": False,
                "completion_candidate": False,
                "completion_candidate_result": "",
            }
        
        last_message = messages[-1]
        if not isinstance(last_message, AIMessage):
            self._emit("info", "[工具解析] 最后一条消息不是 AI 消息")
            return {
                "pending_tool_calls": [],
                "ended_without_tools": False,
                "completion_candidate": False,
                "completion_candidate_result": "",
            }
        
        tool_calls = []
        completion_candidate = bool(state.get("completion_candidate", False))
        completion_candidate_result = (state.get("completion_candidate_result", "") or "").strip()
        
        # 双模式解析
        if self.tool_call_mode == "native":
            # Native 模式：优先从 AIMessage 的 additional_kwargs 中读取原生 tool_calls
            native_tcs = last_message.additional_kwargs.get("tool_calls", [])
            if native_tcs:
                tool_calls = native_tcs
            else:
                # Native 模式回退：模型可能以纯文本方式返回了 XML 工具调用
                tool_calls = self.xml_parser.parse(last_message.content, extra_tool_names=list(self.tools.keys()))
        else:
            # XML 模式：使用传统 XML 解析
            tool_calls = self.xml_parser.parse(last_message.content, extra_tool_names=list(self.tools.keys()))
        
        if tool_calls:
            pass
        else:
            self._emit("info", "[无工具调用]")

        # 无论 native/xml，统一检测 attempt_completion（候选完成）
        for tc in tool_calls:
            if tc.get("tool", "") == "attempt_completion":
                completion_candidate = True
                params = tc.get("params", {}) or {}
                if isinstance(params, dict):
                    result_text = str(params.get("result", "") or "").strip()
                    if result_text:
                        completion_candidate_result = result_text
                elif not completion_candidate_result:
                    completion_candidate_result = str(params).strip()
                break

        if not completion_candidate and self.xml_parser.has_attempt_completion(last_message.content or ""):
            completion_candidate = True
            completion_candidate_result = self._extract_attempt_completion_result(last_message.content or "")

        ended_without_tools = False
        if not tool_calls and not completion_candidate:
            ai_content = (getattr(last_message, "content", "") or "").strip()
            reasoning_content = ""
            if getattr(last_message, "additional_kwargs", None):
                reasoning_content = str(last_message.additional_kwargs.get("reasoning_content", "") or "").strip()

            # Prefer assistant content. If empty, fall back to reasoning text for routing.
            routing_text = ai_content or reasoning_content
            if routing_text:
                lowered = routing_text.lower()
                has_task_goal_tag = "<task_goal>" in lowered
                has_tool_xml_hint = any(f"<{tool_name}" in lowered for tool_name in self.tools.keys())

                # Remove meta-thinking blocks so normal conversational replies can end directly.
                normalized_content = re.sub(r"(?is)<thinking>\s*.*?\s*</thinking>\s*", "", routing_text)
                normalized_content = re.sub(r"(?is)<reflection>\s*.*?\s*</reflection>\s*", "", normalized_content).strip()

                if normalized_content:
                    normalized_lower = normalized_content.lower()
                    has_task_goal_tag = has_task_goal_tag or ("<task_goal>" in normalized_lower)
                    has_tool_xml_hint = has_tool_xml_hint or any(
                        f"<{tool_name}" in normalized_lower for tool_name in self.tools.keys()
                    )

                # If there are no tool hints and no task-goal control tag, end this round directly.
                # This also prevents infinite loops for DeepSeek-style reasoning-only turns.
                ended_without_tools = (not has_task_goal_tag) and (not has_tool_xml_hint)

        return {
            "pending_tool_calls": tool_calls,
            "ended_without_tools": ended_without_tools,
            "completion_candidate": completion_candidate,
            "completion_candidate_result": completion_candidate_result,
        }


    def _extract_paper_summary(self, content: str, file_path: str) -> str:
        """从论文内容中提取摘要信息"""
        lines = content.split('\n')
        title = ""
        abstract = ""
        
        # 提取标题（通常在前几行的##开头）
        for line in lines[:30]:
            if line.strip().startswith('##') and 'abstract' not in line.lower():
                title = line.replace('#', '').strip()
                if len(title) > 10:  # 确保是有效标题
                    break
        
        # 提取摘要（寻找Abstract后的内容或DOI前的长段落）
        in_abstract = False
        for i, line in enumerate(lines):
            if 'abstract' in line.lower() or i < 20:  # 摘要通常在开头
                if len(line.strip()) > 200:  # 找到长段落作为摘要
                    abstract = line.strip()[:500]
                    break
            if 'DOI:' in line and abstract:
                break
        
        if not abstract:
            # 如果没找到，取前20行中最长的段落
            for line in lines[:20]:
                if len(line.strip()) > len(abstract):
                    abstract = line.strip()[:500]
        
        return f"标题: {title[:100]}\n摘要: {abstract[:300]}..."


    def _should_continue(self, state: AgentState) -> str:
        """
        路由决策 - 让LLM决定下一步流向
        
        Returns:
            "tools" - 有工具调用需要执行
            "agent" - 回到思考
            "verify" - 进入完成校验节点
            "end_direct" - 直接结束
        """
        iteration = state.get("iteration_count", 0)
        
        # 检查是否达到最大迭代次数
        if iteration >= self.max_iterations:
            self._emit("routing", f"[路由] 达到最大迭代次数 ({self.max_iterations})，强制结束执行")
            self._emit("warning", "[提示] 任务可能未完成，请检查任务复杂度或增加 max_iterations")
            return "end_direct"
        
        # 检查任务是否已完成
        if state.get("task_completed", False):
            self._emit("routing", "[路由] 任务已完成，结束执行")
            return "end_direct"
        
        # 让LLM决定下一步行动（通过last_action状态）
        last_action = state.get("last_action", "")
        pending_tools = state.get("pending_tool_calls", [])
        
        # 优先检查是否有工具待执行（工具执行优先级最高）
        if pending_tools:
            self._emit("routing", f"[路由] 有 {len(pending_tools)} 个工具待执行")
            return "tools"

        # 需要用户确认时，立即结束本轮并等待用户输入
        if state.get("needs_user_confirmation", False):
            self._emit("routing", "[路由] 需要用户确认，结束本轮等待用户回复")
            return "end_direct"

        # 检测到完成申请，进入完成校验
        if state.get("completion_candidate", False):
            self._emit("routing", "[路由] 检测到完成申请，进入完成校验")
            return "verify"

        # 无工具调用且已给出文本答复时，直接结束，避免重复空转迭代
        if state.get("ended_without_tools", False):
            if (state.get("verification_decision", "") or "").strip().lower() == "retry":
                self._emit("routing", "[路由] 上次完成校验为 retry，本轮继续执行")
                return "agent"
            has_progress = bool((state.get("progress_summary", "") or "").strip())
            if has_progress:
                self._emit("routing", "[路由] 无工具调用答复，但存在执行进度，继续思考")
                return "agent"
            self._emit("routing", "[路由] 无工具调用且已给出文本答复，直接结束")
            return "end_direct"
        
        if last_action == "use_tool":
            # LLM决定使用工具但没有解析到工具
            self._emit("routing", "[路由] LLM决定使用工具但没有解析到工具，回到思考")
            return "agent"
        elif last_action == "think_more":
            # LLM决定继续思考
            self._emit("routing", "[路由] LLM决定继续思考")
            return "agent"
        elif last_action == "complete_candidate":
            self._emit("routing", "[路由] 工具执行后提交完成申请，进入完成校验")
            return "verify"
        elif last_action == "clarify":
            self._emit("routing", "[路由] 需要用户澄清，结束本轮等待用户回复")
            return "end_direct"
        elif last_action == "complete":
            # LLM决定任务完成
            self._emit("routing", "[路由] LLM决定任务完成")
            return "end_direct"
        else:
            # 默认行为：继续思考
            self._emit("routing", "[路由] 默认路由：无工具调用，继续思考")
            return "agent"
