"""
Klynx Agent - LangGraph StateGraph
实现 OODA 循环的核心图结构
"""

import os
import platform
import re
import time
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple

from langchain_core.messages import AIMessage, HumanMessage, RemoveMessage, SystemMessage
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import END, StateGraph

from klynx.agent.browser import BrowserManager
from klynx.agent.mcp_manager import MCPManager
from klynx.agent.terminal import TerminalManager
from klynx.agent.tools import ToolRegistry, XMLParser, get_json_schemas
from klynx.agent.tui import TUIManager
from klynx.agent.web_search import WebSearchTool
from kbase.kb_manager import KBManager

from .nodes import NodesMixin
from .prompt_builder import PromptBuilderMixin
from .state import AgentState
from .tool_dispatch import ToolDispatchMixin


class KlynxAgent(PromptBuilderMixin, NodesMixin, ToolDispatchMixin):
    """Klynx Agent main class."""

    DEFAULT_SKILLS_ROOT = os.path.join(
        os.path.expanduser(os.environ.get("KLYNX_HOME", "~/.klynx")),
        "skills",
    )
    DEFAULT_BASIC_SKILLS = ("skill-creator", "skill-installer")

    BASE_TOOLS = {
        "read_file": "读取文件内容，支持指定行范围：read_file(path: str, start_line: int=None, end_line: int=None)。可以使用 <lines>10-20</lines> 替代 start_line/end_line 参数。",
        "write_to_file": "将内容写入文件（覆盖或新建）：write_to_file(path: str, content: str)",
        "replace_in_file": "使用 SEARCH/REPLACE 块局部替换文件内容：replace_in_file(path: str, diff_string: str)。兼容别名参数 diff。请严格使用标准的上下文diff格式（带 - 和 + 前缀）或 SEARCH/REPLACE 块格式提供差异内容。",
        "execute_command": "执行系统命令（Windows 环境：dir, type, copy 等）：execute_command(command: str)",
        "list_directory": "列出目录树结构，支持指定递归深度：list_directory(path: str='.', depth: int=2)",
        "create_directory": "创建目录（包括父目录）：create_directory(path: str)",
        "preview_file": "预览文件前 N 行内容：preview_file(path: str, num_lines: int=50)",
        "search_in_files": "在文件中搜索匹配内容（类似grep，支持正则）：search_in_files(pattern: str, path: str='.', file_pattern: str='*', case_insensitive: bool=True, is_regex: bool=False, max_results: int=50, context_lines: int=0)。定位代码位置后，再用 read_file 精确阅读。",
        "create_terminal": "创建终端会话：create_terminal(name: str)。【重要】仅用于后台批量执行非交互式命令。",
        "run_in_terminal": "在终端执行命令：run_in_terminal(name: str, command: str)",
        "read_terminal": "读取终端输出：read_terminal(name: str, lines: int)",
        "check_syntax": "检查代码语法：check_syntax(path: str)",
        "open_tui": "启动 TUI 应用：open_tui(name: str, command: str, rows: int, cols: int)",
        "read_tui": "读取 TUI 屏幕快照：read_tui(name: str)",
        "send_keys": "向 TUI 发送按键：send_keys(name: str, keys: str)",
        "close_tui": "关闭 TUI 会话：close_tui(name: str)",
        "activate_tui_mode": "激活 TUI 交互模式：加载 TUI 操作指南到系统提示。在使用 open_tui 等 TUI 工具前必须先调用此工具。",
        "launch_interactive_session": "一键启动交互式会话（推荐）：自动激活 TUI 模式并启动应用。参数：command (如 'python', 'vim test.py')。【重要】如需启动交互式环境（如 Python REPL, Node, Vim）或查看实时屏幕输出，请优先使用本工具。",
        "web_search": "联网搜索：web_search(query: str, max_results: int=5, search_depth: str='basic')。【重要】当你需要搜索任何信息（最新动态、技术文档、API用法、新闻、事实核查等），都必须使用此工具进行搜索，严禁凭记忆编造。可设 search_depth='advanced' 进行深度搜索。",
        "query_knowledge": "查询向量知识库：query_knowledge(query: str, top_k: int=5, kb_name: str=None)。【重要】当用户询问专业领域知识时，优先使用此工具查询本地向量知识库获取精准的专业内容。可用 list_knowledge_collections 查看可用集合。",
        "list_knowledge_collections": "列出所有已添加的知识库及其集合：list_knowledge_collections(kb_name: str=None)",
        "load_skill": "加载已安装技能包的 SKILL.md 内容到上下文：load_skill(name: str)。当你需要使用某个 skills 包中的工作流时，先调用此工具。",
        "browser_open": "打开浏览器并访问 URL：browser_open(url: str)。【浏览器调试】常用于验证前端界面或获取动态网页内容。后续配合 browser_view 查看内容、browser_act 操作元素、browser_screenshot 截图。",
        "browser_view": "获取页面内容：browser_view(selector: str=None)。不传 selector 则返回页面摘要。",
        "browser_act": "操作页面元素：browser_act(action: str, selector: str, value: str=None)。如果不确定 action，就使用 click。action 支持 click, type, press, hover。",
        "browser_scroll": "滚动页面：browser_scroll(direction: str='down', amount: int=None)。用于查看视口外的内容。",
        "browser_screenshot": "截图并保存：browser_screenshot()。返回截图路径。",
        "browser_console_logs": "获取浏览器控制台日志：browser_console_logs()"
    }


    def __init__(self, working_dir: str = ".", model=None, max_iterations: int = 1000,
                 memory_dir: str = "", load_project_docs: bool = True,
                 os_name: str = "windows", browser_headless: bool = False,
                 tool_call_mode: str = "native", skills: Optional[List[Any]] = None,
                 skills_root: str = ""):
        """
        初始化 Klynx Agent
        
        Args:
            working_dir: 工作目录
            model: LangChain 模型实例（需设置 max_context_tokens 属性）
            max_iterations: 最大迭代次数，防止无限循环
            memory_dir: Agent 记忆目录路径（.klynx/.rules/.memory 的存储位置），为空则不加载记忆
            load_project_docs: 是否递归加载工作目录下的 KLYNX.md 项目文档，默认 True
            os_name: 操作系统名称，用于给 Agent 提供环境提示 (例如: windows, linux, macos)
            browser_headless: 浏览器工具是否运行在无头模式，默认为 False (显示界面)
            tool_call_mode: 工具调用模式，"native" 使用 LiteLLM 原生 Function Calling，
                           "xml" 使用传统 XML 文本解析（兼容小模型/不支持 FC 的模型）
            skills: Agent 启动时可安装的技能配置。支持：
                    - ["skill-name", "path/to/skill"]
                    - [("name", "path", "description")]
                    - [{"name": "...", "path": "...", "description": "..."}]
            skills_root: 技能根目录，支持传入技能名称时在该目录下解析
        """
        self.working_dir = os.path.abspath(working_dir)
        self.model = model
        self.max_iterations = max_iterations
        self.memory_dir = os.path.abspath(memory_dir) if memory_dir else ""
        self.load_project_docs = load_project_docs
        self.os_name = os_name
        self.tool_call_mode = tool_call_mode  # "native" | "xml"
        
        # 自动回退：如果模型明确是 o1 或 reasoner，强制使用 xml 模式（因不支持 Native FC）
        if self.model and hasattr(self.model, "model"):
            model_name_lower = self.model.model.lower()
            if "reasoner" in model_name_lower or "o1" in model_name_lower:
                self.tool_call_mode = "xml"
        
        # 终端管理器
        self.terminal_manager = TerminalManager(working_dir)
        # TUI 管理器
        self.tui_manager = TUIManager(working_dir)
        # 联网搜索工具
        self.web_search_tool = WebSearchTool()
        # 知识库查询工具
        self.kb_manager = KBManager()
        # 浏览器管理工具
        self.browser_manager = BrowserManager(headless=browser_headless)
        # 从模型对象读取上下文窗口大小，默认 128k
        self.max_context_tokens = getattr(model, 'max_context_tokens', 128000)
        
        # MCP (Model Context Protocol) 管理器
        self.mcp_manager = MCPManager()
        
        # 流式输出回调函数
        self.streaming_callback = None
        
        # 初始化组件
        self.xml_parser = XMLParser()
        
        # 工具系统：已加载的工具（名称 -> 说明）和外部工具函数（名称 -> callable）
        self.tools: Dict[str, str] = {}
        self.external_tool_funcs: Dict[str, callable] = {}
        
        # 预加载的工具提示词缓存
        self._tool_prompts_cache: str = ""
        
        # Native Function Calling 用的 JSON Schema 缓存
        self._json_schemas: list = []

        # 技能系统：注册信息和提示词缓存
        if skills_root:
            self.skills_root = os.path.abspath(os.path.expanduser(skills_root))
        else:
            self.skills_root = os.path.abspath(os.path.expanduser(self.DEFAULT_SKILLS_ROOT))
        self.builtin_skills_root = str(
            (Path(__file__).resolve().parent.parent / "skills" / "system").resolve()
        )
        self.basic_skills_enabled = True
        self.skill_registry: Dict[str, Dict[str, str]] = {}
        self._skills_prompt_cache: str = ""
        self._skill_body_cache: Dict[str, str] = {}
        
        # 回滚目标配置（存储 checkpoint config，供 stream 使用）
        self._rollback_config = None
        
        # 事件缓冲区（流式输出用）
        self._event_buffer: List[Dict[str, Any]] = []
        
        # 设置工具的工作目录
        ToolRegistry.set_working_dir(self.working_dir)

        # 预加载内置技能（可通过 basic_skills("off") 关闭）
        self._load_builtin_skills()

        # 预加载技能目录（可选）
        if skills:
            if isinstance(skills, str):
                self.add_skills(skills)
            else:
                self.add_skills(*skills)
        
        # 构建图
        self.workflow = self._build_graph()
        # 使用 MemorySaver 作为 checkpointer 来保存状态
        self.memory = MemorySaver()
        self.app = self.workflow.compile(checkpointer=self.memory)


    def _refresh_tool_prompts(self):
        """刷新工具提示词缓存 & Native Function Calling Schema 缓存"""
        if not self.tools:
            self._tool_prompts_cache = ""
            self._json_schemas = []
            return
        
        # 始终刷新 Native FC Schema 缓存（即使当前模式是 xml，也提前准备好以支持动态切换）
        external_descs = {name: desc for name, desc in self.tools.items() if name in self.external_tool_funcs}
        self._json_schemas = get_json_schemas(
            list(self.tools.keys()),
            external_tools=external_descs,
            external_tool_funcs=self.external_tool_funcs,
        )
        
        if self.tool_call_mode == "native":
            # Native 模式：工具定义已通过 API tools 参数传递，prompt 中只做简略提示
            lines = []
            lines.append("\n<tool_usage_guide>")
            lines.append("  <description>你可以直接调用工具函数来完成任务。工具列表已通过 API 提供。当任务完成时，请**必须**调用 `attempt_completion` 工具函数并传递 `result` 参数来结束任务。</description>")
            lines.append("  <note>在调用原生工具的同时，你可以并且应该继续在文本回复中输出 `<thinking>` 或 `<reflection>` 等辅助 XML 标签。若需更新目标，请调用 `update_task_state` 工具。</note>")
            lines.append("</tool_usage_guide>")
            self._tool_prompts_cache = "\n".join(lines)
        else:
            # XML 模式：保持原来的详细工具列表
            lines = []
            lines.append("\n<tool_usage_guide>")
            lines.append("  <description>你可以使用以下工具来完成任务。使用 XML 格式调用工具。</description>")
            lines.append("  <available_tools>")
            for name, desc in self.tools.items():
                lines.append(f'    <tool name="{name}">{desc}</tool>')
            lines.append("  </available_tools>")
            lines.append("</tool_usage_guide>")
            lines.append("<completion_format>")
            lines.append("  &lt;attempt_completion&gt;&lt;result&gt;任务结果说明&lt;/result&gt;&lt;/attempt_completion&gt;")
            lines.append("</completion_format>")
            self._tool_prompts_cache = "\n".join(lines)

    def _find_skill_md_path(self, skill_dir: Path) -> Optional[Path]:
        """在技能目录中查找 SKILL.md 文件（大小写不敏感）。"""
        if not skill_dir.exists() or not skill_dir.is_dir():
            return None

        direct = skill_dir / "SKILL.md"
        if direct.exists() and direct.is_file():
            return direct

        for child in skill_dir.iterdir():
            if child.is_file() and child.name.lower() == "skill.md":
                return child
        return None

    def _extract_skill_frontmatter(self, content: str) -> Dict[str, str]:
        """从 SKILL.md frontmatter 提取 name 和 description。"""
        text = (content or "").lstrip("\ufeff")
        lines = text.splitlines()
        if not lines or lines[0].strip() != "---":
            return {}

        end_idx = None
        for i in range(1, len(lines)):
            if lines[i].strip() == "---":
                end_idx = i
                break
        if end_idx is None:
            return {}

        frontmatter = "\n".join(lines[1:end_idx])
        name_match = re.search(r"(?m)^name:\s*(.+?)\s*$", frontmatter)
        desc_match = re.search(r"(?m)^description:\s*(.+?)\s*$", frontmatter)

        parsed: Dict[str, str] = {}
        if name_match:
            parsed["name"] = name_match.group(1).strip().strip("'\"")
        if desc_match:
            parsed["description"] = desc_match.group(1).strip().strip("'\"")
        return parsed

    def _resolve_skill_dir(self, skill_ref: str) -> Path:
        """将技能引用解析为目录路径。支持名称、目录路径或 SKILL.md 文件路径。"""
        if not isinstance(skill_ref, str) or not skill_ref.strip():
            raise ValueError("技能引用不能为空")

        raw_ref = skill_ref.strip()
        path_ref = Path(raw_ref).expanduser()
        candidate_dirs = []

        if path_ref.is_absolute():
            candidate_dirs.append(path_ref)
        else:
            candidate_dirs.append(Path(self.working_dir) / path_ref)
            candidate_dirs.append(Path(self.skills_root) / path_ref)

        for candidate in candidate_dirs:
            if candidate.is_file() and candidate.name.lower() == "skill.md":
                return candidate.parent.resolve()
            if candidate.is_dir() and self._find_skill_md_path(candidate):
                return candidate.resolve()

        # 回退：允许以技能名匹配 skills_root 下的任意子目录（例如 .system/skill-creator）
        skills_root_path = Path(self.skills_root)
        if skills_root_path.exists() and skills_root_path.is_dir():
            for found in skills_root_path.rglob("*"):
                if not found.is_dir() or found.name != raw_ref:
                    continue
                if self._find_skill_md_path(found):
                    return found.resolve()

        # 回退：允许以内置技能名匹配 package 内置技能目录
        builtin_root_path = Path(self.builtin_skills_root)
        if builtin_root_path.exists() and builtin_root_path.is_dir():
            for found in builtin_root_path.rglob("*"):
                if not found.is_dir() or found.name != raw_ref:
                    continue
                if self._find_skill_md_path(found):
                    return found.resolve()

        raise FileNotFoundError(f"未找到技能目录或 SKILL.md: {skill_ref}")

    def _iter_skill_specs(self, *skills):
        """
        将 add_skills 的多种入参规范化为统一描述结构。

        支持：
        1) "skill-name" / "path/to/skill"
        2) ("name", "path") / ("name", "path", "description")
        3) {"name": "...", "path": "...", "description": "...", "source": "..."}
        4) add_skills("name", "path", "description")  # 兼容用户习惯写法
        """
        if len(skills) == 1 and isinstance(skills[0], (list, tuple, set)):
            skills = tuple(skills[0])

        if not skills:
            raise ValueError("add_skills 需要至少一个技能引用")

        # 兼容三参数调用：add_skills(name, path, description)
        if (
            len(skills) in (2, 3)
            and all(isinstance(item, str) for item in skills)
            and (
                os.path.exists(os.path.expanduser(skills[1]))
                or any(ch in skills[1] for ch in ("/", "\\", ":"))
            )
        ):
            maybe_name = skills[0].strip()
            maybe_path = skills[1].strip()
            maybe_desc = skills[2].strip() if len(skills) == 3 else ""
            return [
                {
                    "name": maybe_name,
                    "path": maybe_path,
                    "description": maybe_desc,
                    "source": "external",
                }
            ]

        normalized = []
        for item in skills:
            if isinstance(item, str):
                normalized.append(
                    {
                        "name": "",
                        "path": item,
                        "description": "",
                        "source": "external",
                    }
                )
                continue

            if isinstance(item, (tuple, list)):
                if len(item) not in (2, 3):
                    raise ValueError(f"无效技能元组: {item}")
                if not all(isinstance(x, str) for x in item):
                    raise ValueError(f"技能元组必须全部为字符串: {item}")
                normalized.append(
                    {
                        "name": item[0].strip(),
                        "path": item[1].strip(),
                        "description": item[2].strip() if len(item) == 3 else "",
                        "source": "external",
                    }
                )
                continue

            if isinstance(item, dict):
                raw_name = str(item.get("name", "") or "").strip()
                raw_path = str(item.get("path", "") or item.get("skill", "") or "").strip()
                if not raw_path:
                    raise ValueError(f"技能配置缺少 path: {item}")
                raw_desc = str(item.get("description", "") or "").strip()
                raw_source = str(item.get("source", "external") or "external").strip()
                normalized.append(
                    {
                        "name": raw_name,
                        "path": raw_path,
                        "description": raw_desc,
                        "source": raw_source,
                    }
                )
                continue

            raise ValueError(f"无效技能引用: {item}")

        return normalized

    def _discover_skill_dirs(self, root_dir: Path) -> List[Path]:
        """从根目录递归发现包含 SKILL.md 的技能目录。"""
        if not root_dir.exists() or not root_dir.is_dir():
            return []

        found_dirs = set()
        for md_name in ("SKILL.md", "skill.md"):
            for md_path in root_dir.rglob(md_name):
                if md_path.is_file():
                    found_dirs.add(md_path.parent.resolve())
        return sorted(found_dirs)

    def _sync_load_skill_tool(self):
        """按技能可用性自动维护 load_skill 工具。"""
        if self.skill_registry:
            if "load_skill" not in self.tools:
                self.tools["load_skill"] = self.BASE_TOOLS["load_skill"]
        else:
            self.tools.pop("load_skill", None)
        self._refresh_tool_prompts()

    def _load_builtin_skills(self):
        """加载内置技能（skill-creator / skill-installer）。"""
        if not self.basic_skills_enabled:
            return self

        specs = []
        for skill_name in self.DEFAULT_BASIC_SKILLS:
            skill_dir = Path(self.builtin_skills_root) / skill_name
            if skill_dir.exists() and skill_dir.is_dir():
                specs.append(
                    {
                        "name": skill_name,
                        "path": str(skill_dir),
                        "description": "",
                        "source": "builtin",
                    }
                )
            else:
                self._emit("warning", f"[Skills] 内置技能缺失: {skill_name} ({skill_dir})")

        if specs:
            self.add_skills(specs)
        return self

    def _load_memory_skills(self):
        """自动加载 memory_dir/.klynx/skills 下的技能目录。"""
        if not self.memory_dir:
            return self

        skills_root = Path(self.memory_dir) / ".klynx" / "skills"
        if not skills_root.exists() or not skills_root.is_dir():
            return self

        existing_md_paths = {
            str(item.get("skill_md_path", ""))
            for item in self.skill_registry.values()
            if item.get("skill_md_path")
        }
        specs = []
        for skill_dir in self._discover_skill_dirs(skills_root):
            skill_md = self._find_skill_md_path(skill_dir)
            if not skill_md:
                continue
            resolved_md = str(skill_md.resolve())
            if resolved_md in existing_md_paths:
                continue
            specs.append(
                {
                    "name": "",
                    "path": str(skill_dir),
                    "description": "",
                    "source": "memory",
                }
            )

        if specs:
            self.add_skills(specs)
            self._emit("info", f"[Skills] 已从 .klynx/skills 自动加载 {len(specs)} 个技能")
        return self

    def _refresh_skills_prompt(self):
        """刷新 skills 目录提示词缓存。"""
        if not self.skill_registry:
            self._skills_prompt_cache = ""
            return

        lines = []
        lines.append("<skills_registry>")
        lines.append("  <description>你可以使用已安装的技能包。先调用 `load_skill(name)`，再按加载出的 SKILL.md 工作流执行。</description>")
        lines.append("  <rules>")
        lines.append("    <rule>仅能使用下方已安装的技能名称，不要虚构技能。</rule>")
        lines.append("    <rule>需要技能细节时，必须先调用 load_skill 加载对应 SKILL.md。</rule>")
        lines.append("    <rule>加载后优先遵循 SKILL.md 中的步骤、脚本和 references 指引。</rule>")
        lines.append("  </rules>")
        lines.append("  <available_skills>")
        for name in sorted(self.skill_registry.keys()):
            item = self.skill_registry[name]
            skill_dir = item.get("skill_dir", "")
            skill_md = item.get("skill_md_path", "")
            source = item.get("source", "external")
            desc = item.get("description", "") or "（无描述）"
            lines.append(
                f'    <skill name="{self._escape_xml(name)}" dir="{self._escape_xml(skill_dir)}" '
                f'skill_md="{self._escape_xml(skill_md)}" source="{self._escape_xml(source)}">{self._escape_xml(desc)}</skill>'
            )
        lines.append("  </available_skills>")
        lines.append("</skills_registry>")
        self._skills_prompt_cache = "\n".join(lines)

    def add_skills(self, *skills):
        """
        安装技能目录到当前 Agent 实例。

        支持传入：
        1) 技能目录路径
        2) SKILL.md 文件路径
        3) 技能名称（会在 skills_root 下解析）
        4) (name, path) / (name, path, description)
        5) add_skills(name, path, description) 兼容写法
        """
        specs = self._iter_skill_specs(*skills)
        installed_names = []

        for spec in specs:
            skill_ref = spec.get("path", "")
            alias_name = spec.get("name", "").strip()
            alias_desc = spec.get("description", "").strip()
            source = spec.get("source", "external")

            skill_dir = self._resolve_skill_dir(skill_ref)
            skill_md = self._find_skill_md_path(skill_dir)
            if skill_md is None:
                raise FileNotFoundError(f"目录中未找到 SKILL.md: {skill_dir}")

            try:
                with open(skill_md, "r", encoding="utf-8", errors="ignore") as f:
                    content = f.read()
            except Exception as e:
                raise RuntimeError(f"读取技能文件失败: {skill_md} ({e})") from e

            frontmatter = self._extract_skill_frontmatter(content)
            skill_name = (alias_name or frontmatter.get("name") or skill_dir.name).strip()
            description = (alias_desc or frontmatter.get("description") or "").strip()

            existing = self.skill_registry.get(skill_name)
            if existing and existing.get("skill_md_path") != str(skill_md.resolve()):
                raise ValueError(
                    f"技能名冲突: {skill_name} 同时指向 {existing.get('skill_md_path')} 和 {skill_md}"
                )

            self.skill_registry[skill_name] = {
                "name": skill_name,
                "description": description,
                "skill_dir": str(skill_dir),
                "skill_md_path": str(skill_md.resolve()),
                "source": source,
            }
            installed_names.append(skill_name)
            self._emit("info", f"[Skills] 已安装技能: {skill_name} ({skill_md}) [{source}]")

        self._sync_load_skill_tool()
        self._refresh_skills_prompt()
        if installed_names:
            self._emit("info", f"[Skills] 共安装 {len(installed_names)} 个技能")
        return self

    def basic_skills(self, mode: str = "on"):
        """
        开关内置基础技能（skill-creator / skill-installer）。

        Args:
            mode: "on" 或 "off"
        """
        normalized = str(mode or "").strip().lower()
        if normalized in ("on", "true", "1", "enable", "enabled"):
            self.basic_skills_enabled = True
            self._load_builtin_skills()
            return self

        if normalized in ("off", "false", "0", "disable", "disabled"):
            self.basic_skills_enabled = False
            removed = []
            for name, meta in list(self.skill_registry.items()):
                if meta.get("source") == "builtin":
                    self.skill_registry.pop(name, None)
                    self._skill_body_cache.pop(name, None)
                    removed.append(name)
            self._sync_load_skill_tool()
            self._refresh_skills_prompt()
            if removed:
                self._emit("info", f"[Skills] 已禁用内置技能: {', '.join(sorted(removed))}")
            return self

        raise ValueError("basic_skills 仅支持 'on' 或 'off'")

    def get_skill_markdown(self, skill_name: str) -> Dict[str, Any]:
        """按技能名读取 SKILL.md 内容。"""
        if not isinstance(skill_name, str) or not skill_name.strip():
            available = ", ".join(sorted(self.skill_registry.keys())) or "(无)"
            return {
                "ok": False,
                "error": f"技能名为空。可用技能: {available}",
            }

        normalized = skill_name.strip()
        match_name = None
        if normalized in self.skill_registry:
            match_name = normalized
        else:
            for name in self.skill_registry.keys():
                if name.lower() == normalized.lower():
                    match_name = name
                    break

        if not match_name:
            available = ", ".join(sorted(self.skill_registry.keys())) or "(无)"
            return {
                "ok": False,
                "error": f"未安装技能: {normalized}。可用技能: {available}",
            }

        meta = self.skill_registry[match_name]
        if match_name in self._skill_body_cache:
            content = self._skill_body_cache[match_name]
        else:
            try:
                with open(meta["skill_md_path"], "r", encoding="utf-8", errors="ignore") as f:
                    content = f.read().strip()
                self._skill_body_cache[match_name] = content
            except Exception as e:
                return {
                    "ok": False,
                    "error": f"读取 SKILL.md 失败: {e}",
                }

        return {
            "ok": True,
            "name": match_name,
            "description": meta.get("description", ""),
            "skill_dir": meta.get("skill_dir", ""),
            "skill_md_path": meta.get("skill_md_path", ""),
            "content": content,
        }

    def add_tools(self, *args):
        """
        加载工具到 Agent
        
        支持三种调用方式：
        1. agent.add_tools("all")              - 加载所有基础工具
        2. agent.add_tools("read_file", "write_to_file")  - 加载指定基础工具
        3. agent.add_tools(my_func, "工具说明")  - 加载外部函数作为工具
        
        可多次调用以组合加载：
            agent.add_tools("read_file", "list_directory")
            agent.add_tools(my_analyzer, "分析代码结构并返回报告")
        """
        if not args:
            raise ValueError("add_tools 需要至少一个参数")
        
        # 模式1: add_tools("all") - 加载所有基础工具
        if len(args) == 1 and args[0] == "all":
            self.tools.update(self.BASE_TOOLS)
            self._sync_load_skill_tool()
            self._emit("info", f"[工具] 已加载全部 {len(self.BASE_TOOLS)} 个基础工具")
            self._refresh_tool_prompts()
            return self
        
        # 模式3: add_tools(callable, "说明") - 加载外部函数
        if len(args) == 2 and callable(args[0]) and isinstance(args[1], str):
            func, description = args
            tool_name = func.__name__
            self.tools[tool_name] = description
            self.external_tool_funcs[tool_name] = func
            self._emit("info", f"[工具] 已加载外部工具: {tool_name} - {description}")
            self._refresh_tool_prompts()
            return self
        
        # 模式2: add_tools("read_file", "write_to_file", ...) - 加载指定基础工具
        for name in args:
            if not isinstance(name, str):
                raise ValueError(f"无效参数: {name}，期望工具名称字符串或 (callable, str)")
            if name not in self.BASE_TOOLS:
                raise ValueError(f"未知基础工具: {name}，可用工具: {', '.join(self.BASE_TOOLS.keys())}")
            self.tools[name] = self.BASE_TOOLS[name]
        self._sync_load_skill_tool()
        self._emit("info", f"[工具] 已加载 {len(args)} 个基础工具: {', '.join(args)}")
        self._refresh_tool_prompts()
        return self


    def add_mcp(self, mcp_json_path: str):
        """
        加载外部 MCP (Model Context Protocol) Server，
        并将它们暴露出的 Tools 挂载到 Agent 中。
        """
        self._emit("info", f"[MCP] 加载 MCP 配置文件: {mcp_json_path}")
        mcp_tools = self.mcp_manager.load_from_json(mcp_json_path)
        if mcp_tools:
            # MCP tools are returned as LangChain StructuredTools
            for tool in mcp_tools:
                self.add_tools(tool, tool.description)
            self._emit("info", f"[MCP] 成功挂载 {len(mcp_tools)} 个 MCP 工具")
        else:
            self._emit("warning", f"[MCP] 未从 {mcp_json_path} 加载到任何工具")
        return self


    def add_kb(self, name: str, path: str, embedding_model: str = None):
        """
        添加一个向量知识库到 Agent
        
        Args:
            name: 知识库名称（标识符，如 'my_papers'）
            path: ChromaDB chroma_store 目录路径
            embedding_model: 嵌入模型名称或本地路径（可选）
        
        Usage:
            agent.add_kb('papers', '/path/to/chroma_store')
            agent.add_kb('notes',  '/path/to/another_store')
        """
        self.kb_manager.add_kb(name, path, embedding_model)
        # 自动加载知识库工具（如果尚未加载）
        if "query_knowledge" not in self.tools:
            self.tools["query_knowledge"] = self.BASE_TOOLS["query_knowledge"]
            self.tools["list_knowledge_collections"] = self.BASE_TOOLS["list_knowledge_collections"]
            self._refresh_tool_prompts()
        return self


    def _build_graph(self) -> StateGraph:
        """Graph orchestration is defined by concrete agent classes in agents.py."""
        raise NotImplementedError("_build_graph must be implemented in libs/klynx/klynx/agent/agents.py")


    def _emit(self, event_type: str, content: str, **kwargs):
        """将事件存入缓冲区（替代 print）"""
        self._event_buffer.append({"type": event_type, "content": content, **kwargs})


    def ask(self, message: str, system_prompt: str = None, thread_id: str = "default"):
        """Ask flow is defined by concrete agent classes in agents.py."""
        raise NotImplementedError("ask must be implemented in libs/klynx/klynx/agent/agents.py")


    def invoke(
        self,
        task: str,
        thread_id: str = "default",
        thinking_context: bool = False,
        system_prompt_append: str = "",
    ):
        """Invoke flow is defined by concrete agent classes in agents.py."""
        raise NotImplementedError("invoke must be implemented in libs/klynx/klynx/agent/agents.py")


    def compact_context(self, thread_id: str = "default") -> tuple:
        """
        手动触发上下文压缩
        
        Returns:
            (status_message: str, summary_text: str or None)
        """
        config = {"configurable": {"thread_id": thread_id}}
        current_state = self.app.get_state(config)
        
        if not current_state or not current_state.values:
            return ("无有效上下文", None)
            
        state = current_state.values
        messages = state.get("messages", [])
        
        if not messages:
            return ("消息历史为空，无需压缩", None)
            
        # 强制执行总结
        try:
            self._emit("info", "[手动压缩] 开始压缩上下文...")
            result = self._summarize_context(state)
            
            if result:
                # 更新状态
                self.app.update_state(config, result)
                summary = result.get("context_summary", "")
                return (f"上下文压缩完成。摘要长度: {len(summary)} 字符", summary)
            else:
                return ("上下文压缩失败或无需压缩", None)
        except Exception as e:
            return (f"压缩过程出错: {e}", None)


    def get_context(self, thread_id: str = "default") -> dict:
        """
        获取指定会话的当前完整状态上下文
        
        封装了向 LangGraph MemorySaver 查询特定 thread_id 最新状态的逻辑，
        方便用户直接调用来获取对话历史、Token用量等数据。
        
        Args:
            thread_id: 会话线程 ID
            
        Returns:
            包含当前状态所有键值对的字典。如果会话不存在，返回空字典。
            常用键包括: "messages", "overall_goal", "total_tokens", "iteration_count" 等。
        """
        config = {"configurable": {"thread_id": thread_id}}
        current_state = self.app.get_state(config)
        return current_state.values if current_state else {}


    def get_history(self, thread_id: str = "default", limit: int = 20) -> list:
        """
        获取操作流历史记录
        
        从检查点历史中提取Agent的操作流（工具调用、用户输入），
        以便用户了解Agent做了什么并选择回滚目标。
        
        Args:
            thread_id: 会话线程 ID
            limit: 返回的最大历史记录数
            
        Returns:
            操作流列表，每项包含 index, checkpoint_id, action_summary 等信息
        """
        config = {"configurable": {"thread_id": thread_id}}
        
        history = []
        prev_msg_count = 0
        
        try:
            snapshots = list(self.app.get_state_history(config))
            
            for idx, state_snapshot in enumerate(snapshots):
                if idx >= limit:
                    break
                
                values = state_snapshot.values
                messages = values.get("messages", [])
                checkpoint_id = state_snapshot.config["configurable"].get("checkpoint_id", "")
                next_nodes = list(state_snapshot.next) if state_snapshot.next else []
                node_name = next_nodes[0] if next_nodes else "(结束)"
                
                # 提取本步骤新增的操作摘要
                action_summary = self._extract_action_summary(messages, prev_msg_count)
                prev_msg_count = len(messages)
                
                history.append({
                    "index": idx,
                    "checkpoint_id": checkpoint_id,
                    "node": node_name,
                    "message_count": len(messages),
                    "iteration": values.get("iteration_count", 0),
                    "action": action_summary,
                    "task_completed": values.get("task_completed", False),
                    "progress": values.get("progress_summary", "")  
                })
        except Exception as e:
            self._emit("error", f"[错误] 获取历史记录失败: {e}")
        
        # 反转顺序：从旧到新显示（get_state_history 返回最新在前）
        history.reverse()
        # 重新编号
        for i, item in enumerate(history):
            item["display_index"] = i
        
        return history


    def _extract_action_summary(self, messages: list, prev_count: int) -> str:
        """
        从新增的消息中提取操作摘要
        
        Args:
            messages: 当前所有消息
            prev_count: 上一个检查点的消息数
            
        Returns:
            操作摘要字符串
        """
        if not messages:
            return "初始化"
        
        # 获取新增的消息
        new_messages = messages[prev_count:] if prev_count < len(messages) else messages[-1:]
        
        actions = []
        for msg in new_messages:
            content = getattr(msg, 'content', str(msg))
            
            if isinstance(msg, HumanMessage):
                # 检查是否是工具结果
                if '<tool_result' in content:
                    import re
                    tool_match = re.search(r'tool="(\w+)"', content)
                    if tool_match:
                        tool_name = tool_match.group(1)
                        # 提取关键信息
                        if 'success' in content.lower():
                            actions.append(f"✓ {tool_name}")
                        elif 'error' in content.lower():
                            actions.append(f"✗ {tool_name}")
                        else:
                            actions.append(f"→ {tool_name}")
                elif content.startswith('[---'):
                    actions.append("─── 新一轮对话 ───")
                else:
                    # 用户输入
                    snippet = content[:60] + "..." if len(content) > 60 else content
                    actions.append(f"用户: {snippet}")
            elif isinstance(msg, AIMessage):
                # 从AI消息中提取工具调用
                import re
                tool_tags = ['read_file', 'write_to_file', 'execute_command', 
                            'search_in_files', 'replace_in_file', 'list_directory',
                            'create_directory', 'load_skill', 'attempt_completion']
                found_tools = []
                for tag in tool_tags:
                    if f'<{tag}' in content or f'<{tag}>' in content:
                        found_tools.append(tag)
                
                if found_tools:
                    actions.append(f"Agent调用: {', '.join(found_tools)}")
                elif '<attempt_completion>' in content:
                    actions.append("任务完成")
        
        return " | ".join(actions) if actions else "状态更新"


    def rollback(self, thread_id: str = "default", target_index: int = None) -> bool:
        """
        回滚到指定的检查点
        
        通过存储目标检查点的 config，在下次 stream 调用时恢复到该状态。
        不使用 update_state（会创建新检查点导致重复）。
        
        Args:
            thread_id: 会话线程 ID
            target_index: 目标检查点在 get_state_history 中的索引
            
        Returns:
            是否成功设置回滚
        """
        config = {"configurable": {"thread_id": thread_id}}
        
        try:
            snapshots = list(self.app.get_state_history(config))
            
            if not snapshots:
                self._emit("info", "[回滚] 暂无历史记录")
                return False
            
            if target_index is None:
                # 默认回退1步
                target_index = 1
            
            if target_index >= len(snapshots):
                self._emit("error", f"[回滚] 索引 {target_index} 超出历史范围 (共 {len(snapshots)} 条)")
                return False
            
            target_state = snapshots[target_index]
            target_values = target_state.values
            
            # 存储回滚配置，供下次 stream 使用
            self._rollback_config = target_state.config
            
            # 打印回滚信息
            msgs = target_values.get("messages", [])
            progress = target_values.get("progress_summary", "")
            
            self._emit("info", f"[回滚] 已设置回滚到检查点 (index={target_index})")
            self._emit("info", f"  消息数: {len(msgs)}")
            self._emit("info", f"  迭代: {target_values.get('iteration_count', 0)}")
            if progress:
                # 显示最后几行进度
                progress_lines = progress.strip().split('\n')
                for line in progress_lines[-3:]:
                    self._emit("info", f"  {line}")
            
            return True
            
        except Exception as e:
            self._emit("error", f"[回滚] 回滚失败: {e}")
            import traceback
            traceback.print_exc()
            return False


    def run_terminal_agent_stream(
        self,
        task: str,
        thread_id: str,
        system_prompt_append: str = "",
    ) -> dict:
        """Run invoke() and print streaming events in terminal."""
        from klynx.agent.package import run_terminal_agent_stream
        return run_terminal_agent_stream(
            self,
            task,
            thread_id,
            system_prompt_append=system_prompt_append,
        )


    def run_terminal_ask_stream(self, message: str, system_prompt: str = None, thread_id: str = "default") -> str:
        """运行 agent.ask()，流式打印回答，返回完整回答文本"""
        from klynx.agent.package import run_terminal_ask_stream
        return run_terminal_ask_stream(self, message, system_prompt, thread_id)



GraphKlynxAgent = KlynxAgent


def create_agent(working_dir: str = ".", model=None, max_iterations: int = 1000,
                 memory_dir: str = "", load_project_docs: bool = True,
                 os_name: str = platform.system(), browser_headless: bool = False,
                 append_system_prompt: str = "", skills: Optional[List[Any]] = None,
                 skills_root: str = "") -> KlynxAgent:
    """Create default Klynx agent instance."""
    from .agents import KlynxAgent as DefaultKlynxAgent

    return DefaultKlynxAgent(
        working_dir=working_dir,
        model=model,
        max_iterations=max_iterations,
        memory_dir=memory_dir,
        load_project_docs=load_project_docs,
        os_name=os_name,
        browser_headless=browser_headless,
        append_system_prompt=append_system_prompt,
        skills=skills,
        skills_root=skills_root,
    )
