"""
Agent 基类

BaseAgent 包含通用执行逻辑：
- 流式输出
- 工具调用和执行
- 权限检查
- 死循环检测
- 空响应重试

上下文管理委托给 Context 模块处理。

子类只需定义差异化配置：
- name: Agent 名称
- system_prompt: 系统提示词
- get_tools(): 返回允许使用的工具列表
- 可选覆盖各种配置属性
"""

from abc import ABC, abstractmethod
import asyncio
from typing import AsyncGenerator, List, Dict, Any, Optional, Callable, Awaitable
from collections import defaultdict
from pathlib import Path

from provider import (
    BaseModelClient,
    Message,
    MessageRole,
    ToolCall,
    ToolResult,
    ToolDefinition,
)
from context import BaseContext, DefaultContext, ContextConfig
from core.logger import log_event, log_tool_call, log_tool_result, log_error
from core.permission import (
    PermissionManager,
    PermissionDeniedError,
    PermissionRuleset,
    get_permission_manager,
)
from tools.base import BaseTool, ToolOutput
from tools.registry import ToolRegistry


# 延迟导入 EventType 以避免循环依赖
def _get_event_type():
    from server.constants import EventType
    return EventType


# 权限回调类型：返回 PermissionResponse
from core.permission import PermissionResponse
PermissionCallback = Callable[[str, str, Dict[str, Any]], Awaitable[PermissionResponse]]


def load_prompt(filename: str) -> str:
    """加载 Agent 提示词文件（从 assets/prompts/agents/ 目录）"""
    prompt_path = Path(__file__).parent.parent / "assets" / "prompts" / "agents" / filename
    if prompt_path.exists():
        return prompt_path.read_text(encoding="utf-8")
    return ""


class BaseAgent(ABC):
    """
    Agent 基类 - 包含通用执行逻辑

    子类需要实现：
    - name: Agent 名称
    - get_tools(): 返回允许使用的工具列表

    子类可选覆盖：
    - max_iterations: 最大工具调用迭代次数
    - doom_loop_threshold: 死循环检测阈值
    - doom_loop_history_size: 死循环检测历史大小
    - enable_permission_check: 是否启用权限检查
    - permission_ruleset: 权限规则集
    - empty_response_retry_prompt: 空响应重试提示
    - create_context(): 创建上下文（可自定义 Context 实现）
    """

    def __init__(
        self,
        client: BaseModelClient,
        tool_registry: ToolRegistry,
        permission_manager: Optional[PermissionManager] = None,
        permission_callback: Optional[PermissionCallback] = None,
    ):
        """
        初始化 Agent

        Args:
            client: 模型客户端
            tool_registry: 工具注册表
            permission_manager: 权限管理器
            permission_callback: 权限请求回调函数（用于询问用户）
        """
        self.client = client
        self.tool_registry = tool_registry
        self.permission_manager = permission_manager or get_permission_manager()
        self.permission_callback = permission_callback

        # 设置工具
        self._tools = self.get_tools(tool_registry)
        self._tool_map: Dict[str, BaseTool] = {}
        self._tool_definitions: List[ToolDefinition] = []
        self._setup_tools()

        # 上下文管理（按 thread_id 分组）
        self._contexts: Dict[str, BaseContext] = {}

        # 持久化配置（由 Session 设置）
        self._persistence_session_id: Optional[str] = None

        # 死循环检测历史
        self._tool_call_history: Dict[str, List[Dict[str, Any]]] = defaultdict(list)

    # ==================== 子类必须实现 ====================

    @property
    @abstractmethod
    def name(self) -> str:
        """Agent 名称"""
        pass

    @abstractmethod
    def get_tools(self, registry: ToolRegistry) -> List[BaseTool]:
        """
        返回该 Agent 允许使用的工具列表

        Args:
            registry: 工具注册表

        Returns:
            工具列表
        """
        pass

    # ==================== 子类可选覆盖 ====================

    @property
    def max_iterations(self) -> int:
        """最大工具调用迭代次数"""
        return 100

    @property
    def llm_stream_max_retries(self) -> int:
        """LLM 流式调用最大重试次数"""
        return 5

    @property
    def doom_loop_threshold(self) -> int:
        """死循环检测阈值（连续相同调用次数）"""
        return 3

    @property
    def doom_loop_history_size(self) -> int:
        """死循环检测历史大小"""
        return 10

    @property
    def enable_permission_check(self) -> bool:
        """是否启用权限检查"""
        return True

    @property
    def permission_ruleset(self) -> Optional[PermissionRuleset]:
        """权限规则集"""
        return None

    @property
    def enable_empty_response_retry(self) -> bool:
        """是否启用空响应重试"""
        return True

    @property
    def empty_response_retry_prompt(self) -> str:
        """空响应重试提示"""
        return "请根据上述工具执行结果，总结并回答用户的原始问题。"

    def set_persistence(self, session_id: str) -> None:
        """设置持久化（由 Session 调用）"""
        self._persistence_session_id = session_id

    def create_context(self) -> BaseContext:
        """
        创建上下文实例

        自动根据模型获取上下文窗口大小。
        构造 chat_fn / count_tokens_fn 闭包捕获 self，模型切换后自动解析到新 client。
        子类可覆盖此方法以使用不同的 Context 实现。

        Returns:
            BaseContext 实例
        """
        from provider.models import get_context_window
        max_tokens = get_context_window(self.client.model)

        # 闭包捕获 self（不是 self.client），
        # 当 self.client 被替换（模型切换）时自动解析到新 client
        async def chat_fn(messages, system_prompts=None, tools=None):
            async for chunk in self.client.chat_stream(
                messages=messages,
                system_prompts=system_prompts,
                tools=tools,
            ):
                yield chunk

        # token 计数闭包：捕获 self 以动态获取当前 client 和工具定义
        async def count_tokens_fn(messages, system_prompts=None):
            return await self.client.count_tokens(
                messages=messages,
                system_prompts=system_prompts,
                tools=self._tool_definitions if self._tools else None,
            )

        return DefaultContext(
            chat_fn=chat_fn,
            config=ContextConfig(max_tokens=max_tokens),
            session_id=self._persistence_session_id,
            count_tokens_fn=count_tokens_fn,
        )

    # ==================== 上下文管理 ====================

    async def build_system_prompt(self) -> List[Message]:
        """
        使用 SystemPrompt.build() 构建完整的分层系统提示词

        返回 Message 列表，role 为 PROVIDER/AGENT/ENVIRONMENT/CUSTOM。
        """
        from core.prompt import get_system_prompt
        builder = get_system_prompt()
        parts = await builder.build(
            provider=self.client.provider,
            model=self.client.model,
            agent=self.name,
        )
        return parts

    async def _get_context(self, thread_id: str) -> BaseContext:
        """获取或创建指定线程的上下文"""
        if thread_id not in self._contexts:
            context = self.create_context()
            prompts = await self.build_system_prompt()
            context.set_system_prompt(prompts)

            # 尝试从磁盘恢复历史
            if self._persistence_session_id and hasattr(context, 'load_from_disk'):
                await context.load_from_disk()

            self._contexts[thread_id] = context
        return self._contexts[thread_id]

    # ==================== 工具设置 ====================

    def _setup_tools(self):
        """设置工具映射"""
        for tool in self._tools:
            self._tool_map[tool.name] = tool
            self._tool_definitions.append(ToolDefinition(
                name=tool.name,
                description=tool.description,
                parameters=tool.args_schema.model_json_schema(),
            ))

    def _refresh_tools(self) -> None:
        """每次调用 LLM 前刷新工具列表（动态 MCP 工具可见）"""
        self._tools = self.get_tools(self.tool_registry)
        self._tool_map.clear()
        self._tool_definitions.clear()
        self._setup_tools()

    # ==================== 中断检查 ====================

    @staticmethod
    def _is_cancelled(cancel_event: Optional[asyncio.Event]) -> bool:
        """检查是否已请求中断"""
        return cancel_event is not None and cancel_event.is_set()

    # ==================== 死循环检测 ====================

    def _detect_doom_loop(self, tool_call: ToolCall, thread_id: str) -> bool:
        """检测死循环（连续相同的工具调用）"""
        history = self._tool_call_history[thread_id]

        # 添加当前调用
        call_signature = {
            "tool": tool_call.name,
            "args": str(tool_call.arguments),
        }
        history.append(call_signature)

        # 限制历史大小
        if len(history) > self.doom_loop_history_size:
            history.pop(0)

        # 检测连续相同调用
        if len(history) >= self.doom_loop_threshold:
            recent = history[-self.doom_loop_threshold:]
            first = recent[0]
            if all(
                call["tool"] == first["tool"] and
                call["args"] == first["args"]
                for call in recent
            ):
                log_event(
                    "doom_loop_detected",
                    thread_id=thread_id,
                    tool=first["tool"],
                    consecutive_count=self.doom_loop_threshold,
                )
                return True

        return False

    # ==================== 权限检查 ====================

    async def _check_tool_permission(
        self,
        tool_name: str,
        args: Dict[str, Any],
        thread_id: str,
    ) -> None:
        """
        检查工具调用权限

        调用 PermissionManager.check() 获取评估结果，
        如果结果是 ASK，则调用 permission_callback 询问用户，
        并处理 remember 持久化。
        """
        if not self.enable_permission_check:
            return

        # 构建 PermissionRequest
        from core.permission import PermissionRequest, PermissionAction
        pattern = self._build_permission_pattern(tool_name, args)
        request = PermissionRequest(
            permission=tool_name,
            patterns=[pattern],
            session_id=thread_id,
            metadata={"args": args},
        )

        # 调用 PermissionManager.check() 获取评估结果
        action = await self.permission_manager.check(
            request,
            agent_ruleset=self.permission_ruleset,
        )

        # 根据结果处理
        if action == PermissionAction.ALLOW:
            return

        if action == PermissionAction.DENY:
            raise PermissionDeniedError(
                permission=tool_name,
                pattern=pattern,
                message=f"Permission denied by rule: {tool_name}"
            )

        # ASK: 调用 permission_callback 询问用户
        if action == PermissionAction.ASK:
            if self.permission_callback:
                response = await self.permission_callback(tool_name, pattern, args)

                # 处理 remember 持久化
                if response.remember:
                    pm = self.permission_manager
                    remember_action = PermissionAction.ALLOW if response.approved else PermissionAction.DENY
                    raw_pattern = response.remember_pattern or pattern
                    broad_pattern = pm._generate_broad_pattern(tool_name, raw_pattern)
                    pm.remember_choice(tool_name, broad_pattern, remember_action)

                if not response.approved:
                    raise PermissionDeniedError(
                        permission=tool_name,
                        pattern=pattern,
                        message=f"Permission denied by user: {tool_name}"
                    )
            else:
                # 没有回调，默认拒绝
                raise PermissionDeniedError(
                    permission=tool_name,
                    pattern=pattern,
                    message=f"Permission denied (no callback): {tool_name}"
                )

    def _build_permission_pattern(self, tool_name: str, args: Dict[str, Any]) -> str:
        """构建权限检查的目标模式"""
        if tool_name in ["write", "edit"]:
            return args.get("file_path", args.get("path", "*"))
        elif tool_name == "bash":
            return args.get("command", "*")
        else:
            return "*"

    # ==================== Skill 消息清理 ====================

    def _cleanup_skill_history(self, thread_id: str) -> None:
        """清理上下文中的 skill 加载记录（assistant 的 skill tool_calls + 对应的 tool 结果）"""
        context = self._contexts.get(thread_id)
        if not context or not context._messages:
            return

        skill_call_ids = {
            tc.id for msg in context._messages
            for tc in msg.tool_calls if tc.name == "skill"
        }
        if not skill_call_ids:
            return

        new_messages = []
        for msg in context._messages:
            # 移除 skill 的 tool result
            if msg.tool_result and msg.tool_result.tool_call_id in skill_call_ids:
                continue
            # 移除 assistant 消息中的 skill tool_calls
            if msg.tool_calls:
                filtered = [tc for tc in msg.tool_calls if tc.name != "skill"]
                if len(filtered) != len(msg.tool_calls):
                    if filtered:
                        new_messages.append(Message(role=msg.role, content=msg.content, tool_calls=filtered))
                    continue
            new_messages.append(msg)

        removed = len(context._messages) - len(new_messages)
        context._messages = new_messages
        log_event("skill_history_cleaned", thread_id=thread_id, removed_count=removed)

    # ==================== 工具执行 ====================

    async def _execute_tool(
        self,
        tool_call: ToolCall,
        thread_id: str,
        cancel_event: Optional[asyncio.Event] = None,
    ) -> AsyncGenerator[Dict[str, Any], None]:
        """
        执行单个工具调用，包含权限检查、事件发送和日志记录

        Yields:
            事件字典，最后一个事件的 type 为 "tool_result"，包含执行结果
        """
        EventType = _get_event_type()

        # 权限检查（在执行工具之前）
        try:
            await self._check_tool_permission(tool_call.name, tool_call.arguments, thread_id)
        except PermissionDeniedError as e:
            # 权限被拒绝，不发送任何工具事件，直接返回结果
            result = ToolResult(
                tool_call_id=tool_call.id,
                name=tool_call.name,
                content=f"Permission denied: {str(e)}",
                is_error=True,
            )
            log_tool_result(
                tool_name=tool_call.name,
                result=result.content,
                call_id=tool_call.id,
                is_error=True,
            )
            yield {"type": "tool_result", "result": result}
            return

        # 权限通过，发送工具开始事件
        yield {
            "type": EventType.TOOL_CALL_START,
            "data": {
                "tool": tool_call.name,
                "args": tool_call.arguments,
                "call_id": tool_call.id,
            },
        }

        log_tool_call(
            tool_name=tool_call.name,
            tool_input=tool_call.arguments,
            call_id=tool_call.id,
            session_id=thread_id,
        )

        # 执行工具
        tool = self._tool_map.get(tool_call.name)
        if not tool:
            result = ToolResult(
                tool_call_id=tool_call.id,
                name=tool_call.name,
                content=f"Error: Tool '{tool_call.name}' not found",
                is_error=True,
            )
        else:
            try:
                # 如果有 cancel_event，竞争执行：工具 vs 取消
                if cancel_event is not None and not cancel_event.is_set():
                    tool_task = asyncio.ensure_future(tool.run(**tool_call.arguments))
                    cancel_waiter = asyncio.ensure_future(cancel_event.wait())
                    done, pending = await asyncio.wait(
                        {tool_task, cancel_waiter},
                        return_when=asyncio.FIRST_COMPLETED,
                    )
                    for p in pending:
                        p.cancel()
                        try:
                            await p
                        except (asyncio.CancelledError, Exception):
                            pass

                    if tool_task in done:
                        raw_result = tool_task.result()
                    else:
                        # cancel_event 先触发，工具被取消
                        raw_result = "[中断] 工具调用被用户中断"
                elif cancel_event is not None and cancel_event.is_set():
                    # 已经取消，直接返回
                    raw_result = "[中断] 工具调用被用户中断"
                else:
                    raw_result = await tool.run(**tool_call.arguments)

                if isinstance(raw_result, ToolOutput):
                    content = raw_result.content
                    tool_metadata = raw_result.metadata or {}
                else:
                    content = str(raw_result)
                    tool_metadata = {}

                # 统一截断/溢出到磁盘，防止上下文溢出
                # 跳过已自行处理截断的工具（参考 opencode 的 metadata.truncated 检查）
                if "truncated" not in tool_metadata:
                    from tools.truncation import truncate_and_spill
                    tr = truncate_and_spill(content, tool_name=tool_call.name)
                    content = tr.preview
                    if tr.truncated:
                        tool_metadata = dict(tool_metadata)
                        tool_metadata.update({
                            "truncated": True,
                            "output_path": tr.output_path,
                            "original_bytes": tr.original_bytes,
                            "preview_bytes": tr.preview_bytes,
                        })

                result = ToolResult(
                    tool_call_id=tool_call.id,
                    name=tool_call.name,
                    content=content,
                    is_error=False,
                    metadata=tool_metadata or None,
                )
            except Exception as e:
                result = ToolResult(
                    tool_call_id=tool_call.id,
                    name=tool_call.name,
                    content=f"Error: {str(e)}",
                    is_error=True,
                )

        log_tool_result(
            tool_name=tool_call.name,
            result=result.content,
            call_id=tool_call.id,
            is_error=result.is_error,
        )

        # 发送工具结束事件
        result_preview = result.content[:500] if len(result.content) > 500 else result.content
        # 获取工具返回的额外元数据（如 edit 工具的 start_line）
        event_args = dict(tool_call.arguments)
        if result.metadata:
            event_args.update(result.metadata)
        yield {
            "type": EventType.TOOL_CALL_END,
            "data": {
                "tool": tool_call.name,
                "call_id": tool_call.id,
                "args": event_args,
                "result": result_preview,
                "success": not result.is_error,
            },
        }

        # 返回结果（内部事件，不会发送给客户端）
        yield {"type": "tool_result", "result": result}

    # ==================== 主执行流程 ====================

    async def run_stream(
        self,
        user_input: str,
        thread_id: str = "default",
        cancel_event: Optional[asyncio.Event] = None,
    ) -> AsyncGenerator[Dict[str, Any], None]:
        """
        流式执行 Agent

        Args:
            user_input: 用户输入
            thread_id: 线程 ID

        Yields:
            事件字典，包含 type 和 data
        """
        # 延迟导入 EventType 以避免循环依赖
        EventType = _get_event_type()

        log_event(
            "agent_start",
            agent_name=self.name,
            thread_id=thread_id,
        )

        # 获取上下文
        context = await self._get_context(thread_id)

        # 添加用户消息到上下文（Context 内部自动检查压缩）
        context.add_message(Message(role=MessageRole.USER, content=user_input))

        # 获取消息列表
        messages = context.get_messages()

        streamed_content = ""
        tool_calls_made = False
        iteration = 0

        try:
            while iteration < self.max_iterations:
                iteration += 1

                # 中断检查点 1：每轮迭代开始
                if self._is_cancelled(cancel_event):
                    break

                # 动态刷新工具（每轮调用都从 registry 获取）
                self._refresh_tools()

                # 在调用 LLM 之前检查并执行上下文压缩
                if context.needs_compress:
                    yield {"type": EventType.CONTEXT_COMPRESS_START, "data": {}}
                    await context.compress_if_needed()
                    yield {"type": EventType.CONTEXT_COMPRESS_END, "data": {"success": True}}
                    messages = context.get_messages()

                # 发送 thinking 开始事件
                yield {"type": EventType.THINKING_START, "data": {}}

                # 调用 LLM（带重试）
                current_content = ""
                current_tool_calls: List[ToolCall] = []
                thinking_ended = False
                stream_error = None

                for attempt in range(1 + self.llm_stream_max_retries):
                    if attempt > 0:
                        # 重试：重置状态
                        current_content = ""
                        current_tool_calls = []
                        thinking_ended = False
                        stream_error = None
                        log_event(
                            "llm_stream_retry",
                            attempt=attempt + 1,
                            max_retries=self.llm_stream_max_retries,
                            thread_id=thread_id,
                        )

                    try:
                        async for chunk in self.client.chat_stream(
                            messages=messages,
                            tools=self._tool_definitions if self._tools else None,
                            system_prompts=context.system_prompts,
                        ):
                            # 中断检查点 2：每个 LLM chunk 到达时
                            if self._is_cancelled(cancel_event):
                                break

                            # 更新 token 使用量
                            if chunk.usage:
                                context.update_token_usage(chunk.usage)

                            # 处理文本内容
                            if chunk.text:
                                if not thinking_ended:
                                    yield {"type": EventType.THINKING_END, "data": {}}
                                    thinking_ended = True
                                current_content += chunk.text
                                streamed_content += chunk.text
                                yield {"type": EventType.TEXT_CHUNK, "data": {"content": chunk.text}}

                            # 处理文本流结束
                            if chunk.text_complete:
                                yield {"type": EventType.TEXT_CHUNK_COMPLETE, "data": {}}

                            # 处理完成
                            if chunk.is_finished:
                                if not thinking_ended:
                                    yield {"type": EventType.THINKING_END, "data": {}}
                                current_tool_calls = chunk.tool_calls

                        # 流式成功完成，跳出重试循环
                        stream_error = None
                        break

                    except Exception as e:
                        stream_error = e
                        error_msg = f"LLM 调用错误 (第 {attempt + 1}/{1 + self.llm_stream_max_retries} 次): [{type(e).__name__}] {str(e) or repr(e)}"
                        log_error(
                            f"llm_stream_error (attempt {attempt + 1}/{1 + self.llm_stream_max_retries}): [{type(e).__name__}] {str(e) or repr(e)}",
                            agent=self.name,
                        )

                        # 中断检查点：异常时也检查取消状态，避免无效重试
                        if self._is_cancelled(cancel_event):
                            break

                        # 每次错误都发送 error 事件
                        yield {
                            "type": EventType.ERROR,
                            "data": {
                                "message": error_msg,
                                "error_type": type(e).__name__,
                                "attempt": attempt + 1,
                                "max_attempts": 1 + self.llm_stream_max_retries,
                                "will_retry": attempt < self.llm_stream_max_retries,
                            },
                        }
                        if attempt >= self.llm_stream_max_retries:
                            raise

                if not thinking_ended:
                    yield {"type": EventType.THINKING_END, "data": {}}

                # 中断检查点（LLM 调用后）
                if self._is_cancelled(cancel_event):
                    break

                # 如果没有工具调用，结束循环
                if not current_tool_calls:
                    if current_content:
                        context.add_message(
                            Message(role=MessageRole.ASSISTANT, content=current_content)
                        )

                    # 检测空响应
                    if tool_calls_made and not streamed_content and self.enable_empty_response_retry:
                        # 临时添加重试提示
                        messages = context.get_messages()
                        messages.append(Message(
                            role=MessageRole.USER,
                            content=self.empty_response_retry_prompt,
                        ))

                        async for chunk in self.client.chat_stream(
                            messages=messages,
                            tools=None,
                            system_prompts=context.system_prompts,
                        ):
                            if self._is_cancelled(cancel_event):
                                break
                            if chunk.text:
                                streamed_content += chunk.text
                                yield {"type": EventType.TEXT_CHUNK, "data": {"content": chunk.text}}

                        if streamed_content:
                            context.add_message(
                                Message(role=MessageRole.ASSISTANT, content=streamed_content)
                            )
                        else:
                            default_msg = "\n\n[工具执行完成，但模型未能生成总结]"
                            yield {"type": EventType.TEXT_CHUNK, "data": {"content": default_msg}}

                    break

                # 有工具调用
                tool_calls_made = True

                # 本轮有 skill 调用时，清理 context 中旧的 skill 消息
                if any(tc.name == "skill" for tc in current_tool_calls):
                    self._cleanup_skill_history(thread_id)

                # 保存带工具调用的 AI 消息
                context.add_message(
                    Message(
                        role=MessageRole.ASSISTANT,
                        content=current_content,
                        tool_calls=current_tool_calls,
                    )
                )

                # 执行工具调用
                for tool_call in current_tool_calls:
                    # 中断检查点 3：每个工具调用前，直接写入中断结果而非 break
                    if self._is_cancelled(cancel_event):
                        context.add_message(
                            Message(role=MessageRole.TOOL, tool_result=ToolResult(
                                tool_call_id=tool_call.id,
                                name=tool_call.name,
                                content="[中断] 工具调用被用户中断",
                                is_error=True,
                            ))
                        )
                        continue

                    # 死循环检测
                    if self._detect_doom_loop(tool_call, thread_id):
                        error_msg = f"检测到死循环：工具 {tool_call.name} 被连续调用 {self.doom_loop_threshold} 次（已阻止本次调用）。请换一种方式或使用不同的参数。"
                        yield {
                            "type": EventType.ERROR,
                            "data": {
                                "tool": tool_call.name,
                                "message": error_msg,
                            },
                        }
                        # 将错误作为 tool result 反馈给 LLM，避免重复调用
                        context.add_message(
                            Message(role=MessageRole.TOOL, tool_result=ToolResult(
                                tool_call_id=tool_call.id,
                                name=tool_call.name,
                                content=error_msg,
                                is_error=True,
                            ))
                        )
                        continue

                    # 执行工具（包含权限检查、事件发送、日志记录）
                    result = None
                    async for event in self._execute_tool(tool_call, thread_id, cancel_event):
                        if event.get("type") == "tool_result":
                            result = event["result"]
                        else:
                            yield event

                    # 保存工具结果到上下文
                    if result:
                        context.add_message(
                            Message(role=MessageRole.TOOL, tool_result=result)
                        )

                # 更新消息列表，继续循环
                messages = context.get_messages()

                # 中断检查点 4：所有工具执行完后
                if self._is_cancelled(cancel_event):
                    break

            # 中断时不需要额外修复，checkpoint 3 已为所有未执行的 tool_call 写入结果

            # 发送完成事件
            yield {"type": EventType.TASK_COMPLETED, "data": {}}

            log_event(
                "agent_end",
                agent_name=self.name,
                thread_id=thread_id,
                response_length=len(streamed_content),
            )

        except Exception as e:
            error_detail = f"[{type(e).__name__}] {str(e) or repr(e)}"
            log_error(f"agent_error: {error_detail}", agent=self.name)
            yield {"type": EventType.TASK_ERROR, "data": {"message": f"Agent error: {error_detail}"}}

    async def run(self, user_input: str, thread_id: str = "default") -> str:
        """非流式执行 Agent"""
        full_response = ""
        async for event in self.run_stream(user_input, thread_id):
            if event.get("type") == "text_chunk":
                full_response += event.get("data", {}).get("content", "")
        return full_response

    def clear_history(self, thread_id: str = "default"):
        """清除指定线程的历史"""
        if thread_id in self._contexts:
            self._contexts[thread_id].clear()
        if thread_id in self._tool_call_history:
            del self._tool_call_history[thread_id]

    def get_capabilities(self) -> dict:
        """获取 Agent 能力描述"""
        return {
            "name": self.name,
            "tools": [t.name for t in self._tools],
            "max_iterations": self.max_iterations,
            "enable_permission_check": self.enable_permission_check,
        }
