Przeglądaj źródła

feat: opencode tools adapter

Talegorithm 1 miesiąc temu
rodzic
commit
8bd2fa166d

+ 1 - 0
.gitignore

@@ -35,6 +35,7 @@ env/
 *.swp
 *.swp
 *.swo
 *.swo
 *~
 *~
+CLAUDE.md
 
 
 # Testing
 # Testing
 .pytest_cache/
 .pytest_cache/

+ 55 - 11
agent/llm/providers/gemini.py

@@ -71,14 +71,14 @@ def _convert_messages_to_gemini(messages: List[Dict]) -> tuple[List[Dict], Optio
         # 非 tool 消息:先 flush buffer
         # 非 tool 消息:先 flush buffer
         flush_tool_buffer()
         flush_tool_buffer()
 
 
-        content_text = msg.get("content", "")
+        content = msg.get("content", "")
         tool_calls = msg.get("tool_calls")
         tool_calls = msg.get("tool_calls")
 
 
         # Assistant 消息 + tool_calls
         # Assistant 消息 + tool_calls
         if role == "assistant" and tool_calls:
         if role == "assistant" and tool_calls:
             parts = []
             parts = []
-            if content_text and content_text.strip():
-                parts.append({"text": content_text})
+            if content and (isinstance(content, str) and content.strip()):
+                parts.append({"text": content})
 
 
             # 转换 tool_calls 为 functionCall
             # 转换 tool_calls 为 functionCall
             for tc in tool_calls:
             for tc in tool_calls:
@@ -104,16 +104,60 @@ def _convert_messages_to_gemini(messages: List[Dict]) -> tuple[List[Dict], Optio
                 })
                 })
             continue
             continue
 
 
-        # 跳过空消息
-        if not content_text or not content_text.strip():
+        # 处理多模态消息(content 为数组)
+        if isinstance(content, list):
+            parts = []
+            for item in content:
+                item_type = item.get("type")
+
+                # 文本部分
+                if item_type == "text":
+                    text = item.get("text", "")
+                    if text.strip():
+                        parts.append({"text": text})
+
+                # 图片部分(OpenAI format -> Gemini format)
+                elif item_type == "image_url":
+                    image_url = item.get("image_url", {})
+                    url = image_url.get("url", "")
+
+                    # 处理 data URL (data:image/png;base64,...)
+                    if url.startswith("data:"):
+                        # 解析 MIME type 和 base64 数据
+                        # 格式:data:image/png;base64,<base64_data>
+                        try:
+                            header, base64_data = url.split(",", 1)
+                            mime_type = header.split(";")[0].replace("data:", "")
+
+                            parts.append({
+                                "inline_data": {
+                                    "mime_type": mime_type,
+                                    "data": base64_data
+                                }
+                            })
+                        except Exception as e:
+                            print(f"[WARNING] Failed to parse image data URL: {e}")
+
+            if parts:
+                gemini_role = "model" if role == "assistant" else "user"
+                contents.append({
+                    "role": gemini_role,
+                    "parts": parts
+                })
             continue
             continue
 
 
-        # 普通消息
-        gemini_role = "model" if role == "assistant" else "user"
-        contents.append({
-            "role": gemini_role,
-            "parts": [{"text": content_text}]
-        })
+        # 普通文本消息(content 为字符串)
+        if isinstance(content, str):
+            # 跳过空消息
+            if not content.strip():
+                continue
+
+            gemini_role = "model" if role == "assistant" else "user"
+            contents.append({
+                "role": gemini_role,
+                "parts": [{"text": content}]
+            })
+            continue
 
 
     # Flush 剩余的 tool messages
     # Flush 剩余的 tool messages
     flush_tool_buffer()
     flush_tool_buffer()

+ 6 - 0
agent/prompts/__init__.py

@@ -0,0 +1,6 @@
+"""Prompt loading and processing utilities"""
+
+from .loader import load_prompt, get_message
+from .wrapper import SimplePrompt, create_prompt
+
+__all__ = ["load_prompt", "get_message", "SimplePrompt", "create_prompt"]

+ 190 - 0
agent/prompts/loader.py

@@ -0,0 +1,190 @@
+"""
+Prompt Loader
+
+支持 .prompt 文件格式:
+- YAML frontmatter 配置(model, temperature等)
+- $section$ 分节语法
+- %variable% 参数替换
+
+格式:
+---
+model: gemini-2.5-flash
+temperature: 0.3
+---
+
+$system$
+系统提示...
+
+$user$
+用户提示...
+%variable%
+"""
+
+import re
+import yaml
+from pathlib import Path
+from typing import Dict, Any, Tuple, Union
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+def load_prompt(path: Union[Path, str]) -> Tuple[Dict[str, Any], Dict[str, str]]:
+    """
+    加载 .prompt 文件
+
+    Args:
+        path: .prompt 文件路径
+
+    Returns:
+        (config, messages)
+        - config: 配置字典 {'model': 'gemini-2.5-flash', 'temperature': 0.3, ...}
+        - messages: 消息字典 {'system': '...', 'user': '...'}
+
+    Raises:
+        FileNotFoundError: 文件不存在
+        ValueError: 文件格式错误
+
+    Example:
+        >>> config, messages = load_prompt(Path("task.prompt"))
+        >>> config['model']
+        'gemini-2.5-flash'
+        >>> messages['system']
+        '你是一位计算机视觉专家...'
+    """
+    path = Path(path) if isinstance(path, str) else path
+
+    if not path.exists():
+        raise FileNotFoundError(f".prompt 文件不存在: {path}")
+
+    try:
+        content = path.read_text(encoding='utf-8')
+    except Exception as e:
+        raise ValueError(f"读取 .prompt 文件失败: {e}")
+
+    # 解析文件
+    config, messages = _parse_prompt(content)
+
+    logger.debug(f"加载 .prompt 文件: {path}, 配置项: {len(config)}, 消息段: {len(messages)}")
+
+    return config, messages
+
+
+def _parse_prompt(content: str) -> Tuple[Dict[str, Any], Dict[str, str]]:
+    """
+    解析 .prompt 文件内容
+
+    格式:
+    ---
+    model: gemini-2.5-flash
+    temperature: 0.3
+    ---
+
+    $system$
+    系统提示...
+
+    $user$
+    用户提示...
+    """
+    # 1. 分离 YAML frontmatter 和正文
+    parts = content.split('---', 2)
+
+    if len(parts) < 3:
+        raise ValueError(".prompt 文件格式错误: 缺少 YAML frontmatter(需要 --- 包裹)")
+
+    # 2. 解析 YAML 配置
+    try:
+        config = yaml.safe_load(parts[1]) or {}
+    except yaml.YAMLError as e:
+        raise ValueError(f".prompt 文件 YAML 解析失败: {e}")
+
+    # 3. 解析正文(按 $section$ 分割)
+    body = parts[2]
+    messages = _parse_sections(body)
+
+    return config, messages
+
+
+def _parse_sections(body: str) -> Dict[str, str]:
+    """
+    解析 .prompt 正文分节
+
+    支持语法:
+    - $section$ (如 $system$, $user$)
+
+    Args:
+        body: .prompt 正文内容
+
+    Returns:
+        消息字典 {'system': '...', 'user': '...'}
+
+    Example:
+        >>> body = "$system$\\n你好\\n$user$\\n世界"
+        >>> _parse_sections(body)
+        {'system': '你好', 'user': '世界'}
+    """
+    messages = {}
+
+    # 使用正则表达式分割(匹配 $key$)
+    pattern = r'\$([^$]+)\$'
+    parts = re.split(pattern, body)
+
+    # parts 格式:['前置空白', 'key1', '内容1', 'key2', '内容2', ...]
+    # 跳过 parts[0](前置空白)
+    i = 1
+    while i < len(parts):
+        if i + 1 >= len(parts):
+            break
+
+        key = parts[i].strip()
+        value = parts[i + 1].strip()
+
+        if key and value:
+            messages[key] = value
+
+        i += 2
+
+    if not messages:
+        logger.warning(".prompt 文件没有找到任何分节($section$)")
+
+    return messages
+
+
+def get_message(messages: Dict[str, str], key: str, **params) -> str:
+    """
+    获取消息(带参数替换)
+
+    参数替换:
+    - 使用 %variable% 语法
+    - 直接字符串替换
+
+    Args:
+        messages: 消息字典(来自 load_prompt)
+        key: 消息键(如 'system', 'user')
+        **params: 参数替换(如 text='内容')
+
+    Returns:
+        替换后的消息字符串
+
+    Example:
+        >>> messages = {'user': '特征:%text%'}
+        >>> get_message(messages, 'user', text='整体构图')
+        '特征:整体构图'
+    """
+    message = messages.get(key, "")
+
+    if not message:
+        logger.warning(f".prompt 消息未找到: key='{key}'")
+        return ""
+
+    # 参数替换(%variable% 直接替换)
+    if params:
+        try:
+            for param_name, param_value in params.items():
+                placeholder = f"%{param_name}%"
+                if placeholder in message:
+                    message = message.replace(placeholder, str(param_value))
+        except Exception as e:
+            logger.error(f".prompt 参数替换错误: key='{key}', error={e}")
+
+    return message

+ 168 - 0
agent/prompts/wrapper.py

@@ -0,0 +1,168 @@
+"""
+Prompt Wrapper - 为 .prompt 文件提供 Prompt 实现
+
+类似 Resonote 的 SimpleHPrompt,但增加了多模态支持
+"""
+
+import base64
+from pathlib import Path
+from typing import List, Dict, Any, Union, Optional
+from agent.prompts.loader import load_prompt, get_message
+
+
+class SimplePrompt:
+    """
+    通用的 Prompt 包装器
+
+    特性:
+    - 加载 .prompt 文件(YAML frontmatter + sections)
+    - 支持参数替换(%variable%)
+    - 支持多模态消息(图片)
+
+    使用示例:
+        # 纯文本
+        prompt = SimplePrompt(Path("task.prompt"))
+        messages = prompt.build_messages(text="内容")
+
+        # 多模态(文本 + 图片)
+        messages = prompt.build_messages(
+            text="分析这张图片",
+            images="path/to/image.png"  # 或 images=["img1.png", "img2.png"]
+        )
+    """
+
+    def __init__(self, prompt_path: Union[Path, str]):
+        """
+        Args:
+            prompt_path: .prompt 文件路径
+        """
+        self.prompt_path = Path(prompt_path) if isinstance(prompt_path, str) else prompt_path
+
+        # 加载 .prompt 文件
+        self.config, self._messages = load_prompt(self.prompt_path)
+
+    def build_messages(self, **context) -> List[Dict[str, Any]]:
+        """
+        构造消息列表(支持多模态)
+
+        Args:
+            **context: 参数
+                - 普通参数:用于替换 %variable%
+                - images: 图片资源(可选)
+                  - 单个图片:str 或 Path
+                  - 多个图片:List[str | Path]
+                  - 格式:文件路径或 base64 字符串
+
+        Returns:
+            消息列表,格式遵循 OpenAI API 规范
+
+        Example:
+            >>> messages = prompt.build_messages(
+            ...     text="特征描述",
+            ...     images="input/image.png"
+            ... )
+            [
+                {"role": "system", "content": "..."},
+                {
+                    "role": "user",
+                    "content": [
+                        {"type": "text", "text": "..."},
+                        {"type": "image_url", "image_url": {"url": "data:image/png;base64,..."}}
+                    ]
+                }
+            ]
+        """
+        # 提取图片资源(从 context 中移除,避免传入 get_message)
+        images = context.pop('images', None)
+
+        # 构建文本内容(支持参数替换)
+        system_content = get_message(self._messages, 'system', **context)
+        user_content = get_message(self._messages, 'user', **context)
+
+        messages = []
+
+        # 添加 system 消息
+        if system_content:
+            messages.append({"role": "system", "content": system_content})
+
+        # 添加 user 消息(可能是多模态)
+        if images:
+            # 多模态消息
+            user_message = {"role": "user", "content": []}
+
+            # 添加文本部分
+            if user_content:
+                user_message["content"].append({
+                    "type": "text",
+                    "text": user_content
+                })
+
+            # 添加图片部分
+            if isinstance(images, (list, tuple)):
+                for img in images:
+                    user_message["content"].append(self._build_image_content(img))
+            else:
+                user_message["content"].append(self._build_image_content(images))
+
+            messages.append(user_message)
+        else:
+            # 纯文本消息
+            if user_content:
+                messages.append({"role": "user", "content": user_content})
+
+        return messages
+
+    def _build_image_content(self, image: Union[str, Path]) -> Dict[str, Any]:
+        """
+        构建图片内容部分(OpenAI 格式)
+
+        Args:
+            image: 图片路径或 base64 字符串
+
+        Returns:
+            {"type": "image_url", "image_url": {"url": "data:..."}}
+        """
+        # 如果已经是 base64 data URL,直接使用
+        if isinstance(image, str) and image.startswith("data:"):
+            return {
+                "type": "image_url",
+                "image_url": {"url": image}
+            }
+
+        # 否则,读取文件并转为 base64
+        image_path = Path(image) if isinstance(image, str) else image
+
+        # 推断 MIME type
+        suffix = image_path.suffix.lower()
+        mime_type_map = {
+            '.png': 'image/png',
+            '.jpg': 'image/jpeg',
+            '.jpeg': 'image/jpeg',
+            '.gif': 'image/gif',
+            '.webp': 'image/webp'
+        }
+        mime_type = mime_type_map.get(suffix, 'image/png')
+
+        # 读取并编码
+        with open(image_path, 'rb') as f:
+            image_data = base64.b64encode(f.read()).decode('utf-8')
+
+        data_url = f"data:{mime_type};base64,{image_data}"
+
+        return {
+            "type": "image_url",
+            "image_url": {"url": data_url}
+        }
+
+
+def create_prompt(prompt_path: Union[Path, str]) -> SimplePrompt:
+    """
+    工厂函数:创建 SimplePrompt 实例
+
+    Args:
+        prompt_path: .prompt 文件路径
+
+    Returns:
+        SimplePrompt 实例
+    """
+    return SimplePrompt(prompt_path)

+ 13 - 0
agent/tools/adapters/__init__.py

@@ -0,0 +1,13 @@
+"""
+工具适配器 - 集成第三方工具到 Agent 框架
+
+提供统一的适配器接口,将外部工具(如 opencode)适配到我们的工具系统。
+"""
+
+from agent.tools.adapters.base import ToolAdapter
+from agent.tools.adapters.opencode_bun_adapter import OpenCodeBunAdapter
+
+__all__ = [
+    "ToolAdapter",
+    "OpenCodeBunAdapter",
+]

+ 62 - 0
agent/tools/adapters/base.py

@@ -0,0 +1,62 @@
+"""
+基础工具适配器 - 第三方工具适配接口
+
+职责:
+1. 定义统一的适配器接口
+2. 处理工具执行上下文的转换
+3. 统一返回值格式
+"""
+
+from abc import ABC, abstractmethod
+from typing import Any, Callable, Dict
+
+from agent.tools.models import ToolResult, ToolContext
+
+
+class ToolAdapter(ABC):
+    """工具适配器基类"""
+
+    @abstractmethod
+    async def adapt_execute(
+        self,
+        tool_func: Callable,
+        args: Dict[str, Any],
+        context: ToolContext
+    ) -> ToolResult:
+        """
+        适配第三方工具的执行
+
+        Args:
+            tool_func: 原始工具函数/对象
+            args: 工具参数
+            context: 我们的上下文对象
+
+        Returns:
+            ToolResult: 统一的返回格式
+        """
+        pass
+
+    @abstractmethod
+    def adapt_schema(self, original_schema: Dict) -> Dict:
+        """
+        适配工具 Schema 到我们的格式
+
+        Args:
+            original_schema: 原始工具的 Schema
+
+        Returns:
+            适配后的 Schema(OpenAI Tool Schema 格式)
+        """
+        pass
+
+    def extract_memory(self, result: Any) -> str:
+        """
+        从结果中提取长期记忆摘要
+
+        Args:
+            result: 工具执行结果
+
+        Returns:
+            记忆摘要字符串
+        """
+        return ""

+ 120 - 0
agent/tools/adapters/opencode-wrapper.ts

@@ -0,0 +1,120 @@
+/**
+ * OpenCode Tool Wrapper - 供 Python 调用的命令行接口
+ *
+ * 用法:
+ * bun run tool-wrapper.ts <tool_name> <args_json>
+ *
+ * 示例:
+ * bun run tool-wrapper.ts read '{"filePath": "config.py"}'
+ *
+ * 支持的工具:
+ * - read: 读取文件
+ * - edit: 编辑文件
+ * - write: 写入文件
+ * - bash: 执行命令
+ * - glob: 文件匹配
+ * - grep: 内容搜索
+ * - webfetch: 抓取网页
+ * - lsp: LSP 诊断
+ */
+
+import { resolve } from 'path'
+
+// 动态导入工具(避免编译时依赖)
+async function loadTool(toolName: string) {
+  // 从 agent/tools/adapters/ 定位到 vendor/opencode
+  const toolPath = resolve(__dirname, '../../../../vendor/opencode/packages/opencode/src/tool')
+
+  switch (toolName) {
+    case 'read':
+      return (await import(`${toolPath}/read.ts`)).ReadTool
+    case 'edit':
+      return (await import(`${toolPath}/edit.ts`)).EditTool
+    case 'write':
+      return (await import(`${toolPath}/write.ts`)).WriteTool
+    case 'bash':
+      return (await import(`${toolPath}/bash.ts`)).BashTool
+    case 'glob':
+      return (await import(`${toolPath}/glob.ts`)).GlobTool
+    case 'grep':
+      return (await import(`${toolPath}/grep.ts`)).GrepTool
+    case 'webfetch':
+      return (await import(`${toolPath}/webfetch.ts`)).WebFetchTool
+    case 'lsp':
+      return (await import(`${toolPath}/lsp.ts`)).LspTool
+    default:
+      throw new Error(`Unknown tool: ${toolName}`)
+  }
+}
+
+async function main() {
+  const toolName = process.argv[2]
+  const argsJson = process.argv[3]
+
+  if (!toolName || !argsJson) {
+    console.error('Usage: bun run tool-wrapper.ts <tool_name> <args_json>')
+    console.error('Example: bun run tool-wrapper.ts read \'{"filePath": "test.py"}\'')
+    process.exit(1)
+  }
+
+  try {
+    // 解析参数
+    const args = JSON.parse(argsJson)
+
+    // 加载工具
+    const Tool = await loadTool(toolName)
+
+    // 构造最小化的 context(Python 调用不需要完整 context)
+    const context = {
+      sessionID: 'python-adapter',
+      messageID: 'python-adapter',
+      agent: 'python',
+      abort: new AbortController().signal,
+      messages: [],
+      metadata: () => {},
+      ask: async () => {}, // 跳过权限检查(由 Python 层处理)
+    }
+
+    // 初始化工具
+    const toolInfo = await Tool.init()
+
+    // 执行工具
+    const result = await toolInfo.execute(args, context)
+
+    // 输出 JSON 结果
+    const output = {
+      title: result.title,
+      output: result.output,
+      metadata: result.metadata || {},
+      attachments: result.attachments || []
+    }
+
+    console.log(JSON.stringify(output))
+
+  } catch (error: any) {
+    // 输出错误信息
+    const errorOutput = {
+      title: 'Error',
+      output: `Tool execution failed: ${error.message}`,
+      metadata: {
+        error: error.message,
+        stack: error.stack
+      }
+    }
+
+    console.error(JSON.stringify(errorOutput))
+    process.exit(1)
+  }
+}
+
+main().catch((error) => {
+  console.error(JSON.stringify({
+    title: 'Fatal Error',
+    output: `Fatal error: ${error.message}`,
+    metadata: {
+      error: error.message,
+      stack: error.stack
+    }
+  }))
+  process.exit(1)
+})

+ 138 - 0
agent/tools/adapters/opencode_bun_adapter.py

@@ -0,0 +1,138 @@
+"""
+OpenCode Bun 适配器 - 通过子进程调用 opencode 工具
+
+这个适配器真正调用 opencode 的 TypeScript 实现,
+而不是 Python 重新实现。
+
+使用场景:
+- 高级工具(LSP、CodeSearch 等)
+- 需要完整功能(9 种编辑策略)
+- 不在意性能开销(50-100ms per call)
+"""
+
+import json
+import asyncio
+import subprocess
+from pathlib import Path
+from typing import Any, Dict, Optional
+
+from agent.tools.adapters.base import ToolAdapter
+from agent.tools.models import ToolResult, ToolContext
+
+
+class OpenCodeBunAdapter(ToolAdapter):
+    """
+    通过 Bun 子进程调用 opencode 工具
+
+    需要安装 Bun: https://bun.sh/
+    """
+
+    def __init__(self):
+        # wrapper 和 adapter 在同一目录
+        self.wrapper_script = Path(__file__).parent / "opencode-wrapper.ts"
+        self.opencode_path = Path(__file__).parent.parent.parent.parent / "vendor/opencode"
+
+        # 检查 Bun 是否可用
+        self._check_bun()
+
+    def _check_bun(self):
+        """检查 Bun 运行时是否可用"""
+        try:
+            result = subprocess.run(
+                ["bun", "--version"],
+                capture_output=True,
+                timeout=5
+            )
+            if result.returncode != 0:
+                raise RuntimeError("Bun is not available")
+        except FileNotFoundError:
+            raise RuntimeError(
+                "Bun runtime not found. Install from https://bun.sh/\n"
+                "Or use Python-based tools instead."
+            )
+
+    async def adapt_execute(
+        self,
+        tool_name: str,  # 'read', 'edit', 'bash' 等
+        args: Dict[str, Any],
+        context: Optional[ToolContext] = None
+    ) -> ToolResult:
+        """
+        调用 opencode 工具
+
+        Args:
+            tool_name: opencode 工具名称
+            args: 工具参数
+            context: 上下文
+        """
+        # 构造命令
+        cmd = [
+            "bun", "run",
+            str(self.wrapper_script),
+            tool_name,
+            json.dumps(args)
+        ]
+
+        # 执行
+        try:
+            process = await asyncio.create_subprocess_exec(
+                *cmd,
+                stdout=asyncio.subprocess.PIPE,
+                stderr=asyncio.subprocess.PIPE,
+                cwd=str(self.opencode_path)
+            )
+
+            stdout, stderr = await asyncio.wait_for(
+                process.communicate(),
+                timeout=30  # 30 秒超时
+            )
+
+            if process.returncode != 0:
+                error_msg = stderr.decode('utf-8', errors='replace')
+                return ToolResult(
+                    title="OpenCode Error",
+                    output=f"工具执行失败: {error_msg}",
+                    error=error_msg
+                )
+
+            # 解析结果
+            result_data = json.loads(stdout.decode('utf-8'))
+
+            # 转换为 ToolResult
+            return ToolResult(
+                title=result_data.get("title", ""),
+                output=result_data.get("output", ""),
+                metadata=result_data.get("metadata", {}),
+                long_term_memory=self.extract_memory(result_data)
+            )
+
+        except asyncio.TimeoutError:
+            return ToolResult(
+                title="Timeout",
+                output="OpenCode 工具执行超时",
+                error="Timeout after 30s"
+            )
+        except Exception as e:
+            return ToolResult(
+                title="Execution Error",
+                output=f"调用 OpenCode 失败: {str(e)}",
+                error=str(e)
+            )
+
+    def adapt_schema(self, original_schema: Dict) -> Dict:
+        """OpenCode 使用 OpenAI 格式,直接返回"""
+        return original_schema
+
+    def extract_memory(self, result: Dict) -> str:
+        """从 opencode 结果提取记忆"""
+        metadata = result.get("metadata", {})
+
+        if metadata.get("truncated"):
+            return f"输出被截断 (file: {result.get('title', '')})"
+
+        if "diagnostics" in metadata:
+            count = len(metadata["diagnostics"])
+            if count > 0:
+                return f"检测到 {count} 个诊断问题"
+
+        return ""

+ 15 - 0
agent/tools/advanced/__init__.py

@@ -0,0 +1,15 @@
+"""
+高级工具 - 通过 Bun 适配器调用 OpenCode
+
+这些工具实现复杂,直接调用 opencode 的 TypeScript 实现。
+
+需要 Bun 运行时:https://bun.sh/
+"""
+
+from agent.tools.advanced.webfetch import webfetch
+from agent.tools.advanced.lsp import lsp_diagnostics
+
+__all__ = [
+    "webfetch",
+    "lsp_diagnostics",
+]

+ 52 - 0
agent/tools/advanced/lsp.py

@@ -0,0 +1,52 @@
+"""
+LSP Tool - 通过 Bun 适配器调用 OpenCode
+
+Language Server Protocol 集成,提供代码诊断、补全等功能。
+"""
+
+from typing import Optional
+from agent.tools import tool, ToolResult, ToolContext
+from agent.tools.adapters.opencode_bun_adapter import OpenCodeBunAdapter
+
+
+# 创建适配器实例
+_adapter = None
+
+def _get_adapter():
+    global _adapter
+    if _adapter is None:
+        _adapter = OpenCodeBunAdapter()
+    return _adapter
+
+
+@tool(description="获取文件的 LSP 诊断信息(语法错误、类型错误等)")
+async def lsp_diagnostics(
+    file_path: str,
+    uid: str = "",
+    context: Optional[ToolContext] = None
+) -> ToolResult:
+    """
+    获取 LSP 诊断信息
+
+    使用 OpenCode 的 LSP 工具(通过 Bun 适配器调用)。
+    返回文件的语法错误、类型错误、代码警告等。
+
+    Args:
+        file_path: 文件路径
+        uid: 用户 ID
+        context: 工具上下文
+
+    Returns:
+        ToolResult: 诊断信息
+    """
+    adapter = _get_adapter()
+
+    args = {
+        "filePath": file_path,
+    }
+
+    return await adapter.adapt_execute(
+        tool_name="lsp",
+        args=args,
+        context=context
+    )

+ 60 - 0
agent/tools/advanced/webfetch.py

@@ -0,0 +1,60 @@
+"""
+WebFetch Tool - 通过 Bun 适配器调用 OpenCode
+
+网页抓取功能,包括 HTML 转 Markdown、内容提取等复杂逻辑。
+"""
+
+from typing import Optional
+from agent.tools import tool, ToolResult, ToolContext
+from agent.tools.adapters.opencode_bun_adapter import OpenCodeBunAdapter
+
+
+# 创建适配器实例
+_adapter = None
+
+def _get_adapter():
+    global _adapter
+    if _adapter is None:
+        _adapter = OpenCodeBunAdapter()
+    return _adapter
+
+
+@tool(description="抓取网页内容并转换为 Markdown 格式")
+async def webfetch(
+    url: str,
+    format: str = "markdown",
+    timeout: Optional[int] = None,
+    uid: str = "",
+    context: Optional[ToolContext] = None
+) -> ToolResult:
+    """
+    抓取网页内容
+
+    使用 OpenCode 的 webfetch 工具(通过 Bun 适配器调用)。
+    包含 HTML 到 Markdown 转换、内容清理等功能。
+
+    Args:
+        url: 网页 URL
+        format: 输出格式(markdown, text, html),默认 markdown
+        timeout: 超时时间(秒)
+        uid: 用户 ID
+        context: 工具上下文
+
+    Returns:
+        ToolResult: 网页内容
+    """
+    adapter = _get_adapter()
+
+    args = {
+        "url": url,
+        "format": format,
+    }
+
+    if timeout is not None:
+        args["timeout"] = timeout
+
+    return await adapter.adapt_execute(
+        tool_name="webfetch",
+        args=args,
+        context=context
+    )

+ 24 - 0
agent/tools/builtin/__init__.py

@@ -0,0 +1,24 @@
+"""
+内置基础工具 - 参考 opencode 实现
+
+这些工具参考 vendor/opencode/packages/opencode/src/tool/ 的设计,
+在 Python 中重新实现核心功能。
+
+参考版本:opencode main branch (2025-01)
+"""
+
+from agent.tools.builtin.read import read_file
+from agent.tools.builtin.edit import edit_file
+from agent.tools.builtin.write import write_file
+from agent.tools.builtin.glob import glob_files
+from agent.tools.builtin.grep import grep_content
+from agent.tools.builtin.bash import bash_command
+
+__all__ = [
+    "read_file",
+    "edit_file",
+    "write_file",
+    "glob_files",
+    "grep_content",
+    "bash_command",
+]

+ 152 - 0
agent/tools/builtin/bash.py

@@ -0,0 +1,152 @@
+"""
+Bash Tool - 命令执行工具
+
+参考 OpenCode bash.ts 完整实现。
+
+核心功能:
+- 执行 shell 命令
+- 超时控制
+- 工作目录设置
+- 环境变量传递
+"""
+
+import subprocess
+import asyncio
+from pathlib import Path
+from typing import Optional, Dict
+
+from agent.tools import tool, ToolResult, ToolContext
+
+# 常量
+DEFAULT_TIMEOUT = 120  # 2 分钟
+MAX_OUTPUT_LENGTH = 50000  # 最大输出长度
+
+
+@tool(description="执行 bash 命令")
+async def bash_command(
+    command: str,
+    timeout: Optional[int] = None,
+    workdir: Optional[str] = None,
+    env: Optional[Dict[str, str]] = None,
+    description: str = "",
+    uid: str = "",
+    context: Optional[ToolContext] = None
+) -> ToolResult:
+    """
+    执行 bash 命令
+
+    Args:
+        command: 要执行的命令
+        timeout: 超时时间(秒),默认 120 秒
+        workdir: 工作目录,默认为当前目录
+        env: 环境变量字典(会合并到系统环境变量)
+        description: 命令描述(5-10 个词)
+        uid: 用户 ID
+        context: 工具上下文
+
+    Returns:
+        ToolResult: 命令输出
+    """
+    # 参数验证
+    if timeout is not None and timeout < 0:
+        return ToolResult(
+            title="参数错误",
+            output=f"无效的 timeout 值: {timeout}。必须是正数。",
+            error="Invalid timeout"
+        )
+
+    timeout_sec = timeout or DEFAULT_TIMEOUT
+
+    # 工作目录
+    cwd = Path(workdir) if workdir else Path.cwd()
+    if not cwd.exists():
+        return ToolResult(
+            title="目录不存在",
+            output=f"工作目录不存在: {workdir}",
+            error="Directory not found"
+        )
+
+    # 准备环境变量
+    import os
+    process_env = os.environ.copy()
+    if env:
+        process_env.update(env)
+
+    # 执行命令
+    try:
+        process = await asyncio.create_subprocess_shell(
+            command,
+            stdout=asyncio.subprocess.PIPE,
+            stderr=asyncio.subprocess.PIPE,
+            cwd=str(cwd),
+            env=process_env
+        )
+
+        # 等待命令完成(带超时)
+        try:
+            stdout, stderr = await asyncio.wait_for(
+                process.communicate(),
+                timeout=timeout_sec
+            )
+        except asyncio.TimeoutError:
+            # 超时,终止进程
+            process.kill()
+            await process.wait()
+            return ToolResult(
+                title="命令超时",
+                output=f"命令执行超时(>{timeout_sec}s): {command[:100]}",
+                error="Timeout",
+                metadata={"command": command, "timeout": timeout_sec}
+            )
+
+        # 解码输出
+        stdout_text = stdout.decode('utf-8', errors='replace') if stdout else ""
+        stderr_text = stderr.decode('utf-8', errors='replace') if stderr else ""
+
+        # 截断过长输出
+        truncated = False
+        if len(stdout_text) > MAX_OUTPUT_LENGTH:
+            stdout_text = stdout_text[:MAX_OUTPUT_LENGTH] + f"\n\n(输出被截断,总长度: {len(stdout_text)} 字符)"
+            truncated = True
+
+        # 组合输出
+        output = ""
+        if stdout_text:
+            output += stdout_text
+
+        if stderr_text:
+            if output:
+                output += "\n\n--- stderr ---\n"
+            output += stderr_text
+
+        if not output:
+            output = "(命令无输出)"
+
+        # 检查退出码
+        exit_code = process.returncode
+        success = exit_code == 0
+
+        title = description or f"命令: {command[:50]}"
+        if not success:
+            title += f" (exit code: {exit_code})"
+
+        return ToolResult(
+            title=title,
+            output=output,
+            metadata={
+                "exit_code": exit_code,
+                "success": success,
+                "truncated": truncated,
+                "command": command,
+                "cwd": str(cwd)
+            },
+            error=None if success else f"Command failed with exit code {exit_code}"
+        )
+
+    except Exception as e:
+        return ToolResult(
+            title="执行错误",
+            output=f"命令执行失败: {str(e)}",
+            error=str(e),
+            metadata={"command": command}
+        )

+ 533 - 0
agent/tools/builtin/edit.py

@@ -0,0 +1,533 @@
+"""
+Edit Tool - 文件编辑工具
+
+参考 OpenCode 的 edit.ts 完整实现。
+
+核心功能:
+- 精确字符串替换
+- 9 种智能匹配策略(按优先级依次尝试)
+- 生成 diff 预览
+"""
+
+from pathlib import Path
+from typing import Optional, Generator
+import difflib
+import re
+
+from agent.tools import tool, ToolResult, ToolContext
+
+
+@tool(description="编辑文件,使用精确字符串替换。支持多种智能匹配策略。")
+async def edit_file(
+    file_path: str,
+    old_string: str,
+    new_string: str,
+    replace_all: bool = False,
+    uid: str = "",
+    context: Optional[ToolContext] = None
+) -> ToolResult:
+    """
+    编辑文件内容
+
+    使用 9 种智能匹配策略,按优先级尝试:
+    1. SimpleReplacer - 精确匹配
+    2. LineTrimmedReplacer - 忽略行首尾空白
+    3. BlockAnchorReplacer - 基于首尾锚点的块匹配(使用 Levenshtein 相似度)
+    4. WhitespaceNormalizedReplacer - 空白归一化
+    5. IndentationFlexibleReplacer - 灵活缩进匹配
+    6. EscapeNormalizedReplacer - 转义序列归一化
+    7. TrimmedBoundaryReplacer - 边界空白裁剪
+    8. ContextAwareReplacer - 上下文感知匹配
+    9. MultiOccurrenceReplacer - 多次出现匹配
+
+    Args:
+        file_path: 文件路径
+        old_string: 要替换的文本
+        new_string: 替换后的文本
+        replace_all: 是否替换所有匹配(默认 False,只替换唯一匹配)
+        uid: 用户 ID
+        context: 工具上下文
+
+    Returns:
+        ToolResult: 编辑结果和 diff
+    """
+    if old_string == new_string:
+        return ToolResult(
+            title="无需编辑",
+            output="old_string 和 new_string 相同",
+            error="Strings are identical"
+        )
+
+    # 解析路径
+    path = Path(file_path)
+    if not path.is_absolute():
+        path = Path.cwd() / path
+
+    # 检查文件
+    if not path.exists():
+        return ToolResult(
+            title="文件未找到",
+            output=f"文件不存在: {file_path}",
+            error="File not found"
+        )
+
+    if path.is_dir():
+        return ToolResult(
+            title="路径错误",
+            output=f"路径是目录,不是文件: {file_path}",
+            error="Path is a directory"
+        )
+
+    # 读取文件
+    try:
+        with open(path, 'r', encoding='utf-8') as f:
+            content_old = f.read()
+    except Exception as e:
+        return ToolResult(
+            title="读取失败",
+            output=f"无法读取文件: {str(e)}",
+            error=str(e)
+        )
+
+    # 执行替换
+    try:
+        content_new = replace(content_old, old_string, new_string, replace_all)
+    except ValueError as e:
+        return ToolResult(
+            title="替换失败",
+            output=str(e),
+            error=str(e)
+        )
+
+    # 生成 diff
+    diff = _create_diff(file_path, content_old, content_new)
+
+    # 写入文件
+    try:
+        with open(path, 'w', encoding='utf-8') as f:
+            f.write(content_new)
+    except Exception as e:
+        return ToolResult(
+            title="写入失败",
+            output=f"无法写入文件: {str(e)}",
+            error=str(e)
+        )
+
+    # 统计变更
+    old_lines = content_old.count('\n')
+    new_lines = content_new.count('\n')
+
+    return ToolResult(
+        title=path.name,
+        output=f"编辑成功\n\n{diff}",
+        metadata={
+            "diff": diff,
+            "old_lines": old_lines,
+            "new_lines": new_lines,
+            "additions": max(0, new_lines - old_lines),
+            "deletions": max(0, old_lines - new_lines)
+        },
+        long_term_memory=f"已编辑文件 {path.name}"
+    )
+
+
+# ============================================================================
+# 替换策略(Replacers)
+# ============================================================================
+
+def replace(content: str, old_string: str, new_string: str, replace_all: bool = False) -> str:
+    """
+    使用多种策略尝试替换
+
+    按优先级尝试所有策略,直到找到匹配
+    """
+    if old_string == new_string:
+        raise ValueError("old_string 和 new_string 必须不同")
+
+    not_found = True
+
+    # 按优先级尝试策略
+    for replacer in [
+        simple_replacer,
+        line_trimmed_replacer,
+        block_anchor_replacer,
+        whitespace_normalized_replacer,
+        indentation_flexible_replacer,
+        escape_normalized_replacer,
+        trimmed_boundary_replacer,
+        context_aware_replacer,
+        multi_occurrence_replacer,
+    ]:
+        for search in replacer(content, old_string):
+            index = content.find(search)
+            if index == -1:
+                continue
+
+            not_found = False
+
+            if replace_all:
+                return content.replace(search, new_string)
+
+            # 检查唯一性
+            last_index = content.rfind(search)
+            if index != last_index:
+                continue
+
+            return content[:index] + new_string + content[index + len(search):]
+
+    if not_found:
+        raise ValueError("在文件中未找到匹配的文本")
+
+    raise ValueError(
+        "找到多个匹配。请在 old_string 中提供更多上下文以唯一标识,"
+        "或使用 replace_all=True 替换所有匹配。"
+    )
+
+
+# 1. SimpleReplacer - 精确匹配
+def simple_replacer(content: str, find: str) -> Generator[str, None, None]:
+    """精确匹配"""
+    yield find
+
+
+# 2. LineTrimmedReplacer - 忽略行首尾空白
+def line_trimmed_replacer(content: str, find: str) -> Generator[str, None, None]:
+    """忽略行首尾空白进行匹配"""
+    content_lines = content.split('\n')
+    find_lines = find.rstrip('\n').split('\n')
+
+    for i in range(len(content_lines) - len(find_lines) + 1):
+        # 检查所有行是否匹配(忽略首尾空白)
+        matches = all(
+            content_lines[i + j].strip() == find_lines[j].strip()
+            for j in range(len(find_lines))
+        )
+
+        if matches:
+            # 计算原始文本位置
+            match_start = sum(len(content_lines[k]) + 1 for k in range(i))
+            match_end = match_start + sum(
+                len(content_lines[i + k]) + (1 if k < len(find_lines) - 1 else 0)
+                for k in range(len(find_lines))
+            )
+            yield content[match_start:match_end]
+
+
+# 3. BlockAnchorReplacer - 基于首尾锚点的块匹配
+def block_anchor_replacer(content: str, find: str) -> Generator[str, None, None]:
+    """
+    基于首尾行作为锚点进行块匹配
+    使用 Levenshtein 距离计算中间行的相似度
+    """
+    content_lines = content.split('\n')
+    find_lines = find.rstrip('\n').split('\n')
+
+    if len(find_lines) < 3:
+        return
+
+    first_line_find = find_lines[0].strip()
+    last_line_find = find_lines[-1].strip()
+    find_block_size = len(find_lines)
+
+    # 收集所有候选位置(首尾行都匹配)
+    candidates = []
+    for i in range(len(content_lines)):
+        if content_lines[i].strip() != first_line_find:
+            continue
+
+        # 查找匹配的尾行
+        for j in range(i + 2, len(content_lines)):
+            if content_lines[j].strip() == last_line_find:
+                candidates.append((i, j))
+                break
+
+    if not candidates:
+        return
+
+    # 单个候选:使用宽松阈值
+    if len(candidates) == 1:
+        start_line, end_line = candidates[0]
+        actual_block_size = end_line - start_line + 1
+
+        similarity = _calculate_block_similarity(
+            content_lines[start_line:end_line + 1],
+            find_lines
+        )
+
+        if similarity >= 0.0:  # SINGLE_CANDIDATE_SIMILARITY_THRESHOLD
+            match_start = sum(len(content_lines[k]) + 1 for k in range(start_line))
+            match_end = match_start + sum(
+                len(content_lines[k]) + (1 if k < end_line else 0)
+                for k in range(start_line, end_line + 1)
+            )
+            yield content[match_start:match_end]
+        return
+
+    # 多个候选:选择相似度最高的
+    best_match = None
+    max_similarity = -1
+
+    for start_line, end_line in candidates:
+        similarity = _calculate_block_similarity(
+            content_lines[start_line:end_line + 1],
+            find_lines
+        )
+
+        if similarity > max_similarity:
+            max_similarity = similarity
+            best_match = (start_line, end_line)
+
+    if max_similarity >= 0.3 and best_match:  # MULTIPLE_CANDIDATES_SIMILARITY_THRESHOLD
+        start_line, end_line = best_match
+        match_start = sum(len(content_lines[k]) + 1 for k in range(start_line))
+        match_end = match_start + sum(
+            len(content_lines[k]) + (1 if k < end_line else 0)
+            for k in range(start_line, end_line + 1)
+        )
+        yield content[match_start:match_end]
+
+
+def _calculate_block_similarity(content_block: list, find_block: list) -> float:
+    """计算块相似度(使用 Levenshtein 距离)"""
+    actual_size = len(content_block)
+    find_size = len(find_block)
+    lines_to_check = min(find_size - 2, actual_size - 2)
+
+    if lines_to_check <= 0:
+        return 1.0
+
+    similarity = 0.0
+    for j in range(1, min(find_size - 1, actual_size - 1)):
+        content_line = content_block[j].strip()
+        find_line = find_block[j].strip()
+        max_len = max(len(content_line), len(find_line))
+
+        if max_len == 0:
+            continue
+
+        distance = _levenshtein(content_line, find_line)
+        similarity += (1 - distance / max_len) / lines_to_check
+
+    return similarity
+
+
+def _levenshtein(a: str, b: str) -> int:
+    """Levenshtein 距离算法"""
+    if not a:
+        return len(b)
+    if not b:
+        return len(a)
+
+    matrix = [[0] * (len(b) + 1) for _ in range(len(a) + 1)]
+
+    for i in range(len(a) + 1):
+        matrix[i][0] = i
+    for j in range(len(b) + 1):
+        matrix[0][j] = j
+
+    for i in range(1, len(a) + 1):
+        for j in range(1, len(b) + 1):
+            cost = 0 if a[i - 1] == b[j - 1] else 1
+            matrix[i][j] = min(
+                matrix[i - 1][j] + 1,      # 删除
+                matrix[i][j - 1] + 1,      # 插入
+                matrix[i - 1][j - 1] + cost  # 替换
+            )
+
+    return matrix[len(a)][len(b)]
+
+
+# 4. WhitespaceNormalizedReplacer - 空白归一化
+def whitespace_normalized_replacer(content: str, find: str) -> Generator[str, None, None]:
+    """空白归一化匹配(所有空白序列归一化为单个空格)"""
+    def normalize_ws(text: str) -> str:
+        return re.sub(r'\s+', ' ', text).strip()
+
+    normalized_find = normalize_ws(find)
+    lines = content.split('\n')
+
+    # 单行匹配
+    for line in lines:
+        if normalize_ws(line) == normalized_find:
+            yield line
+            continue
+
+        # 子串匹配
+        if normalized_find in normalize_ws(line):
+            words = find.strip().split()
+            if words:
+                pattern = r'\s+'.join(re.escape(word) for word in words)
+                match = re.search(pattern, line)
+                if match:
+                    yield match.group(0)
+
+    # 多行匹配
+    find_lines = find.split('\n')
+    if len(find_lines) > 1:
+        for i in range(len(lines) - len(find_lines) + 1):
+            block = lines[i:i + len(find_lines)]
+            if normalize_ws('\n'.join(block)) == normalized_find:
+                yield '\n'.join(block)
+
+
+# 5. IndentationFlexibleReplacer - 灵活缩进匹配
+def indentation_flexible_replacer(content: str, find: str) -> Generator[str, None, None]:
+    """移除缩进后匹配"""
+    def remove_indentation(text: str) -> str:
+        lines = text.split('\n')
+        non_empty = [line for line in lines if line.strip()]
+
+        if not non_empty:
+            return text
+
+        min_indent = min(len(line) - len(line.lstrip()) for line in non_empty)
+        return '\n'.join(
+            line[min_indent:] if line.strip() else line
+            for line in lines
+        )
+
+    normalized_find = remove_indentation(find)
+    content_lines = content.split('\n')
+    find_lines = find.split('\n')
+
+    for i in range(len(content_lines) - len(find_lines) + 1):
+        block = '\n'.join(content_lines[i:i + len(find_lines)])
+        if remove_indentation(block) == normalized_find:
+            yield block
+
+
+# 6. EscapeNormalizedReplacer - 转义序列归一化
+def escape_normalized_replacer(content: str, find: str) -> Generator[str, None, None]:
+    """反转义后匹配"""
+    def unescape_string(s: str) -> str:
+        replacements = {
+            r'\n': '\n',
+            r'\t': '\t',
+            r'\r': '\r',
+            r"\'": "'",
+            r'\"': '"',
+            r'\`': '`',
+            r'\\': '\\',
+            r'\$': '$',
+        }
+        result = s
+        for escaped, unescaped in replacements.items():
+            result = result.replace(escaped, unescaped)
+        return result
+
+    unescaped_find = unescape_string(find)
+
+    # 直接匹配
+    if unescaped_find in content:
+        yield unescaped_find
+
+    # 尝试反转义后匹配
+    lines = content.split('\n')
+    find_lines = unescaped_find.split('\n')
+
+    for i in range(len(lines) - len(find_lines) + 1):
+        block = '\n'.join(lines[i:i + len(find_lines)])
+        if unescape_string(block) == unescaped_find:
+            yield block
+
+
+# 7. TrimmedBoundaryReplacer - 边界空白裁剪
+def trimmed_boundary_replacer(content: str, find: str) -> Generator[str, None, None]:
+    """裁剪边界空白后匹配"""
+    trimmed_find = find.strip()
+
+    if trimmed_find == find:
+        return  # 已经是 trimmed,无需尝试
+
+    # 尝试匹配 trimmed 版本
+    if trimmed_find in content:
+        yield trimmed_find
+
+    # 尝试块匹配
+    lines = content.split('\n')
+    find_lines = find.split('\n')
+
+    for i in range(len(lines) - len(find_lines) + 1):
+        block = '\n'.join(lines[i:i + len(find_lines)])
+        if block.strip() == trimmed_find:
+            yield block
+
+
+# 8. ContextAwareReplacer - 上下文感知匹配
+def context_aware_replacer(content: str, find: str) -> Generator[str, None, None]:
+    """基于上下文(首尾行)匹配,允许中间行有差异"""
+    find_lines = find.split('\n')
+    if find_lines and find_lines[-1] == '':
+        find_lines.pop()
+
+    if len(find_lines) < 3:
+        return
+
+    content_lines = content.split('\n')
+    first_line = find_lines[0].strip()
+    last_line = find_lines[-1].strip()
+
+    # 查找首尾匹配的块
+    for i in range(len(content_lines)):
+        if content_lines[i].strip() != first_line:
+            continue
+
+        for j in range(i + 2, len(content_lines)):
+            if content_lines[j].strip() == last_line:
+                block_lines = content_lines[i:j + 1]
+
+                # 检查块长度是否匹配
+                if len(block_lines) == len(find_lines):
+                    # 计算中间行匹配率
+                    matching_lines = 0
+                    total_non_empty = 0
+
+                    for k in range(1, len(block_lines) - 1):
+                        block_line = block_lines[k].strip()
+                        find_line = find_lines[k].strip()
+
+                        if block_line or find_line:
+                            total_non_empty += 1
+                            if block_line == find_line:
+                                matching_lines += 1
+
+                    # 至少 50% 的中间行匹配
+                    if total_non_empty == 0 or matching_lines / total_non_empty >= 0.5:
+                        yield '\n'.join(block_lines)
+                        break
+                break
+
+
+# 9. MultiOccurrenceReplacer - 多次出现匹配
+def multi_occurrence_replacer(content: str, find: str) -> Generator[str, None, None]:
+    """yield 所有精确匹配,用于 replace_all"""
+    start_index = 0
+    while True:
+        index = content.find(find, start_index)
+        if index == -1:
+            break
+        yield find
+        start_index = index + len(find)
+
+
+# ============================================================================
+# 辅助函数
+# ============================================================================
+
+def _create_diff(filepath: str, old_content: str, new_content: str) -> str:
+    """生成 unified diff"""
+    old_lines = old_content.splitlines(keepends=True)
+    new_lines = new_content.splitlines(keepends=True)
+
+    diff_lines = list(difflib.unified_diff(
+        old_lines,
+        new_lines,
+        fromfile=f"a/{filepath}",
+        tofile=f"b/{filepath}",
+        lineterm=''
+    ))
+
+    if not diff_lines:
+        return "(无变更)"
+
+    return ''.join(diff_lines)

+ 110 - 0
agent/tools/builtin/glob.py

@@ -0,0 +1,110 @@
+"""
+Glob Tool - 文件模式匹配工具
+
+参考:vendor/opencode/packages/opencode/src/tool/glob.ts
+
+核心功能:
+- 使用 glob 模式匹配文件
+- 按修改时间排序
+- 限制返回数量
+"""
+
+import glob as glob_module
+from pathlib import Path
+from typing import Optional
+
+from agent.tools import tool, ToolResult, ToolContext
+
+# 常量
+LIMIT = 100  # 最大返回数量(参考 opencode glob.ts:35)
+
+
+@tool(description="使用 glob 模式匹配文件")
+async def glob_files(
+    pattern: str,
+    path: Optional[str] = None,
+    uid: str = "",
+    context: Optional[ToolContext] = None
+) -> ToolResult:
+    """
+    使用 glob 模式匹配文件
+
+    参考 OpenCode 实现
+
+    Args:
+        pattern: glob 模式(如 "*.py", "src/**/*.ts")
+        path: 搜索目录(默认当前目录)
+        uid: 用户 ID
+        context: 工具上下文
+
+    Returns:
+        ToolResult: 匹配的文件列表
+    """
+    # 确定搜索路径
+    search_path = Path(path) if path else Path.cwd()
+    if not search_path.is_absolute():
+        search_path = Path.cwd() / search_path
+
+    if not search_path.exists():
+        return ToolResult(
+            title="目录不存在",
+            output=f"搜索目录不存在: {path}",
+            error="Directory not found"
+        )
+
+    # 执行 glob 搜索
+    try:
+        # 使用 pathlib 的 glob(支持 ** 递归)
+        if "**" in pattern:
+            matches = list(search_path.glob(pattern))
+        else:
+            # 使用标准 glob(更快)
+            pattern_path = search_path / pattern
+            matches = [Path(p) for p in glob_module.glob(str(pattern_path))]
+
+        # 过滤掉目录,只保留文件
+        file_matches = [m for m in matches if m.is_file()]
+
+        # 按修改时间排序(参考 opencode:47-56)
+        file_matches_with_mtime = []
+        for file_path in file_matches:
+            try:
+                mtime = file_path.stat().st_mtime
+                file_matches_with_mtime.append((file_path, mtime))
+            except Exception:
+                file_matches_with_mtime.append((file_path, 0))
+
+        # 按修改时间降序排序(最新的在前)
+        file_matches_with_mtime.sort(key=lambda x: x[1], reverse=True)
+
+        # 限制数量
+        truncated = len(file_matches_with_mtime) > LIMIT
+        file_matches_with_mtime = file_matches_with_mtime[:LIMIT]
+
+        # 格式化输出
+        if not file_matches_with_mtime:
+            output = "未找到匹配的文件"
+        else:
+            file_paths = [str(f[0]) for f in file_matches_with_mtime]
+            output = "\n".join(file_paths)
+
+            if truncated:
+                output += f"\n\n(结果已截断。考虑使用更具体的路径或模式。)"
+
+        return ToolResult(
+            title=f"匹配: {pattern}",
+            output=output,
+            metadata={
+                "count": len(file_matches_with_mtime),
+                "truncated": truncated,
+                "pattern": pattern,
+                "search_path": str(search_path)
+            }
+        )
+
+    except Exception as e:
+        return ToolResult(
+            title="Glob 错误",
+            output=f"glob 匹配失败: {str(e)}",
+            error=str(e)
+        )

+ 218 - 0
agent/tools/builtin/grep.py

@@ -0,0 +1,218 @@
+"""
+Grep Tool - 内容搜索工具
+
+参考:vendor/opencode/packages/opencode/src/tool/grep.ts
+
+核心功能:
+- 在文件中搜索正则表达式模式
+- 支持文件类型过滤
+- 按修改时间排序结果
+"""
+
+import re
+import subprocess
+from pathlib import Path
+from typing import Optional, List, Tuple
+
+from agent.tools import tool, ToolResult, ToolContext
+
+# 常量
+LIMIT = 100  # 最大返回匹配数(参考 opencode grep.ts:107)
+MAX_LINE_LENGTH = 2000  # 最大行长度(参考 opencode grep.ts:10)
+
+
+@tool(description="在文件内容中搜索模式")
+async def grep_content(
+    pattern: str,
+    path: Optional[str] = None,
+    include: Optional[str] = None,
+    uid: str = "",
+    context: Optional[ToolContext] = None
+) -> ToolResult:
+    """
+    在文件中搜索正则表达式模式
+
+    参考 OpenCode 实现
+
+    优先使用 ripgrep(如果可用),否则使用 Python 实现。
+
+    Args:
+        pattern: 正则表达式模式
+        path: 搜索目录(默认当前目录)
+        include: 文件模式(如 "*.py", "*.{ts,tsx}")
+        uid: 用户 ID
+        context: 工具上下文
+
+    Returns:
+        ToolResult: 搜索结果
+    """
+    # 确定搜索路径
+    search_path = Path(path) if path else Path.cwd()
+    if not search_path.is_absolute():
+        search_path = Path.cwd() / search_path
+
+    if not search_path.exists():
+        return ToolResult(
+            title="目录不存在",
+            output=f"搜索目录不存在: {path}",
+            error="Directory not found"
+        )
+
+    # 尝试使用 ripgrep
+    try:
+        matches = await _ripgrep_search(pattern, search_path, include)
+    except Exception:
+        # ripgrep 不可用,使用 Python 实现
+        matches = await _python_search(pattern, search_path, include)
+
+    # 按修改时间排序(参考 opencode:105)
+    matches_with_mtime = []
+    for file_path, line_num, line_text in matches:
+        try:
+            mtime = file_path.stat().st_mtime
+            matches_with_mtime.append((file_path, line_num, line_text, mtime))
+        except Exception:
+            matches_with_mtime.append((file_path, line_num, line_text, 0))
+
+    matches_with_mtime.sort(key=lambda x: x[3], reverse=True)
+
+    # 限制数量
+    truncated = len(matches_with_mtime) > LIMIT
+    matches_with_mtime = matches_with_mtime[:LIMIT]
+
+    # 格式化输出(参考 opencode:118-133)
+    if not matches_with_mtime:
+        output = "未找到匹配"
+    else:
+        output = f"找到 {len(matches_with_mtime)} 个匹配\n"
+
+        current_file = None
+        for file_path, line_num, line_text, _ in matches_with_mtime:
+            if current_file != file_path:
+                if current_file is not None:
+                    output += "\n"
+                current_file = file_path
+                output += f"\n{file_path}:\n"
+
+            # 截断过长的行
+            if len(line_text) > MAX_LINE_LENGTH:
+                line_text = line_text[:MAX_LINE_LENGTH] + "..."
+
+            output += f"  Line {line_num}: {line_text}\n"
+
+        if truncated:
+            output += "\n(结果已截断。考虑使用更具体的路径或模式。)"
+
+    return ToolResult(
+        title=f"搜索: {pattern}",
+        output=output,
+        metadata={
+            "matches": len(matches_with_mtime),
+            "truncated": truncated,
+            "pattern": pattern
+        }
+    )
+
+
+async def _ripgrep_search(
+    pattern: str,
+    search_path: Path,
+    include: Optional[str]
+) -> List[Tuple[Path, int, str]]:
+    """
+    使用 ripgrep 搜索
+
+    参考 OpenCode 实现
+    """
+    args = [
+        "rg",
+        "-nH",  # 显示行号和文件名
+        "--hidden",
+        "--follow",
+        "--no-messages",
+        "--field-match-separator=|",
+        "--regexp", pattern
+    ]
+
+    if include:
+        args.extend(["--glob", include])
+
+    args.append(str(search_path))
+
+    # 执行 ripgrep
+    process = await subprocess.create_subprocess_exec(
+        *args,
+        stdout=subprocess.PIPE,
+        stderr=subprocess.PIPE
+    )
+
+    stdout, stderr = await process.communicate()
+    exit_code = process.returncode
+
+    # Exit codes: 0 = matches, 1 = no matches, 2 = errors
+    if exit_code == 1:
+        return []
+
+    if exit_code != 0 and exit_code != 2:
+        raise RuntimeError(f"ripgrep failed: {stderr.decode()}")
+
+    # 解析输出
+    matches = []
+    for line in stdout.decode('utf-8', errors='replace').strip().split('\n'):
+        if not line:
+            continue
+
+        parts = line.split('|', 2)
+        if len(parts) < 3:
+            continue
+
+        file_path_str, line_num_str, line_text = parts
+        matches.append((
+            Path(file_path_str),
+            int(line_num_str),
+            line_text
+        ))
+
+    return matches
+
+
+async def _python_search(
+    pattern: str,
+    search_path: Path,
+    include: Optional[str]
+) -> List[Tuple[Path, int, str]]:
+    """
+    使用 Python 正则实现搜索(fallback)
+    """
+    try:
+        regex = re.compile(pattern)
+    except Exception as e:
+        raise ValueError(f"无效的正则表达式: {e}")
+
+    matches = []
+
+    # 确定要搜索的文件
+    if include:
+        # 简单的 glob 匹配
+        import glob
+        file_pattern = str(search_path / "**" / include)
+        files = [Path(f) for f in glob.glob(file_pattern, recursive=True)]
+    else:
+        # 搜索所有文本文件
+        files = [f for f in search_path.rglob("*") if f.is_file()]
+
+    # 搜索文件内容
+    for file_path in files:
+        try:
+            with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
+                for line_num, line in enumerate(f, 1):
+                    if regex.search(line):
+                        matches.append((file_path, line_num, line.rstrip('\n')))
+
+                    # 限制数量避免过多搜索
+                    if len(matches) >= LIMIT * 2:
+                        return matches
+        except Exception:
+            continue
+
+    return matches

+ 229 - 0
agent/tools/builtin/read.py

@@ -0,0 +1,229 @@
+"""
+Read Tool - 文件读取工具
+
+参考 OpenCode read.ts 完整实现。
+
+核心功能:
+- 支持文本文件、图片、PDF
+- 分页读取(offset/limit)
+- 二进制文件检测
+- 行长度和字节限制
+"""
+
+import os
+import mimetypes
+from pathlib import Path
+from typing import Optional
+
+from agent.tools import tool, ToolResult, ToolContext
+
+# 常量(参考 opencode)
+DEFAULT_READ_LIMIT = 2000
+MAX_LINE_LENGTH = 2000
+MAX_BYTES = 50 * 1024  # 50KB
+
+
+@tool(description="读取文件内容,支持文本文件、图片、PDF 等多种格式")
+async def read_file(
+    file_path: str,
+    offset: int = 0,
+    limit: int = DEFAULT_READ_LIMIT,
+    uid: str = "",
+    context: Optional[ToolContext] = None
+) -> ToolResult:
+    """
+    读取文件内容
+
+    参考 OpenCode 实现
+
+    Args:
+        file_path: 文件路径(绝对路径或相对路径)
+        offset: 起始行号(从 0 开始)
+        limit: 读取行数(默认 2000 行)
+        uid: 用户 ID(自动注入)
+        context: 工具上下文
+
+    Returns:
+        ToolResult: 文件内容
+    """
+    # 解析路径
+    path = Path(file_path)
+    if not path.is_absolute():
+        path = Path.cwd() / path
+
+    # 检查文件是否存在
+    if not path.exists():
+        # 尝试提供建议(参考 opencode:44-60)
+        parent_dir = path.parent
+        if parent_dir.exists():
+            candidates = [
+                f for f in parent_dir.iterdir()
+                if path.name.lower() in f.name.lower() or f.name.lower() in path.name.lower()
+            ][:3]
+
+            if candidates:
+                suggestions = "\n".join(str(c) for c in candidates)
+                return ToolResult(
+                    title=f"文件未找到: {path.name}",
+                    output=f"文件不存在: {file_path}\n\n你是否想要:\n{suggestions}",
+                    error="File not found"
+                )
+
+        return ToolResult(
+            title="文件未找到",
+            output=f"文件不存在: {file_path}",
+            error="File not found"
+        )
+
+    # 检测文件类型
+    mime_type, _ = mimetypes.guess_type(str(path))
+    mime_type = mime_type or ""
+
+    # 图片文件(参考 opencode:66-91)
+    if mime_type.startswith("image/") and mime_type not in ["image/svg+xml", "image/vnd.fastbidsheet"]:
+        # 注意:实际项目中需要实现图片的 base64 编码
+        # 这里简化处理
+        return ToolResult(
+            title=path.name,
+            output=f"图片文件: {path.name} (MIME: {mime_type})",
+            metadata={"mime_type": mime_type, "truncated": False}
+        )
+
+    # PDF 文件
+    if mime_type == "application/pdf":
+        return ToolResult(
+            title=path.name,
+            output=f"PDF 文件: {path.name}",
+            metadata={"mime_type": mime_type, "truncated": False}
+        )
+
+    # 二进制文件检测(参考 opencode:156-211)
+    if _is_binary_file(path):
+        return ToolResult(
+            title="二进制文件",
+            output=f"无法读取二进制文件: {path.name}",
+            error="Binary file"
+        )
+
+    # 读取文本文件(参考 opencode:96-143)
+    try:
+        with open(path, 'r', encoding='utf-8') as f:
+            lines = f.readlines()
+
+        total_lines = len(lines)
+        end_line = min(offset + limit, total_lines)
+
+        # 截取行并处理长度限制
+        output_lines = []
+        total_bytes = 0
+        truncated_by_bytes = False
+
+        for i in range(offset, end_line):
+            line = lines[i].rstrip('\n\r')
+
+            # 行长度限制(参考 opencode:104)
+            if len(line) > MAX_LINE_LENGTH:
+                line = line[:MAX_LINE_LENGTH] + "..."
+
+            # 字节限制(参考 opencode:105-112)
+            line_bytes = len(line.encode('utf-8')) + (1 if output_lines else 0)
+            if total_bytes + line_bytes > MAX_BYTES:
+                truncated_by_bytes = True
+                break
+
+            output_lines.append(line)
+            total_bytes += line_bytes
+
+        # 格式化输出(参考 opencode:114-134)
+        formatted = []
+        for idx, line in enumerate(output_lines):
+            line_num = offset + idx + 1
+            formatted.append(f"{line_num:5d}| {line}")
+
+        output = "<file>\n" + "\n".join(formatted)
+
+        last_read_line = offset + len(output_lines)
+        has_more = total_lines > last_read_line
+        truncated = has_more or truncated_by_bytes
+
+        # 添加提示
+        if truncated_by_bytes:
+            output += f"\n\n(输出在 {MAX_BYTES} 字节处被截断。使用 'offset' 参数读取第 {last_read_line} 行之后的内容)"
+        elif has_more:
+            output += f"\n\n(文件还有更多内容。使用 'offset' 参数读取第 {last_read_line} 行之后的内容)"
+        else:
+            output += f"\n\n(文件结束 - 共 {total_lines} 行)"
+
+        output += "\n</file>"
+
+        # 预览(前 20 行)
+        preview = "\n".join(output_lines[:20])
+
+        return ToolResult(
+            title=path.name,
+            output=output,
+            metadata={
+                "preview": preview,
+                "truncated": truncated,
+                "total_lines": total_lines,
+                "read_lines": len(output_lines)
+            }
+        )
+
+    except UnicodeDecodeError:
+        return ToolResult(
+            title="编码错误",
+            output=f"无法解码文件(非 UTF-8 编码): {path.name}",
+            error="Encoding error"
+        )
+    except Exception as e:
+        return ToolResult(
+            title="读取错误",
+            output=f"读取文件时出错: {str(e)}",
+            error=str(e)
+        )
+
+
+def _is_binary_file(path: Path) -> bool:
+    """
+    检测是否为二进制文件
+
+    参考 OpenCode 实现
+    """
+    # 常见二进制扩展名
+    binary_exts = {
+        '.zip', '.tar', '.gz', '.exe', '.dll', '.so', '.class',
+        '.jar', '.war', '.7z', '.doc', '.docx', '.xls', '.xlsx',
+        '.ppt', '.pptx', '.odt', '.ods', '.odp', '.bin', '.dat',
+        '.obj', '.o', '.a', '.lib', '.wasm', '.pyc', '.pyo'
+    }
+
+    if path.suffix.lower() in binary_exts:
+        return True
+
+    # 检查文件内容
+    try:
+        file_size = path.stat().st_size
+        if file_size == 0:
+            return False
+
+        # 读取前 4KB
+        buffer_size = min(4096, file_size)
+        with open(path, 'rb') as f:
+            buffer = f.read(buffer_size)
+
+        # 检测 null 字节
+        if b'\x00' in buffer:
+            return True
+
+        # 统计非打印字符(参考 opencode:202-210)
+        non_printable = 0
+        for byte in buffer:
+            if byte < 9 or (13 < byte < 32):
+                non_printable += 1
+
+        # 如果超过 30% 是非打印字符,认为是二进制
+        return non_printable / len(buffer) > 0.3
+
+    except Exception:
+        return False

+ 113 - 0
agent/tools/builtin/write.py

@@ -0,0 +1,113 @@
+"""
+Write Tool - 文件写入工具
+
+参考:vendor/opencode/packages/opencode/src/tool/write.ts
+
+核心功能:
+- 创建新文件或覆盖现有文件
+- 生成 diff 预览
+"""
+
+from pathlib import Path
+from typing import Optional
+import difflib
+
+from agent.tools import tool, ToolResult, ToolContext
+
+
+@tool(description="写入文件内容(创建新文件或覆盖现有文件)")
+async def write_file(
+    file_path: str,
+    content: str,
+    uid: str = "",
+    context: Optional[ToolContext] = None
+) -> ToolResult:
+    """
+    写入文件
+
+    参考 OpenCode 实现
+
+    Args:
+        file_path: 文件路径
+        content: 文件内容
+        uid: 用户 ID
+        context: 工具上下文
+
+    Returns:
+        ToolResult: 写入结果
+    """
+    # 解析路径
+    path = Path(file_path)
+    if not path.is_absolute():
+        path = Path.cwd() / path
+
+    # 检查是否为目录
+    if path.exists() and path.is_dir():
+        return ToolResult(
+            title="路径错误",
+            output=f"路径是目录,不是文件: {file_path}",
+            error="Path is a directory"
+        )
+
+    # 读取旧内容(如果存在)
+    existed = path.exists()
+    old_content = ""
+    if existed:
+        try:
+            with open(path, 'r', encoding='utf-8') as f:
+                old_content = f.read()
+        except Exception:
+            old_content = ""
+
+    # 生成 diff
+    if existed and old_content:
+        diff = _create_diff(str(path), old_content, content)
+    else:
+        diff = f"(新建文件: {path.name})"
+
+    # 确保父目录存在
+    path.parent.mkdir(parents=True, exist_ok=True)
+
+    # 写入文件
+    try:
+        with open(path, 'w', encoding='utf-8') as f:
+            f.write(content)
+    except Exception as e:
+        return ToolResult(
+            title="写入失败",
+            output=f"无法写入文件: {str(e)}",
+            error=str(e)
+        )
+
+    # 统计
+    lines = content.count('\n')
+
+    return ToolResult(
+        title=path.name,
+        output=f"文件写入成功\n\n{diff}",
+        metadata={
+            "existed": existed,
+            "lines": lines,
+            "diff": diff
+        },
+        long_term_memory=f"{'覆盖' if existed else '创建'}文件 {path.name}"
+    )
+
+
+def _create_diff(filepath: str, old_content: str, new_content: str) -> str:
+    """生成 unified diff"""
+    old_lines = old_content.splitlines(keepends=True)
+    new_lines = new_content.splitlines(keepends=True)
+
+    diff_lines = list(difflib.unified_diff(
+        old_lines,
+        new_lines,
+        fromfile=f"a/{filepath}",
+        tofile=f"b/{filepath}",
+        lineterm=''
+    ))
+
+    if not diff_lines:
+        return "(无变更)"
+
+    return ''.join(diff_lines)

+ 111 - 78
docs/README.md

@@ -12,7 +12,7 @@
 2. **保持结构稳定** - 只增删内容,不随意调整层级结构
 2. **保持结构稳定** - 只增删内容,不随意调整层级结构
 3. **流程优先** - 新功能先写入核心流程,再补充模块详情
 3. **流程优先** - 新功能先写入核心流程,再补充模块详情
 4. **链接代码** - 关键实现标注文件路径,格式:`module/file.py:function_name`
 4. **链接代码** - 关键实现标注文件路径,格式:`module/file.py:function_name`
-5. **简洁原则** - 只记录最重要的信息,避免大量代码
+5. **简洁原则** - 只记录最重要的、与代码准确对应的或者明确的已完成的设计的信息,避免推测、建议,或大量代码
 6. **文档分层** - 每层文档是不同层次的overview,在上层文档对应位置引用下层详细文档
 6. **文档分层** - 每层文档是不同层次的overview,在上层文档对应位置引用下层详细文档
 
 
 ---
 ---
@@ -86,7 +86,7 @@ async def run(task: str, max_steps: int = 50):
         response = await llm.chat(
         response = await llm.chat(
             messages=messages,
             messages=messages,
             system=system_prompt,
             system=system_prompt,
-            tools=tool_registry.to_schema()  # 包括 skill 工具
+            tools=tool_registry.to_schema()  # 包括 skill、task 工具
         )
         )
 
 
         # 记录 LLM 调用
         # 记录 LLM 调用
@@ -105,7 +105,7 @@ async def run(task: str, max_steps: int = 50):
             if is_doom_loop(tool_call):
             if is_doom_loop(tool_call):
                 raise DoomLoopError()
                 raise DoomLoopError()
 
 
-            # 执行工具(包括 skill 工具)
+            # 执行工具(包括 skill、task 工具)
             result = await execute_tool(tool_call)
             result = await execute_tool(tool_call)
 
 
             # 记录步骤
             # 记录步骤
@@ -126,6 +126,25 @@ async def run(task: str, max_steps: int = 50):
 **关键机制**:
 **关键机制**:
 - **Doom Loop 检测**:跟踪最近 3 次工具调用,如果都是同一个工具且参数相同,中断循环
 - **Doom Loop 检测**:跟踪最近 3 次工具调用,如果都是同一个工具且参数相同,中断循环
 - **动态工具加载**:Skill 通过 tool 动态加载,按需消耗 context
 - **动态工具加载**:Skill 通过 tool 动态加载,按需消耗 context
+- **Sub-Agent 支持**:通过 task 工具启动专门化的 Sub-Agent 处理子任务
+
+### Sub-Agent 执行流程
+
+主 Agent 通过 `task` 工具启动 Sub-Agent。
+
+**层级关系**:
+```
+Main Trace (primary agent)
+  └─▶ Sub Trace (parent_trace_id 指向主 Trace)
+        └─▶ Steps...
+```
+
+**关键字段**:
+- `Trace.parent_trace_id` - 指向父 Trace
+- `Trace.agent_definition` - Sub-Agent 类型(如 "explore")
+- Sub-Agent 有独立的工具权限配置
+
+**实现位置**:`agent/tools/builtin/task.py`(待实现)
 
 
 ---
 ---
 
 
@@ -138,30 +157,27 @@ async def run(task: str, max_steps: int = 50):
 class Trace:
 class Trace:
     trace_id: str
     trace_id: str
     mode: Literal["call", "agent"]
     mode: Literal["call", "agent"]
-
-    # 任务信息
     task: Optional[str] = None
     task: Optional[str] = None
     agent_type: Optional[str] = None
     agent_type: Optional[str] = None
-
-    # 状态
     status: Literal["running", "completed", "failed"] = "running"
     status: Literal["running", "completed", "failed"] = "running"
 
 
-    # 上下文(灵活的元数据)
-    context: Dict[str, Any] = field(default_factory=dict)
+    # Sub-Agent 支持
+    parent_trace_id: Optional[str] = None      # 父 Trace ID
+    agent_definition: Optional[str] = None     # Agent 类型名称
+    spawned_by_tool: Optional[str] = None      # 启动此 Sub-Agent 的 Step ID
+
+    # 统计
+    total_steps: int = 0
+    total_tokens: int = 0
+    total_cost: float = 0.0
 
 
-    # 时间
-    created_at: datetime
-    completed_at: Optional[datetime] = None
+    # 上下文
+    uid: Optional[str] = None
+    context: Dict[str, Any] = field(default_factory=dict)
 ```
 ```
 
 
 **实现**:`agent/models/trace.py:Trace`
 **实现**:`agent/models/trace.py:Trace`
 
 
-**context 字段**:存储任务相关的元信息
-- `user_id`: 用户 ID
-- `project_id`: 项目 ID
-- `priority`: 优先级
-- `tags`: 标签列表
-
 ### Step(执行步骤)
 ### Step(执行步骤)
 
 
 ```python
 ```python
@@ -170,41 +186,12 @@ class Step:
     step_id: str
     step_id: str
     trace_id: str
     trace_id: str
     step_type: StepType  # "llm_call", "tool_call", "tool_result", ...
     step_type: StepType  # "llm_call", "tool_call", "tool_result", ...
-
-    # DAG 结构
     parent_ids: List[str] = field(default_factory=list)
     parent_ids: List[str] = field(default_factory=list)
-
-    # 灵活的步骤数据
     data: Dict[str, Any] = field(default_factory=dict)
     data: Dict[str, Any] = field(default_factory=dict)
-
-    created_at: datetime
 ```
 ```
 
 
 **实现**:`agent/models/trace.py:Step`
 **实现**:`agent/models/trace.py:Step`
 
 
-**常见 step_type**:
-- `llm_call`: LLM 调用(data: messages, response, tokens, cost)
-- `tool_call`: 工具调用(data: tool_name, arguments)
-- `tool_result`: 工具结果(data: output, metadata)
-- `reasoning`: 推理过程(data: content)
-
-### 执行图示例
-
-```
-Trace
-  │
-  ├─▶ Step(llm_call)
-  │     │
-  │     ├─▶ Step(tool_call: skill)
-  │     │     └─▶ Step(tool_result: "# Error Handling...")
-  │     │
-  │     └─▶ Step(tool_call: search_logs)
-  │           └─▶ Step(tool_result: "...")
-  │
-  └─▶ Step(llm_call)
-        └─▶ ...
-```
-
 ---
 ---
 
 
 ## 模块详情
 ## 模块详情
@@ -212,32 +199,30 @@ Trace
 详细的模块文档请参阅:
 详细的模块文档请参阅:
 
 
 ### [Sub-Agent 机制](./sub-agents.md)
 ### [Sub-Agent 机制](./sub-agents.md)
-- Sub-Agent 架构和类型定义
+- 数据模型:AgentDefinition、Trace 扩展
+- 内置 Sub-Agent:general、explore、analyst
 - Task Tool 实现
 - Task Tool 实现
-- 权限隔离和层级管理
-- 内置 Sub-Agent(explore、general、analyst)
-- 自定义 Sub-Agent 配置
+- Agent Registry 和权限控制
+- 配置文件格式
 
 
-**核心特性**:
-```python
-from agent.tools.builtin import task_tool
-
-# 主 Agent 通过 Task 工具启动 Sub-Agent
-result = await task_tool(
-    subagent_type="explore",
-    description="查找数据库配置",
-    prompt="在项目中查找所有数据库配置文件和连接代码",
-    ctx=tool_context
-)
-```
+**使用示例**:`examples/subagent_example.py`
 
 
 ### [工具系统](./tools.md)
 ### [工具系统](./tools.md)
 - 工具定义和注册
 - 工具定义和注册
 - 双层记忆管理
 - 双层记忆管理
 - 域名过滤、敏感数据处理
 - 域名过滤、敏感数据处理
+- 内置基础工具(文件操作、命令执行)
 - 集成 Browser-Use
 - 集成 Browser-Use
 - 最佳实践
 - 最佳实践
 
 
+**内置工具**:
+- `read_file`, `edit_file`, `write_file` - 文件操作
+- `bash_command` - 命令执行
+- `glob_files`, `grep_content` - 文件搜索
+
+**详细设计**:
+- 内置工具和适配器:参考 [`docs/tools-adapters.md`](./tools-adapters.md)
+
 **核心特性**:
 **核心特性**:
 ```python
 ```python
 from reson_agent import tool, ToolResult, ToolContext
 from reson_agent import tool, ToolResult, ToolContext
@@ -254,6 +239,53 @@ async def my_tool(arg: str, ctx: ToolContext) -> ToolResult:
     )
     )
 ```
 ```
 
 
+### [多模态支持](./multimodal.md)
+- Prompt 层多模态消息构建
+- OpenAI 格式消息规范
+- Gemini Provider 适配
+- 图片资源处理
+
+**实现**:
+- `agent/prompts/wrapper.py:SimplePrompt` - Prompt 包装器
+- `agent/llm/providers/gemini.py:_convert_messages_to_gemini` - 格式转换
+
+**使用示例**:`examples/feature_extract/run.py`
+
+### Prompt Loader(提示加载器)
+
+**职责**:加载和处理 `.prompt` 文件,支持多模态消息构建
+
+**文件格式**:
+```yaml
+---
+model: gemini-2.5-flash
+temperature: 0.3
+---
+
+$system$
+系统提示...
+
+$user$
+用户提示:%variable%
+```
+
+**核心功能**:
+- YAML frontmatter 解析(配置)
+- `$section$` 分节语法
+- `%variable%` 参数替换
+- 多模态消息支持(图片等)
+
+**实现**:
+- `agent/prompts/loader.py:load_prompt()` - 文件解析
+- `agent/prompts/loader.py:get_message()` - 参数替换
+- `agent/prompts/wrapper.py:SimplePrompt` - Prompt 包装器
+
+**使用**:
+```python
+prompt = SimplePrompt("task.prompt")
+messages = prompt.build_messages(text="...", images="img.png")
+```
+
 ### Skills(技能库)
 ### Skills(技能库)
 
 
 **存储**:Markdown 文件
 **存储**:Markdown 文件
@@ -434,25 +466,26 @@ agent/
 
 
 ---
 ---
 
 
-## 实现计划
+## 测试
 
 
-### Phase 1:MVP
-- [ ] AgentRunner 基础循环
-- [ ] 基础工具(read, skill)
-- [ ] 高级工具集成:Browser-Use、Search
-- [ ] 单次执行的监控与分析:Trace/Step 数据模型与文件系统存储、初步的执行历史可视化
+详见 [测试指南](./testing.md)
 
 
-### Phase 2:Sub-Agent 机制
-- [ ] Agent 定义系统:AgentDefinition、AgentRegistry
-- [ ] Trace 层级关系:parent_trace_id 支持
-- [ ] Task Tool:启动和管理 Sub-Agent
-- [ ] 内置 Sub-Agent:general、explore
-- [ ] 权限控制:工具过滤、路径限制
+**测试分层**:
+- **单元测试**:Agent 定义、工具系统、Trace 模型
+- **集成测试**:Sub-Agent、Trace 存储、多模块协作
+- **E2E 测试**:真实 LLM 调用(需要 API Key)
 
 
-### Phase 3:反思能力
-- [ ] Experience:feedback、归纳反思
-- [ ] 批量执行的监控与分析
+**运行测试**:
+```bash
+# 单元测试
+pytest tests/ -v -m "not e2e"
 
 
+# 覆盖率
+pytest --cov=agent tests/ -m "not e2e"
+
+# E2E 测试(可选)
+GEMINI_API_KEY=xxx pytest tests/e2e/ -v -m e2e
+```
 
 
 ---
 ---
 
 

+ 126 - 0
docs/multimodal.md

@@ -0,0 +1,126 @@
+# 多模态支持
+
+多模态消息(文本 + 图片)支持,遵循 OpenAI API 规范。
+
+---
+
+## 架构层次
+
+```
+Prompt 层 (SimplePrompt) → OpenAI 格式消息 → Provider 层适配 → 模型 API
+```
+
+**关键原则**:
+- 遵循 OpenAI API 消息格式规范
+- 模型适配封装在 Provider 层
+- 应用层通过 Prompt 层统一处理
+
+---
+
+## 核心实现
+
+### 1. Prompt 层多模态支持
+
+**实现位置**:`agent/prompts/wrapper.py:SimplePrompt`
+
+**功能**:构建 OpenAI 格式的多模态消息
+
+```python
+# 使用示例
+prompt = SimplePrompt("task.prompt")
+messages = prompt.build_messages(
+    text="内容",
+    images="path/to/image.png"  # 或 images=["img1.png", "img2.png"]
+)
+```
+
+**关键方法**:
+- `build_messages(**context)` - 构建消息列表,支持 `images` 参数
+- `_build_image_content(image)` - 将图片路径转为 OpenAI 格式(data URL)
+
+**消息格式**(OpenAI 规范):
+```python
+[
+  {"role": "system", "content": "系统提示"},
+  {
+    "role": "user",
+    "content": [
+      {"type": "text", "text": "..."},
+      {"type": "image_url", "image_url": {"url": "data:image/png;base64,..."}}
+    ]
+  }
+]
+```
+
+### 2. Gemini Provider 适配
+
+**实现位置**:`agent/llm/providers/gemini.py:_convert_messages_to_gemini`
+
+**功能**:将 OpenAI 多模态格式转换为 Gemini 格式
+
+**转换规则**:
+- 检测 `content` 是否为数组(多模态标志)
+- `{"type": "text"}` → Gemini `{"text": "..."}`
+- `{"type": "image_url"}` → Gemini `{"inline_data": {"mime_type": "...", "data": "..."}}`
+
+**关键逻辑**:
+```python
+# 处理多模态消息
+if isinstance(content, list):
+    parts = []
+    for item in content:
+        if item.get("type") == "text":
+            parts.append({"text": item.get("text")})
+        elif item.get("type") == "image_url":
+            # 解析 data URL 并转换
+            mime_type, base64_data = parse_data_url(url)
+            parts.append({"inline_data": {"mime_type": mime_type, "data": base64_data}})
+```
+
+---
+
+## 使用方式
+
+### .prompt 文件
+
+标准 `.prompt` 文件格式:
+```yaml
+---
+model: gemini-2.5-flash
+temperature: 0.3
+---
+
+$system$
+系统提示...
+
+$user$
+用户提示:%text%
+```
+
+### 应用层调用
+
+**参考示例**:`examples/feature_extract/run.py`
+
+```python
+# 1. 加载 prompt
+prompt = SimplePrompt("task.prompt")
+
+# 2. 构建消息(自动处理图片)
+messages = prompt.build_messages(text="...", images="img.png")
+
+# 3. 调用 Agent
+runner = AgentRunner(llm_call=create_gemini_llm_call())
+result = await runner.call(messages=messages, model="gemini-2.5-flash")
+```
+
+---
+
+## 扩展支持
+
+**当前支持**:
+- 图片格式:PNG, JPEG, GIF, WebP
+- 输入方式:文件路径或 base64 data URL
+
+**未来扩展**:
+- 音频、视频等其他模态
+- 资源缓存和异步加载

+ 0 - 274
docs/sub-agents-quickref.md

@@ -1,274 +0,0 @@
-# Sub-Agent 快速参考
-
-## 什么是 Sub-Agent?
-
-Sub-Agent 是在独立 Trace 中运行的专门化 Agent,用于处理特定类型的子任务。
-
-## 核心概念
-
-- **主 Agent (Primary)**: 直接接收用户请求,可以启动 Sub-Agent
-- **Sub-Agent**: 由主 Agent 通过 Task 工具启动,处理特定子任务
-- **Task Tool**: 启动 Sub-Agent 的工具
-- **Trace 层级**: Sub-Agent 运行在独立的 Trace 中,通过 parent_trace_id 关联
-
-## 内置 Sub-Agent
-
-| 名称 | 用途 | 允许工具 | 典型场景 |
-|------|------|---------|---------|
-| `general` | 通用多步骤任务 | 所有工具(除 task) | 数据收集、复杂分析 |
-| `explore` | 代码探索 | read, search, list | 查找代码、理解结构 |
-| `analyst` | 深度分析 | read, search, web | 技术栈分析、报告 |
-
-## 快速开始
-
-### 1. 主 Agent 调用 Sub-Agent
-
-```python
-from agent import AgentRunner, AgentConfig
-
-# 主 Agent 会自动知道如何使用 Sub-Agent
-runner = AgentRunner(llm_call=your_llm, config=AgentConfig())
-
-async for event in runner.run(
-    task="使用 explore sub-agent 分析这个项目的架构"
-):
-    # 处理事件
-    pass
-```
-
-### 2. 定义自定义 Sub-Agent
-
-```python
-from agent.models.agent import AgentDefinition
-from agent.agent_registry import get_agent_registry
-
-# 定义
-custom_agent = AgentDefinition(
-    name="code-reviewer",
-    description="代码审查专家",
-    mode="subagent",
-    allowed_tools=["read_file", "search_code"],
-    system_prompt="你是代码审查专家...",
-)
-
-# 注册
-get_agent_registry().register(custom_agent)
-```
-
-### 3. 配置文件方式
-
-创建 `sub_agents.json`:
-
-```json
-{
-  "agents": {
-    "my-agent": {
-      "description": "我的自定义 Agent",
-      "mode": "subagent",
-      "allowed_tools": ["read_file", "search_code"],
-      "system_prompt": "..."
-    }
-  }
-}
-```
-
-加载配置:
-
-```python
-get_agent_registry().load_from_config("sub_agents.json")
-```
-
-## Agent 模式
-
-- `primary`: 主 Agent,可以被用户直接调用,可以启动 Sub-Agent
-- `subagent`: Sub-Agent,只能被其他 Agent 通过 Task 工具调用
-- `all`: 两者皆可
-
-## 权限控制
-
-### 工具级别
-
-```python
-AgentDefinition(
-    allowed_tools=["read_file", "search_code"],  # 白名单
-    denied_tools=["write_file", "execute_bash"], # 黑名单
-)
-```
-
-### 路径级别
-
-```python
-AgentDefinition(
-    permissions={
-        "paths": {
-            "/etc": "deny",
-            "~/.ssh": "deny",
-            "/tmp": "allow",
-        }
-    }
-)
-```
-
-### 防止递归
-
-```python
-AgentDefinition(
-    can_spawn_subagent=False,  # 默认为 False
-    denied_tools=["task"],     # 显式禁止 Task 工具
-)
-```
-
-## 事件监控
-
-```python
-async for event in runner.run(task="..."):
-    if event.type == "tool_call" and event.data["tool"] == "task":
-        # Sub-Agent 启动
-        print(f"启动: {event.data['args']['subagent_type']}")
-
-    elif event.type == "tool_result" and event.data["tool"] == "task":
-        # Sub-Agent 完成
-        metadata = event.data["metadata"]
-        print(f"完成: {metadata['sub_trace_id']}")
-```
-
-## Trace 查询
-
-```python
-from agent.storage import TraceStore
-
-# 获取主 Trace
-main_trace = await trace_store.get_trace(trace_id)
-
-# 查询所有子 Trace
-sub_traces = await trace_store.query_traces(
-    parent_trace_id=trace_id
-)
-
-# 获取 Sub-Agent 的详细步骤
-for sub_trace in sub_traces:
-    steps = await trace_store.get_steps(sub_trace.trace_id)
-    print(f"{sub_trace.agent_type}: {len(steps)} steps")
-```
-
-## 最佳实践
-
-### 1. 合理选择 Sub-Agent
-
-- 只读任务 → `explore`
-- 多步骤复杂任务 → `general`
-- 专业领域分析 → 自定义 Sub-Agent
-
-### 2. 限制权限
-
-```python
-# ✅ 好的实践:最小权限原则
-AgentDefinition(
-    allowed_tools=["read_file", "search_code"],  # 只给必要的工具
-    can_spawn_subagent=False,                    # 禁止嵌套
-)
-
-# ❌ 避免:过于宽松的权限
-AgentDefinition(
-    allowed_tools=None,  # 所有工具都可用
-    can_spawn_subagent=True,  # 允许无限嵌套
-)
-```
-
-### 3. 设置合理的迭代次数
-
-```python
-AgentDefinition(
-    max_iterations=15,  # 探索型任务:10-20 次
-    # max_iterations=30,  # 复杂分析任务:20-40 次
-)
-```
-
-### 4. 优化 System Prompt
-
-```python
-system_prompt = """你是 XXX 专家。
-
-专注于:
-1. [核心职责 1]
-2. [核心职责 2]
-
-注意事项:
-- [限制条件 1]
-- [限制条件 2]
-
-输出格式:
-[期望的输出格式]
-"""
-```
-
-### 5. 监控成本
-
-```python
-# 在 Trace 中追踪每个 Sub-Agent 的成本
-trace = await trace_store.get_trace(trace_id)
-print(f"总成本: ${trace.total_cost:.4f}")
-print(f"Token 消耗: {trace.total_tokens}")
-
-# 查看子 Trace 的成本
-for sub_trace in await trace_store.query_traces(parent_trace_id=trace_id):
-    print(f"  {sub_trace.agent_type}: ${sub_trace.total_cost:.4f}")
-```
-
-## 常见问题
-
-### Q: Sub-Agent 可以启动其他 Sub-Agent 吗?
-
-A: 默认不可以。可以通过设置 `can_spawn_subagent=True` 允许,但要注意:
-- 设置最大嵌套深度(建议 2-3 层)
-- 监控成本和性能
-- 避免无限递归
-
-### Q: 如何限制 Sub-Agent 的执行时间?
-
-A: 可以通过以下方式:
-```python
-AgentDefinition(
-    max_iterations=10,  # 限制迭代次数
-    permissions={
-        "limits": {
-            "timeout_seconds": 120  # 2 分钟超时
-        }
-    }
-)
-```
-
-### Q: Sub-Agent 可以访问主 Agent 的上下文吗?
-
-A: 可以通过 Task Tool 传递必要的上下文:
-```python
-# 主 Agent
-prompt = f"""
-分析用户 {user_id} 的项目 {project_id}。
-
-项目路径: {project_path}
-关注点: {focus_areas}
-"""
-
-task_tool(
-    subagent_type="explore",
-    description="分析项目",
-    prompt=prompt,  # 完整的上下文信息
-    ctx=ctx
-)
-```
-
-### Q: 如何调试 Sub-Agent 的行为?
-
-A:
-1. 查看 Trace 和 Steps
-2. 启用详细日志
-3. 检查工具调用历史
-4. 使用可视化工具分析调用链
-
-## 相关文档
-
-- [完整设计文档](../docs/sub-agents.md)
-- [使用示例](../examples/subagent_example.py)
-- [配置示例](../sub_agents.json.example)
-- [架构文档](../docs/README.md)

+ 94 - 558
docs/sub-agents.md

@@ -1,697 +1,233 @@
-# Sub-Agent 机制设计
+# Sub-Agent 实现指南
 
 
-> **可执行规格书**:本文档定义 Sub-Agent 架构。代码修改必须同步更新此文档。
->
-> 📖 **快速开始**:查看 [快速参考指南](./sub-agents-quickref.md) 了解常用操作
+> **可执行规格书**:Sub-Agent 实现细节。代码修改必须同步更新此文档。
 
 
 ---
 ---
 
 
-## 概述
+## 数据模型
 
 
-**Sub-Agent** 是在独立 Trace 中运行的专门化 Agent,用于处理复杂的子任务。
-
-### 核心特性
-
-- **任务隔离**:每个 Sub-Agent 在独立的 Trace 中运行,有完整的执行记录
-- **权限控制**:Sub-Agent 有独立的权限配置,限制其行为范围
-- **层级关系**:通过 `parent_trace_id` 建立父子关系,支持多层嵌套
-- **专门化**:不同类型的 Sub-Agent 专注于特定领域(如代码探索、研究分析)
-- **防止递归**:默认禁止 Sub-Agent 再启动其他 Sub-Agent,避免无限嵌套
-
-### 与主 Agent 的区别
-
-| 特性 | 主 Agent (Primary) | Sub-Agent |
-|------|-------------------|-----------|
-| 触发方式 | 用户直接调用 | 主 Agent 通过 Task 工具调用 |
-| 运行环境 | 独立 Trace | 独立 Trace(有父 Trace) |
-| 权限范围 | 完整权限 | 受限权限(可配置) |
-| 工具访问 | 所有工具 | 受限工具集 |
-| 可见性 | 用户可见 | 主 Agent 可见,用户可选可见 |
-
----
-
-## 架构设计
-
-### 1. Agent 类型定义
+### AgentDefinition
 
 
 ```python
 ```python
 # agent/models/agent.py
 # agent/models/agent.py
 
 
 @dataclass
 @dataclass
 class AgentDefinition:
 class AgentDefinition:
-    """Agent 定义"""
     name: str
     name: str
     description: Optional[str] = None
     description: Optional[str] = None
     mode: Literal["primary", "subagent", "all"] = "all"
     mode: Literal["primary", "subagent", "all"] = "all"
 
 
     # 权限配置
     # 权限配置
+    allowed_tools: Optional[List[str]] = None  # 白名单
+    denied_tools: Optional[List[str]] = None   # 黑名单
     permissions: Dict[str, Any] = field(default_factory=dict)
     permissions: Dict[str, Any] = field(default_factory=dict)
 
 
-    # 工具限制
-    allowed_tools: Optional[List[str]] = None
-    denied_tools: Optional[List[str]] = None
-
     # 模型配置
     # 模型配置
     model: Optional[str] = None
     model: Optional[str] = None
     temperature: Optional[float] = None
     temperature: Optional[float] = None
     max_iterations: Optional[int] = None
     max_iterations: Optional[int] = None
-
-    # 自定义 System Prompt
     system_prompt: Optional[str] = None
     system_prompt: Optional[str] = None
 
 
     # 是否可以调用其他 Sub-Agent
     # 是否可以调用其他 Sub-Agent
     can_spawn_subagent: bool = False
     can_spawn_subagent: bool = False
 ```
 ```
 
 
-**实现位置**:`agent/models/agent.py:AgentDefinition`
+**实现位置**:`agent/models/agent.py:AgentDefinition`(待实现)
 
 
-### 2. Trace 层级关系
+### Trace 扩展
 
 
 ```python
 ```python
 # agent/models/trace.py (扩展现有模型)
 # agent/models/trace.py (扩展现有模型)
 
 
 @dataclass
 @dataclass
 class Trace:
 class Trace:
-    trace_id: str
-    mode: Literal["call", "agent"]
-
-    # 新增:Sub-Agent 支持
-    parent_trace_id: Optional[str] = None  # 父 Trace ID
-    agent_definition: Optional[str] = None  # Agent 类型名称
-    spawned_by_tool: Optional[str] = None   # 启动此 Sub-Agent 的工具调用 ID
-
-    # 原有字段...
-    task: Optional[str] = None
-    agent_type: Optional[str] = None
-    status: Literal["running", "completed", "failed"] = "running"
-    # ...
-```
-
-**实现位置**:`agent/models/trace.py:Trace`(扩展现有类)
-
-### 3. Task Tool 实现
-
-Task Tool 是启动 Sub-Agent 的核心工具:
+    # ... 原有字段
 
 
-```python
-# agent/tools/builtin/task.py
-
-@tool(
-    name="task",
-    description="启动sub-agent处理复杂的子任务",
-    requires_confirmation=False
-)
-async def task_tool(
-    subagent_type: str,  # Sub-Agent 类型
-    description: str,     # 任务简短描述(3-5词)
-    prompt: str,          # 详细任务描述
-    ctx: ToolContext
-) -> ToolResult:
-    """
-    启动一个 Sub-Agent 执行子任务
-
-    Args:
-        subagent_type: Sub-Agent 类型(如 "explore", "general")
-        description: 任务简短描述
-        prompt: 完整的任务描述
-        ctx: 工具上下文
-
-    Returns:
-        Sub-Agent 的执行结果
-    """
-    # 1. 验证 Sub-Agent 类型
-    agent_def = await get_agent_definition(subagent_type)
-    if not agent_def or agent_def.mode == "primary":
-        raise ValueError(f"Invalid subagent type: {subagent_type}")
-
-    # 2. 检查权限
-    if not ctx.current_agent.can_spawn_subagent:
-        raise PermissionError("Current agent cannot spawn sub-agents")
-
-    # 3. 创建子 Trace
-    sub_trace = Trace.create(
-        mode="agent",
-        parent_trace_id=ctx.trace_id,
-        agent_definition=subagent_type,
-        spawned_by_tool=ctx.step_id,
-        task=prompt,
-        agent_type=subagent_type,
-        context={
-            "parent_task": ctx.trace.task,
-            "description": description,
-        }
-    )
-    sub_trace_id = await ctx.trace_store.create_trace(sub_trace)
-
-    # 4. 配置 Sub-Agent Runner
-    sub_config = AgentConfig(
-        agent_type=subagent_type,
-        max_iterations=agent_def.max_iterations or 10,
-        # 继承父 Agent 的某些配置
-        skills_dir=ctx.runner.config.skills_dir,
-    )
-
-    sub_runner = AgentRunner(
-        trace_store=ctx.trace_store,
-        memory_store=ctx.memory_store,
-        state_store=ctx.state_store,
-        tool_registry=_build_restricted_registry(agent_def),
-        llm_call=ctx.runner.llm_call,
-        config=sub_config,
-    )
-
-    # 5. 运行 Sub-Agent(收集所有事件)
-    events = []
-    async for event in sub_runner.run(
-        task=prompt,
-        model=agent_def.model or ctx.model,
-        trace_id=sub_trace_id,
-    ):
-        events.append(event)
-        # 可选:将事件转发给父 Agent
-
-    # 6. 提取结果
-    conclusion_events = [e for e in events if e.type == "conclusion"]
-    final_result = conclusion_events[-1].data["content"] if conclusion_events else ""
-
-    # 7. 生成摘要
-    tool_summary = _summarize_tool_calls(events)
-
-    output = f"{final_result}\n\n<task_metadata>\n"
-    output += f"sub_trace_id: {sub_trace_id}\n"
-    output += f"total_steps: {len(events)}\n"
-    output += f"tool_calls: {tool_summary}\n"
-    output += "</task_metadata>"
-
-    return ToolResult(
-        title=description,
-        output=output,
-        metadata={
-            "sub_trace_id": sub_trace_id,
-            "subagent_type": subagent_type,
-            "tool_summary": tool_summary,
-        }
-    )
-
-
-def _build_restricted_registry(agent_def: AgentDefinition) -> ToolRegistry:
-    """根据 Agent 定义构建受限的工具注册表"""
-    registry = ToolRegistry()
-    global_registry = get_tool_registry()
-
-    for tool_name, tool_def in global_registry.tools.items():
-        # 检查是否在允许列表中
-        if agent_def.allowed_tools and tool_name not in agent_def.allowed_tools:
-            continue
-
-        # 检查是否在拒绝列表中
-        if agent_def.denied_tools and tool_name in agent_def.denied_tools:
-            continue
-
-        registry.register_tool(tool_def)
-
-    # 默认禁止 task 工具(防止递归)
-    if not agent_def.can_spawn_subagent:
-        registry.tools.pop("task", None)
-
-    return registry
-
-
-def _summarize_tool_calls(events: List[AgentEvent]) -> str:
-    """总结工具调用情况"""
-    tool_calls = [e for e in events if e.type == "tool_call"]
-    summary = {}
-    for event in tool_calls:
-        tool = event.data.get("tool", "unknown")
-        summary[tool] = summary.get(tool, 0) + 1
-
-    return ", ".join(f"{tool}×{count}" for tool, count in summary.items())
+    # Sub-Agent 支持(新增)
+    parent_trace_id: Optional[str] = None    # 父 Trace ID
+    agent_definition: Optional[str] = None   # Agent 类型名称
+    spawned_by_tool: Optional[str] = None    # 启动此 Sub-Agent 的 Step ID
 ```
 ```
 
 
-**实现位置**:`agent/tools/builtin/task.py:task_tool`
+**实现位置**:`agent/models/trace.py:Trace`
 
 
 ---
 ---
 
 
-## 内置 Sub-Agent 类型
+## 内置 Sub-Agent
 
 
 ### 1. general - 通用型
 ### 1. general - 通用型
 
 
-**用途**:执行复杂的多步骤任务和研究分析
-
 ```python
 ```python
 GENERAL_AGENT = AgentDefinition(
 GENERAL_AGENT = AgentDefinition(
     name="general",
     name="general",
-    description="通用型 Sub-Agent,用于执行复杂的多步骤任务和研究分析",
+    description="通用型 Sub-Agent,执行复杂的多步骤任务",
     mode="subagent",
     mode="subagent",
-    allowed_tools=None,  # 允许所有工具
-    denied_tools=["task"],  # 禁止启动其他 Sub-Agent
+    denied_tools=["task"],
     max_iterations=20,
     max_iterations=20,
 )
 )
 ```
 ```
 
 
-**典型使用场景**:
-- 多步骤的数据收集和分析
-- 复杂的文件处理任务
-- 需要多个工具协同的任务
+**用途**:多步骤任务、数据收集、复杂分析
 
 
 ### 2. explore - 探索型
 ### 2. explore - 探索型
 
 
-**用途**:快速探索代码库,查找文件和代码
-
 ```python
 ```python
 EXPLORE_AGENT = AgentDefinition(
 EXPLORE_AGENT = AgentDefinition(
     name="explore",
     name="explore",
-    description="探索型 Sub-Agent,专门用于快速探索代码库、查找文件和搜索代码",
+    description="探索型 Sub-Agent,快速查找文件和代码",
     mode="subagent",
     mode="subagent",
-    allowed_tools=[
-        "read_file",
-        "list_files",
-        "search_code",
-        "search_files",
-    ],
-    denied_tools=[
-        "write_file",
-        "edit_file",
-        "execute_bash",
-        "task",
-    ],
+    allowed_tools=["read_file", "list_files", "search_code", "search_files"],
+    denied_tools=["write_file", "edit_file", "execute_bash", "task"],
     max_iterations=15,
     max_iterations=15,
-    system_prompt="""你是一个代码探索专家。专注于:
-1. 快速定位相关文件和代码
-2. 理解代码结构和依赖关系
-3. 总结关键信息
-
-注意:
-- 你只能读取和搜索,不能编辑或执行代码
-- 优先使用搜索工具,而不是逐个读取文件
-- 提供清晰的文件路径和行号引用
-""",
 )
 )
 ```
 ```
 
 
-**典型使用场景**:
-- "找出所有使用了 Redis 的代码"
-- "分析 API 路由的实现方式"
-- "查找配置文件的加载逻辑"
+**用途**:代码库探索、文件查找、结构分析
 
 
 ### 3. analyst - 分析型
 ### 3. analyst - 分析型
 
 
-**用途**:深度分析和报告生成
-
 ```python
 ```python
 ANALYST_AGENT = AgentDefinition(
 ANALYST_AGENT = AgentDefinition(
     name="analyst",
     name="analyst",
-    description="分析型 Sub-Agent,专注于深度分析和报告生成",
+    description="分析型 Sub-Agent,深度分析和报告生成",
     mode="subagent",
     mode="subagent",
-    allowed_tools=[
-        "read_file",
-        "list_files",
-        "search_code",
-        "web_search",
-        "fetch_url",
-    ],
+    allowed_tools=["read_file", "list_files", "search_code", "web_search", "fetch_url"],
     denied_tools=["task", "write_file", "edit_file"],
     denied_tools=["task", "write_file", "edit_file"],
     max_iterations=25,
     max_iterations=25,
-    temperature=0.3,  # 更精确的分析
+    temperature=0.3,
 )
 )
 ```
 ```
 
 
-**典型使用场景**:
-- 技术栈分析
-- 性能瓶颈分析
-- 安全审计报告
+**用途**:技术栈分析、性能分析、安全审计
+
+**实现位置**:`agent/builtin_agents.py`(待实现)
 
 
 ---
 ---
 
 
-## Agent 配置系统
+## 核心 API
 
 
-### 配置文件格式
+### Task Tool
 
 
-```json
-{
-  "agents": {
-    "custom-reviewer": {
-      "description": "代码审查专家",
-      "mode": "subagent",
-      "allowed_tools": ["read_file", "search_code", "list_files"],
-      "denied_tools": ["write_file", "edit_file", "execute_bash"],
-      "max_iterations": 10,
-      "temperature": 0.2,
-      "system_prompt": "你是一个代码审查专家...",
-      "can_spawn_subagent": false
-    },
-    "my-primary": {
-      "description": "自定义主 Agent",
-      "mode": "primary",
-      "can_spawn_subagent": true
-    }
-  },
-  "default_agent": "my-primary"
-}
+```python
+# agent/tools/builtin/task.py
+
+@tool(name="task")
+async def task_tool(
+    subagent_type: str,      # Sub-Agent 类型
+    description: str,         # 任务简短描述(3-5词)
+    prompt: str,              # 详细任务描述
+    ctx: ToolContext
+) -> ToolResult:
+    """启动 Sub-Agent 处理子任务"""
 ```
 ```
 
 
-### 加载和管理
+**实现位置**:`agent/tools/builtin/task.py:task_tool`(待实现)
+
+### Agent Registry
 
 
 ```python
 ```python
 # agent/agent_registry.py
 # agent/agent_registry.py
 
 
 class AgentRegistry:
 class AgentRegistry:
-    """Agent 注册表"""
-
-    def __init__(self):
-        self.agents: Dict[str, AgentDefinition] = {}
-        self._load_builtin_agents()
-
-    def _load_builtin_agents(self):
-        """加载内置 Agent"""
-        self.register(GENERAL_AGENT)
-        self.register(EXPLORE_AGENT)
-        self.register(ANALYST_AGENT)
-
-    def load_from_config(self, config_path: str):
-        """从配置文件加载自定义 Agent"""
-        import json
-        with open(config_path) as f:
-            config = json.load(f)
-
-        for name, cfg in config.get("agents", {}).items():
-            agent_def = AgentDefinition(
-                name=name,
-                **cfg
-            )
-            self.register(agent_def)
-
-    def register(self, agent: AgentDefinition):
+    def register(self, agent: AgentDefinition) -> None:
         """注册 Agent"""
         """注册 Agent"""
-        self.agents[agent.name] = agent
 
 
     def get(self, name: str) -> Optional[AgentDefinition]:
     def get(self, name: str) -> Optional[AgentDefinition]:
         """获取 Agent 定义"""
         """获取 Agent 定义"""
-        return self.agents.get(name)
 
 
     def list_subagents(self) -> List[AgentDefinition]:
     def list_subagents(self) -> List[AgentDefinition]:
         """列出所有可用的 Sub-Agent"""
         """列出所有可用的 Sub-Agent"""
-        return [
-            agent for agent in self.agents.values()
-            if agent.mode in ("subagent", "all")
-        ]
 
 
+    def load_from_config(self, config_path: str) -> None:
+        """从配置文件加载"""
 
 
 # 全局注册表
 # 全局注册表
-_agent_registry = AgentRegistry()
-
 def get_agent_registry() -> AgentRegistry:
 def get_agent_registry() -> AgentRegistry:
     """获取全局 Agent 注册表"""
     """获取全局 Agent 注册表"""
-    return _agent_registry
-
-async def get_agent_definition(name: str) -> Optional[AgentDefinition]:
-    """获取 Agent 定义"""
-    return _agent_registry.get(name)
 ```
 ```
 
 
-**实现位置**:`agent/agent_registry.py:AgentRegistry`
+**实现位置**:`agent/agent_registry.py:AgentRegistry`(待实现)
 
 
 ---
 ---
 
 
-## 权限控制
+## 配置文件
 
 
-### 权限模型
+### 格式
 
 
-每个 Agent 可以定义细粒度的权限:
-
-```python
-PERMISSIONS_EXAMPLE = {
-    # 工具级别权限
-    "tools": {
-        "write_file": "deny",
-        "read_file": "allow",
-        "execute_bash": "deny",
-    },
-
-    # 路径级别权限
-    "paths": {
-        "/etc": "deny",
-        "/tmp": "allow",
-        "~/.ssh": "deny",
-    },
-
-    # 网络级别权限
-    "network": {
-        "allowed_domains": ["*.example.com", "api.github.com"],
-        "blocked_domains": ["*.evil.com"],
-    },
-
-    # 资源限制
-    "limits": {
-        "max_file_size": 10_000_000,  # 10MB
-        "max_iterations": 15,
-        "timeout_seconds": 300,
+```json
+{
+  "agents": {
+    "code-reviewer": {
+      "description": "代码审查专家",
+      "mode": "subagent",
+      "allowed_tools": ["read_file", "search_code", "list_files"],
+      "max_iterations": 15,
+      "temperature": 0.2,
+      "system_prompt": "你是代码审查专家...",
+      "can_spawn_subagent": false
     }
     }
+  }
 }
 }
 ```
 ```
 
 
-### 权限检查流程
-
-```python
-# agent/permission.py
-
-class PermissionChecker:
-    """权限检查器"""
-
-    def __init__(self, agent_def: AgentDefinition):
-        self.agent_def = agent_def
-        self.permissions = agent_def.permissions
-
-    def check_tool_access(self, tool_name: str) -> bool:
-        """检查工具访问权限"""
-        # 检查拒绝列表
-        if self.agent_def.denied_tools and tool_name in self.agent_def.denied_tools:
-            return False
-
-        # 检查允许列表
-        if self.agent_def.allowed_tools and tool_name not in self.agent_def.allowed_tools:
-            return False
-
-        # 检查工具权限配置
-        tool_perms = self.permissions.get("tools", {})
-        if tool_perms.get(tool_name) == "deny":
-            return False
-
-        return True
+**配置示例**:`sub_agents.json.example`
 
 
-    def check_path_access(self, path: str, mode: str = "read") -> bool:
-        """检查路径访问权限"""
-        path_perms = self.permissions.get("paths", {})
+### 加载
 
 
-        for pattern, action in path_perms.items():
-            if self._match_path(path, pattern):
-                return action == "allow"
-
-        # 默认允许读取,拒绝写入
-        return mode == "read"
+```python
+from agent.agent_registry import get_agent_registry
 
 
-    def _match_path(self, path: str, pattern: str) -> bool:
-        """路径匹配(支持通配符)"""
-        import fnmatch
-        return fnmatch.fnmatch(path, pattern)
+# 加载配置
+get_agent_registry().load_from_config("sub_agents.json")
 ```
 ```
 
 
-**实现位置**:`agent/permission.py:PermissionChecker`
-
 ---
 ---
 
 
 ## 使用示例
 ## 使用示例
 
 
-### 1. 主 Agent 调用 Sub-Agent
+### 基本使用
 
 
 ```python
 ```python
-# 主 Agent 的 System Prompt 中会包含 Task 工具说明
-system_prompt = """
-你是一个智能助手。当遇到复杂任务时,可以使用 task 工具启动专门的 Sub-Agent。
-
-可用的 Sub-Agent:
-- explore: 探索代码库,查找文件和代码
-- general: 执行复杂的多步骤任务
-- analyst: 深度分析和报告生成
-
-示例:
-用户:"这个项目用了哪些数据库?"
-你:使用 task(subagent_type="explore", description="查找数据库使用", prompt="...")
-"""
-
-# 主 Agent 执行时
-async for event in runner.run(task="分析这个项目的架构"):
-    if event.type == "tool_call" and event.data["tool"] == "task":
-        # Sub-Agent 被启动
-        print(f"启动 Sub-Agent: {event.data['args']['subagent_type']}")
+from agent import AgentRunner, AgentConfig
+
+runner = AgentRunner(
+    llm_call=your_llm,
+    config=AgentConfig(max_iterations=20),
+)
+
+# 主 Agent 会自动使用 task 工具启动 Sub-Agent
+async for event in runner.run(
+    task="使用 explore sub-agent 查找所有配置文件"
+):
+    if event.type == "conclusion":
+        print(event.data["content"])
 ```
 ```
 
 
-### 2. 自定义 Sub-Agent
+### 自定义 Sub-Agent
 
 
 ```python
 ```python
-from agent import AgentRunner, AgentConfig
-from agent.agent_registry import get_agent_registry
 from agent.models.agent import AgentDefinition
 from agent.models.agent import AgentDefinition
+from agent.agent_registry import get_agent_registry
 
 
-# 1. 定义自定义 Sub-Agent
-custom_agent = AgentDefinition(
+# 定义
+custom = AgentDefinition(
     name="security-scanner",
     name="security-scanner",
     description="安全扫描专家",
     description="安全扫描专家",
     mode="subagent",
     mode="subagent",
-    allowed_tools=["read_file", "search_code", "list_files"],
-    system_prompt="""你是一个安全扫描专家。专注于:
-1. 查找常见安全漏洞(SQL注入、XSS、CSRF等)
-2. 检查敏感信息泄露(密钥、密码等)
-3. 分析依赖项的安全问题
-
-输出格式:
-- 漏洞类型
-- 影响范围
-- 修复建议
-""",
-    max_iterations=20,
+    allowed_tools=["read_file", "search_code"],
 )
 )
 
 
-# 2. 注册到全局注册表
-get_agent_registry().register(custom_agent)
-
-# 3. 主 Agent 可以调用它
-# 在 System Prompt 中会自动列出这个新的 Sub-Agent
+# 注册
+get_agent_registry().register(custom)
 ```
 ```
 
 
-### 3. 监控 Sub-Agent 执行
-
-```python
-from agent.storage import TraceStore
-
-async def analyze_subagent_performance(trace_id: str, trace_store: TraceStore):
-    """分析 Sub-Agent 性能"""
-    trace = await trace_store.get_trace(trace_id)
-    steps = await trace_store.get_steps(trace_id)
-
-    # 找出所有 Sub-Agent 调用
-    subagent_calls = []
-    for step in steps:
-        if step.step_type == "tool_call" and step.data.get("tool") == "task":
-            sub_trace_id = step.data.get("result", {}).get("metadata", {}).get("sub_trace_id")
-            if sub_trace_id:
-                sub_trace = await trace_store.get_trace(sub_trace_id)
-                subagent_calls.append({
-                    "type": sub_trace.agent_type,
-                    "task": sub_trace.task,
-                    "steps": sub_trace.total_steps,
-                    "tokens": sub_trace.total_tokens,
-                    "cost": sub_trace.total_cost,
-                })
-
-    # 生成报告
-    print(f"主任务: {trace.task}")
-    print(f"Sub-Agent 调用次数: {len(subagent_calls)}")
-    for i, call in enumerate(subagent_calls, 1):
-        print(f"\n{i}. {call['type']}")
-        print(f"   任务: {call['task'][:50]}...")
-        print(f"   步骤: {call['steps']}, Token: {call['tokens']}, 成本: ${call['cost']:.4f}")
-```
-
----
-
-## 实现计划
-
-### Phase 1: 基础架构(MVP)
-
-- [ ] **AgentDefinition 模型** (`agent/models/agent.py`)
-  - 定义 Agent 类型、权限、配置
-
-- [ ] **Trace 扩展** (`agent/models/trace.py`)
-  - 添加 `parent_trace_id`、`agent_definition` 等字段
-
-- [ ] **AgentRegistry** (`agent/agent_registry.py`)
-  - Agent 注册和管理
-  - 加载内置和自定义 Agent
-
-- [ ] **Task Tool** (`agent/tools/builtin/task.py`)
-  - 实现 Sub-Agent 启动逻辑
-  - Trace 层级管理
-  - 结果聚合
-
-- [ ] **内置 Sub-Agent** (`agent/builtin_agents.py`)
-  - `general`: 通用型
-  - `explore`: 探索型
-
-### Phase 2: 权限控制
-
-- [ ] **PermissionChecker** (`agent/permission.py`)
-  - 工具级别权限检查
-  - 路径级别权限检查
-  - 资源限制检查
-
-- [ ] **受限 ToolRegistry** (`agent/tools/builtin/task.py`)
-  - 根据 Agent 定义过滤工具
-
-### Phase 3: 高级特性
-
-- [ ] **配置系统**
-  - JSON 配置文件支持
-  - 动态加载自定义 Agent
-
-- [ ] **监控和分析**
-  - Sub-Agent 性能统计
-  - 调用链可视化
-
-- [ ] **递归控制**
-  - 嵌套深度限制
-  - 循环检测
-
----
-
-## 集成点
-
-### 与现有系统的集成
-
-1. **AgentRunner** (`agent/runner.py`)
-   - 扩展 `run()` 方法支持 `agent_definition` 参数
-   - 根据 Agent 定义配置工具和权限
-
-2. **ToolRegistry** (`agent/tools/registry.py`)
-   - 添加 Task 工具到全局注册表
-   - 支持工具过滤和权限检查
-
-3. **TraceStore** (`agent/storage/protocols.py`)
-   - 支持根据 `parent_trace_id` 查询子 Trace
-   - 添加层级查询方法
-
-4. **Events** (`agent/events.py`)
-   - 添加 Sub-Agent 相关事件类型
-   - `subagent_started`、`subagent_completed`
+**完整示例**:`examples/subagent_example.py`
 
 
 ---
 ---
 
 
 ## 注意事项
 ## 注意事项
 
 
-1. **防止无限递归**
-   - 默认禁止 Sub-Agent 调用 Task 工具
-   - 设置最大嵌套深度(建议 3 层)
-
-2. **性能考虑**
-   - Sub-Agent 增加了额外的 LLM 调用开销
-   - 合理设置 `max_iterations` 限制
-
-3. **成本控制**
-   - 跟踪每个 Sub-Agent 的 token 消耗
-   - 在父 Trace 中聚合成本统计
-
-4. **错误处理**
-   - Sub-Agent 失败不应导致主任务完全失败
-   - 提供降级策略(如超时、重试)
-
-5. **可观测性**
-   - 完整记录 Sub-Agent 的执行过程
-   - 支持调用链追踪和分析
-
----
-
-## 参考资料
-
-- OpenCode 实现:`/Users/sunlit/Code/opencode/packages/opencode/src/tool/task.ts`
-- OpenCode Agent 定义:`/Users/sunlit/Code/opencode/packages/opencode/src/agent/agent.ts`
-- 当前项目架构:`docs/README.md`
+1. **防止递归**:默认禁止 Sub-Agent 调用 task 工具
+2. **权限隔离**:Sub-Agent 只能访问允许的工具
+3. **Trace 完整性**:每个 Sub-Agent 有独立的 Trace
+4. **成本控制**:跟踪每个 Sub-Agent 的 token 消耗

+ 108 - 0
docs/testing.md

@@ -0,0 +1,108 @@
+# 测试指南
+
+> 项目测试策略和运行方法
+
+---
+
+## 测试分层
+
+```
+E2E 测试 - 真实 LLM 调用(需要 API Key)
+    ↑
+集成测试 - 多模块协作(Mock LLM)
+    ↑
+单元测试 - 单个函数/类
+```
+
+---
+
+## 运行测试
+
+### 本地运行
+
+```bash
+# 单元测试 + 集成测试
+pytest tests/ -v -m "not e2e"
+
+# 指定模块
+pytest tests/test_agent_definition.py -v
+
+# 生成覆盖率报告
+pytest --cov=agent --cov-report=html tests/ -m "not e2e"
+
+# E2E 测试(可选,需要 API Key)
+GEMINI_API_KEY=xxx pytest tests/e2e/ -v -m e2e
+```
+
+### CI 运行
+
+```bash
+# 单元测试 + 集成测试(PR 时运行)
+pytest tests/ -v -m "not e2e" --cov=agent
+
+# E2E 测试(仅 main 分支)
+pytest tests/e2e/ -v -m e2e
+```
+
+---
+
+## 覆盖率要求
+
+| 模块 | 目标覆盖率 |
+|------|-----------|
+| agent/models/ | 90%+ |
+| agent/tools/ | 85%+ |
+| agent/storage/ | 80%+ |
+| agent/llm/ | 70%+ |
+
+---
+
+## 测试标记
+
+```python
+# 单元测试(默认)
+def test_something():
+    pass
+
+# E2E 测试(需要 API Key)
+@pytest.mark.e2e
+def test_real_llm():
+    pass
+```
+
+---
+
+## CI 配置
+
+```yaml
+# .github/workflows/test.yml
+
+jobs:
+  test:
+    - name: Run tests
+      run: pytest tests/ -v -m "not e2e" --cov=agent
+
+  e2e:
+    if: github.ref == 'refs/heads/main'
+    - name: Run E2E tests
+      env:
+        GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
+      run: pytest tests/e2e/ -v -m e2e
+```
+
+---
+
+## 测试文件结构
+
+```
+tests/
+├── conftest.py              # Fixtures
+├── test_agent_definition.py
+├── test_trace.py
+├── test_tools.py
+├── integration/
+│   ├── test_subagent.py
+│   └── test_trace_store.py
+└── e2e/
+    └── test_subagent_e2e.py
+```

+ 164 - 0
docs/tools-adapters.md

@@ -0,0 +1,164 @@
+# 工具适配器
+
+> 参考 opencode 实现的基础工具,移植到 Python 并集成到框架
+
+**设计目标**:
+- 参考 opencode 的成熟工具设计(Git Submodule: `vendor/opencode/`)
+- Python 实现:基础工具(builtin/)完整复刻,高级工具(advanced/)通过 Bun 调用
+- 保持解耦,便于独立维护和更新
+
+---
+
+## 架构设计
+
+```
+vendor/opencode/                       # Git submodule(只读参考)
+  └── packages/opencode/src/tool/      # TypeScript 实现
+
+agent/tools/
+  ├── builtin/                         # 基础工具(Python 完整实现)
+  │   ├── read.py, edit.py, write.py  # 文件操作
+  │   ├── bash.py                      # 命令执行
+  │   └── glob.py, grep.py             # 文件搜索
+  │
+  ├── advanced/                        # 高级工具(Bun 适配器)
+  │   ├── webfetch.py                  # 网页抓取
+  │   └── lsp.py                       # LSP 诊断
+  │
+  └── adapters/
+      ├── base.py                      # 适配器基类
+      ├── opencode_bun_adapter.py      # Bun 调用适配器
+      └── opencode-wrapper.ts          # TypeScript 桥接
+```
+
+**实现策略**:
+- 基础工具(高频):Python 完整实现(~1-5ms,零依赖)
+- 高级工具(低频复杂):通过 Bun 调用 opencode(~50-100ms,功能完整)
+
+---
+
+## 工具清单
+
+### 基础工具(Python 实现)
+
+| 工具 | 实现 | 功能 |
+|------|------|------|
+| `read_file` | `builtin/read.py` | 读取文件(文本/图片/PDF),分页支持 |
+| `edit_file` | `builtin/edit.py` | 文件编辑,9 种智能匹配策略 |
+| `write_file` | `builtin/write.py` | 文件写入,自动创建目录 |
+| `bash_command` | `builtin/bash.py` | 命令执行,超时控制 |
+| `glob_files` | `builtin/glob.py` | 文件模式匹配 |
+| `grep_content` | `builtin/grep.py` | 内容搜索(ripgrep 优先) |
+
+**参考源**:`vendor/opencode/packages/opencode/src/tool/*.ts`
+
+**关键实现**:
+- `edit_file` 的 9 种策略:包含 Levenshtein 算法的 BlockAnchorReplacer
+- `bash_command` 的环境变量支持
+- `grep_content` 的 ripgrep fallback
+
+### 高级工具(Bun 适配器)
+
+| 工具 | 实现 | 调用方式 |
+|------|------|----------|
+| `webfetch` | `advanced/webfetch.py` | Bun → opencode(HTML 转 MD)|
+| `lsp_diagnostics` | `advanced/lsp.py` | Bun → opencode(LSP 集成)|
+
+**依赖**:需要 [Bun](https://bun.sh/) 运行时
+
+**桥接**:`adapters/opencode-wrapper.ts` 通过子进程调用 opencode 工具
+
+---
+
+## 适配器接口
+
+**实现**:`agent/tools/adapters/base.py:ToolAdapter`
+
+核心方法:
+- `adapt_execute()` - 执行工具并转换结果为 `ToolResult`
+- `adapt_schema()` - 转换工具 Schema
+- `extract_memory()` - 提取长期记忆摘要
+
+**Bun 适配器**:`adapters/opencode_bun_adapter.py:OpenCodeBunAdapter`
+- 通过子进程调用 `bun run opencode-wrapper.ts <tool> <args>`
+- 30 秒超时
+- JSON 结果解析
+
+---
+
+## 更新 opencode 参考
+
+### 检查更新
+
+```bash
+# 查看 submodule 状态
+git submodule status
+
+# 更新 submodule
+cd vendor/opencode
+git pull origin main
+cd ../..
+
+# 提交更新
+git add vendor/opencode
+git commit -m "chore: update opencode reference"
+```
+
+### 同步改进
+
+1. **查看 opencode 变更**:
+   ```bash
+   cd vendor/opencode
+   git log --oneline --since="1 month ago" -- packages/opencode/src/tool/
+   ```
+
+2. **对比实现差异**:
+   - 查看 opencode 的具体改动
+   - 评估是否需要同步到 Python 实现
+
+3. **更新 Python 实现**:
+   - 在 `agent/tools/builtin/` 中更新对应工具
+   - 更新代码注释中的参考位置
+
+4. **测试验证**:
+   ```bash
+   pytest tests/tools/builtin/ -v
+   ```
+
+---
+
+## 使用示例
+
+```python
+from agent.tools.builtin import read_file, edit_file, bash_command
+from agent.tools.advanced import webfetch
+
+# 基础工具
+result = await read_file(file_path="config.py", limit=100)
+result = await edit_file(file_path="config.py", old_string="DEBUG = True", new_string="DEBUG = False")
+result = await bash_command(command="git status", timeout=30)
+
+# 高级工具(需要 Bun)
+result = await webfetch(url="https://docs.python.org/3/")
+```
+
+工具通过 `@tool` 装饰器自动注册到 `ToolRegistry`。
+
+**完整示例**:`examples/tools_complete_demo.py`
+
+---
+
+## 关键设计
+
+**Git Submodule 原则**:
+- `vendor/opencode/` 只读,绝不修改
+- 作为参考源,可随时更新:`cd vendor/opencode && git pull`
+- 基础工具手动同步重要改进,高级工具自动获得更新
+
+**高内聚原则**:
+- `opencode-wrapper.ts` 与 `opencode_bun_adapter.py` 在同一目录(`adapters/`)
+- Wrapper 是适配器的一部分,不是独立脚本
+
+**性能权衡**:
+- 基础工具(高频):Python 实现避免子进程开销
+- 高级工具(低频):Bun 适配器避免重复实现复杂逻辑

+ 94 - 2
docs/tools.md

@@ -11,8 +11,9 @@
 3. [ToolResult 和记忆管理](#toolresult-和记忆管理)
 3. [ToolResult 和记忆管理](#toolresult-和记忆管理)
 4. [ToolContext 和依赖注入](#toolcontext-和依赖注入)
 4. [ToolContext 和依赖注入](#toolcontext-和依赖注入)
 5. [高级特性](#高级特性)
 5. [高级特性](#高级特性)
-6. [集成 Browser-Use](#集成-browser-use)
-7. [最佳实践](#最佳实践)
+6. [内置基础工具](#内置基础工具)
+7. [集成 Browser-Use](#集成-browser-use)
+8. [最佳实践](#最佳实践)
 
 
 ---
 ---
 
 
@@ -688,6 +689,97 @@ print(f"Success rate: {stats['success_rate']:.1%}")
 
 
 ---
 ---
 
 
+## 内置基础工具
+
+> 参考 opencode 实现的文件操作和命令执行工具
+
+框架提供一组内置的基础工具,用于文件读取、编辑、搜索和命令执行等常见任务。这些工具参考了 [opencode](https://github.com/anomalyco/opencode) 的成熟设计,在 Python 中重新实现。
+
+**实现位置**:
+- 工具实现:`agent/tools/builtin/`
+- 适配器层:`agent/tools/adapters/`
+- OpenCode 参考:`vendor/opencode/` (git submodule)
+
+**详细文档**:参考 [`docs/tools-adapters.md`](./tools-adapters.md)
+
+### 可用工具
+
+| 工具 | 功能 | 参考 |
+|------|------|------|
+| `read_file` | 读取文件内容(支持图片、PDF) | opencode read.ts |
+| `edit_file` | 智能文件编辑(多种匹配策略) | opencode edit.ts |
+| `write_file` | 写入文件(创建或覆盖) | opencode write.ts |
+| `bash_command` | 执行 shell 命令 | opencode bash.ts |
+| `glob_files` | 文件模式匹配 | opencode glob.ts |
+| `grep_content` | 内容搜索(正则表达式) | opencode grep.ts |
+
+### 快速使用
+
+```python
+from agent.tools.builtin import read_file, edit_file, bash_command
+
+# 读取文件
+result = await read_file(file_path="config.py", limit=100)
+print(result.output)
+
+# 编辑文件(智能匹配)
+result = await edit_file(
+    file_path="config.py",
+    old_string="DEBUG = True",
+    new_string="DEBUG = False"
+)
+
+# 执行命令
+result = await bash_command(
+    command="git status",
+    timeout=30,
+    description="Check git status"
+)
+```
+
+### 核心特性
+
+**Read Tool 特性**:
+- 二进制文件检测
+- 分页读取(offset/limit)
+- 行长度和字节限制
+- 图片/PDF 支持
+
+**Edit Tool 特性**:
+- 多种智能匹配策略:
+  - SimpleReplacer - 精确匹配
+  - LineTrimmedReplacer - 忽略行首尾空白
+  - WhitespaceNormalizedReplacer - 空白归一化
+- 自动生成 unified diff
+- 唯一性检查(防止错误替换)
+
+**Bash Tool 特性**:
+- 异步执行
+- 超时控制(默认 120 秒)
+- 工作目录设置
+- 输出截断(防止过长)
+
+### 更新 OpenCode 参考
+
+内置工具参考 `vendor/opencode/` 中的实现,通过 git submodule 管理:
+
+```bash
+# 更新 opencode 参考
+cd vendor/opencode
+git pull origin main
+cd ../..
+git add vendor/opencode
+git commit -m "chore: update opencode reference"
+
+# 查看最近变更
+cd vendor/opencode
+git log --oneline --since="1 month ago" -- packages/opencode/src/tool/
+```
+
+更新后,检查是否需要同步改进到 Python 实现。
+
+---
+
 ## 集成 Browser-Use
 ## 集成 Browser-Use
 
 
 ### 适配器模式
 ### 适配器模式

+ 7 - 0
examples/feature_extract/input_1/feature_extract.prompt → examples/feature_extract/feature_extract.prompt

@@ -19,6 +19,13 @@ $system$
 - tool_builder: 编写代码来创建指定工具,并配置好工具所需环境、对外提供API
 - tool_builder: 编写代码来创建指定工具,并配置好工具所需环境、对外提供API
 - tool_deployer: 完成指定的开源代码工具的环境配置与部署,对外提供API
 - tool_deployer: 完成指定的开源代码工具的环境配置与部署,对外提供API
 
 
+# 经验与反思
+你可以调用 search_experience 搜索过去的经验。
+注意总结任务执行过程中的经验,调用 record_experience 工具进行记录。
+
+过去经验的一个总结如下:
+%memory%
+
 $user$
 $user$
 # 指定的特征:
 # 指定的特征:
 %text%
 %text%

+ 53 - 0
examples/feature_extract/output_1/result.txt

@@ -0,0 +1,53 @@
+作为一位计算机视觉专家和社媒博主,我将根据您提供的“整体构图”特征描述,对其进行结构化和精炼,以便于内容生成、聚类分析和模型训练。
+
+# 特征表示分析
+
+为了在保留关键信息的同时,使特征既能用于生成类似内容(需要细节),又能用于聚类和模型训练(需要精简和标准化),我将采用嵌套的JSON结构。
+
+*   **顶层**:明确指出这是关于“整体构图”的特征,并包含原始的“评分”。
+*   **构图手法列表**:使用一个列表来列出主要的构图类型,这对于快速分类和聚类非常有用。
+*   **具体构图元素**:对于引导线、对称和框架构图,我将它们分别抽象为独立的子对象,每个子对象包含:
+    *   `存在`:布尔值,表示该构图手法是否存在(便于快速判断和训练)。
+    *   `元素`:构成该手法的具体物体(例如“道路”、“树木”)。
+    *   `形式`:该元素的具体呈现方式(例如“拱形通道”、“水坑倒影”)。
+    *   `方向/位置`:描述元素在画面中的相对位置或方向。
+    *   `目标/对象`:该构图手法旨在强调或引导的对象。
+*   **主体放置**:对画面中主体(如骑行者)的相对位置进行概括性描述。
+*   **补充说明**:提供一段概括性的文字,用于在内容生成时增加语境和艺术性。
+
+# 提取的特征
+
+```json
+{
+  "整体构图": {
+    "主要构图手法": [
+      "中心构图",
+      "引导线构图",
+      "对称构图",
+      "框架构图"
+    ],
+    "引导线": {
+      "存在": true,
+      "元素": "道路",
+      "方向": "从画面底部中央向远方延伸",
+      "引导目标": ["骑行者", "光束"]
+    },
+    "对称构图": {
+      "存在": true,
+      "类型": "水平倒影对称",
+      "对称元素": "水坑倒影",
+      "位置描述": "画面下半部,倒影区域宽度覆盖整个画面"
+    },
+    "框架构图": {
+      "存在": true,
+      "元素": "高大树木",
+      "形式": "形成自然的拱形通道",
+      "位置": "画面左右两侧",
+      "框选对象": "骑行者 (主体)"
+    },
+    "主体放置": "中心偏远"
+  },
+  "补充说明": "多种构图手法巧妙结合,营造出强烈的纵深感和空间感,特别是光线和倒影的运用,极大地增强了画面的艺术感染力,使得整体画面富有叙事性和沉浸感。",
+  "评分": 0.652
+}
+```

+ 98 - 0
examples/feature_extract/run.py

@@ -0,0 +1,98 @@
+"""
+特征提取示例
+
+使用 Agent 框架 + Prompt loader + 多模态支持
+"""
+
+import os
+import sys
+import asyncio
+from pathlib import Path
+
+# 添加项目根目录到 Python 路径
+sys.path.insert(0, str(Path(__file__).parent.parent.parent))
+
+from dotenv import load_dotenv
+load_dotenv()
+
+from agent.prompts import SimplePrompt
+from agent.runner import AgentRunner
+from agent.llm.providers.gemini import create_gemini_llm_call
+
+
+async def main():
+    # 路径配置
+    base_dir = Path(__file__).parent
+    prompt_path = base_dir / "test.prompt"
+    feature_md_path = base_dir / "input_1" / "feature.md"
+    image_path = base_dir / "input_1" / "image.png"
+    output_dir = base_dir / "output_1"
+    output_dir.mkdir(exist_ok=True)
+
+    print("=" * 60)
+    print("特征提取任务")
+    print("=" * 60)
+    print()
+
+    # 1. 加载 prompt
+    print("1. 加载 prompt...")
+    prompt = SimplePrompt(prompt_path)
+
+    # 2. 读取特征描述
+    print("2. 读取特征描述...")
+    with open(feature_md_path, 'r', encoding='utf-8') as f:
+        feature_text = f.read()
+
+    # 3. 构建多模态消息
+    print("3. 构建多模态消息(文本 + 图片)...")
+    messages = prompt.build_messages(
+        text=feature_text,
+        images=image_path  # 框架自动处理图片
+    )
+
+    print(f"   - 消息数量: {len(messages)}")
+    print(f"   - 图片: {image_path.name}")
+
+    # 4. 创建 Agent Runner
+    print("4. 创建 Agent Runner...")
+    runner = AgentRunner(
+        llm_call=create_gemini_llm_call()
+    )
+
+    # 5. 调用 Agent
+    print(f"5. 调用模型: {prompt.config.get('model', 'gemini-2.5-flash')}...")
+    print()
+
+    result = await runner.call(
+        messages=messages,
+        model=prompt.config.get('model', 'gemini-2.5-flash'),
+        temperature=float(prompt.config.get('temperature', 0.3)),
+        trace=False  # 暂不记录 trace
+    )
+
+    # 6. 输出结果
+    print("=" * 60)
+    print("模型响应:")
+    print("=" * 60)
+    print(result.reply)
+    print("=" * 60)
+    print()
+
+    # 7. 保存结果
+    output_file = output_dir / "result.txt"
+    with open(output_file, 'w', encoding='utf-8') as f:
+        f.write(result.reply)
+
+    print(f"✓ 结果已保存到: {output_file}")
+    print()
+
+    # 8. 打印统计信息
+    print("统计信息:")
+    if result.tokens:
+        print(f"  输入 tokens: {result.tokens.get('prompt', 0)}")
+        print(f"  输出 tokens: {result.tokens.get('completion', 0)}")
+    print(f"  费用: ${result.cost:.4f}")
+
+
+if __name__ == "__main__":
+    asyncio.run(main())

+ 22 - 0
examples/feature_extract/test.prompt

@@ -0,0 +1,22 @@
+---
+model: gemini-2.5-flash
+temperature: 0.3
+---
+
+$system$
+# 角色
+你是一位计算机视觉专家,也是一位才华横溢的社媒博主、内容创作者。
+
+# 任务
+分析一个优质内容的指定特征适合如何表示,并完成该特征的提取。
+提取的特征将用于在生成类似内容时作为参考内容(所以要保留重要信息),也会和其他内容的同一维度的特征放在一起聚类发现规律(所以特征表示要尽量精简、不要过于具体),或用于模型训练。
+
+# 工具
+- 你可以加载browser-use的skill,并根据skill中的指引操作浏览器,来做调研或检索
+
+$user$
+# 指定的特征:
+%text%
+
+# 结果保存路径
+/Users/sunlit/Code/Agent/examples/feature_extract/output_1/

+ 0 - 224
examples/gemini_basic_agent.py

@@ -1,224 +0,0 @@
-"""
-Gemini Agent 基础示例
-
-使用 Gemini 2.5 Pro 模型,演示带工具调用的 Agent
-
-依赖:
-    pip install httpx python-dotenv
-
-使用方法:
-    python examples/gemini_basic_agent.py
-"""
-
-import os
-import sys
-import json
-import asyncio
-from typing import Dict, Any, List, Optional
-from dotenv import load_dotenv
-
-# 添加项目根目录到 Python 路径
-sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
-
-# 加载环境变量
-load_dotenv()
-
-# 导入框架
-from agent.tools import tool, ToolResult, get_tool_registry
-from agent.runner import AgentRunner
-from agent.llm.providers.gemini import create_gemini_llm_call
-
-
-# ============================================================
-# 定义工具
-# ============================================================
-
-@tool()
-async def get_current_weather(location: str, unit: str = "celsius", uid: str = "") -> Dict[str, Any]:
-    """
-    获取指定地点的当前天气
-
-    Args:
-        location: 城市名称,如 "北京"、"San Francisco"
-        unit: 温度单位,"celsius" 或 "fahrenheit"
-
-    Returns:
-        天气信息字典
-    """
-    # 模拟天气数据
-    weather_data = {
-        "北京": {"temp": 15, "condition": "晴朗", "humidity": 45},
-        "上海": {"temp": 20, "condition": "多云", "humidity": 60},
-        "San Francisco": {"temp": 18, "condition": "Foggy", "humidity": 70},
-        "New York": {"temp": 10, "condition": "Rainy", "humidity": 80}
-    }
-
-    data = weather_data.get(location, {"temp": 22, "condition": "Unknown", "humidity": 50})
-
-    if unit == "fahrenheit":
-        data["temp"] = data["temp"] * 9/5 + 32
-
-    return {
-        "location": location,
-        "temperature": data["temp"],
-        "unit": unit,
-        "condition": data["condition"],
-        "humidity": data["humidity"]
-    }
-
-
-@tool()
-async def calculate(expression: str, uid: str = "") -> ToolResult:
-    """
-    执行数学计算
-
-    Args:
-        expression: 数学表达式,如 "2 + 2"、"10 * 5"
-
-    Returns:
-        计算结果
-    """
-    try:
-        # 安全地计算简单表达式
-        # 注意:实际生产环境应使用更安全的方法
-        result = eval(expression, {"__builtins__": {}}, {})
-
-        return ToolResult(
-            title="计算结果",
-            output=f"{expression} = {result}",
-            long_term_memory=f"计算了 {expression}"
-        )
-    except Exception as e:
-        return ToolResult(
-            title="计算错误",
-            output=f"无法计算 '{expression}': {str(e)}",
-            long_term_memory=f"计算失败: {expression}"
-        )
-
-
-@tool()
-async def search_knowledge(query: str, max_results: int = 3, uid: str = "") -> ToolResult:
-    """
-    搜索知识库
-
-    Args:
-        query: 搜索关键词
-        max_results: 返回结果数量
-
-    Returns:
-        搜索结果
-    """
-    # 模拟知识库搜索
-    knowledge_base = {
-        "Python": "Python 是一种高级编程语言,以简洁易读的语法著称。",
-        "Agent": "Agent 是能够感知环境并采取行动以实现目标的智能体。",
-        "Gemini": "Gemini 是 Google 开发的多模态大语言模型系列。",
-        "AI": "人工智能(AI)是计算机科学的一个分支,致力于创建智能机器。"
-    }
-
-    results = []
-    for key, value in knowledge_base.items():
-        if query.lower() in key.lower() or query.lower() in value.lower():
-            results.append({"title": key, "content": value})
-            if len(results) >= max_results:
-                break
-
-    if not results:
-        output = f"未找到关于 '{query}' 的信息"
-    else:
-        output = "\n\n".join([f"**{r['title']}**\n{r['content']}" for r in results])
-
-    return ToolResult(
-        title=f"搜索结果: {query}",
-        output=output,
-        long_term_memory=f"搜索了 '{query}',找到 {len(results)} 条结果"
-    )
-
-
-# ============================================================
-# 主函数
-# ============================================================
-
-async def main():
-    print("=" * 60)
-    print("Gemini Agent 基础示例")
-    print("=" * 60)
-    print()
-
-    # 获取工具注册表
-    registry = get_tool_registry()
-
-    # 打印可用工具
-    print("可用工具:")
-    for tool_name in registry.get_tool_names():
-        print(f"  - {tool_name}")
-    print()
-
-    # 创建 Gemini LLM 调用函数
-    gemini_llm_call = create_gemini_llm_call()
-
-    # 创建 Agent Runner
-    runner = AgentRunner(
-        tool_registry=registry,
-        llm_call=gemini_llm_call,
-    )
-
-    # 测试任务
-    task = "北京今天的天气怎么样?顺便帮我计算一下 15 * 8 等于多少。"
-
-    print(f"任务: {task}")
-    print("-" * 60)
-    print()
-
-    # 运行 Agent
-    async for event in runner.run(
-        task=task,
-        model="gemini-2.5-pro",
-        tools=["get_current_weather", "calculate", "search_knowledge"],
-        max_iterations=5,
-        enable_memory=False,  # 暂不启用记忆
-        auto_execute_tools=True,
-        system_prompt="你是一个有用的AI助手,可以使用工具来帮助用户。请简洁明了地回答问题。"
-    ):
-        event_type = event.type
-        data = event.data
-
-        if event_type == "trace_started":
-            print(f"✓ Trace 开始: {data['trace_id']}")
-            print()
-
-        elif event_type == "llm_call_completed":
-            print(f"🤖 LLM 响应:")
-            if data.get("content"):
-                print(f"   {data['content']}")
-            if data.get("tool_calls"):
-                print(f"   工具调用: {len(data['tool_calls'])} 个")
-            print(f"   Tokens: {data.get('tokens', 0)}")
-            print()
-
-        elif event_type == "tool_executing":
-            print(f"🔧 执行工具: {data['tool_name']}")
-            print(f"   参数: {json.dumps(data['arguments'], ensure_ascii=False)}")
-
-        elif event_type == "tool_result":
-            print(f"   结果: {data['result'][:100]}...")
-            print()
-
-        elif event_type == "conclusion":
-            print(f"✅ 最终回答:")
-            print(f"   {data['content']}")
-            print()
-
-        elif event_type == "trace_completed":
-            print(f"✓ Trace 完成")
-            print(f"   总 Tokens: {data.get('total_tokens', 0)}")
-            print(f"   总成本: ${data.get('total_cost', 0):.4f}")
-            print()
-
-        elif event_type == "trace_failed":
-            print(f"❌ Trace 失败: {data.get('error')}")
-            print()
-
-
-if __name__ == "__main__":
-    asyncio.run(main())

+ 0 - 36
examples/test_skill.py

@@ -1,36 +0,0 @@
-"""
-Skills 使用示例
-
-演示如何测试 skill 工具
-"""
-
-import asyncio
-from tools.skill import list_skills, skill
-
-
-async def main():
-    """测试 skill 工具"""
-
-    # 1. 列出所有 skills
-    print("=" * 60)
-    print("测试 list_skills")
-    print("=" * 60)
-
-    result = await list_skills()
-    print(f"Title: {result.title}")
-    print(f"Output:\n{result.output}")
-    print()
-
-    # 2. 加载特定 skill
-    print("=" * 60)
-    print("测试 skill")
-    print("=" * 60)
-
-    result = await skill(skill_name="browser-use")
-    print(f"Title: {result.title}")
-    print(f"Output:\n{result.output[:500]}...")
-    print()
-
-
-if __name__ == "__main__":
-    asyncio.run(main())

+ 188 - 0
examples/tools_complete_demo.py

@@ -0,0 +1,188 @@
+"""
+完整工具系统使用示例
+
+演示基础工具和高级工具的使用。
+"""
+
+import asyncio
+from agent.tools.builtin import (
+    read_file,
+    edit_file,
+    write_file,
+    bash_command,
+    glob_files,
+    grep_content
+)
+
+
+async def demo_basic_tools():
+    """演示基础工具(Python 实现)"""
+
+    print("=" * 60)
+    print("基础工具演示")
+    print("=" * 60)
+
+    # 1. 读取文件
+    print("\n1. 读取文件")
+    result = await read_file(file_path="README.md", limit=20)
+    print(f"✅ {result.title}")
+    print(f"   前 5 行: {result.output[:200]}...")
+
+    # 2. 搜索文件
+    print("\n2. Glob 搜索")
+    result = await glob_files(pattern="**/*.py", path="agent/tools")
+    print(f"✅ {result.title}")
+    print(f"   找到 {result.metadata['count']} 个文件")
+
+    # 3. 内容搜索
+    print("\n3. Grep 搜索")
+    result = await grep_content(
+        pattern="async def",
+        path="agent/tools/builtin",
+        include="*.py"
+    )
+    print(f"✅ {result.title}")
+    print(f"   找到 {result.metadata['matches']} 个匹配")
+
+    # 4. 执行命令
+    print("\n4. Bash 命令")
+    result = await bash_command(
+        command="git status --short",
+        timeout=10
+    )
+    print(f"✅ {result.title}")
+    print(f"   退出码: {result.metadata['exit_code']}")
+
+    # 5. 编辑文件(演示智能匹配)
+    print("\n5. 智能编辑(9 种策略)")
+
+    # 创建测试文件
+    test_content = """
+def hello():
+    print("Hello")
+
+def world():
+    print("World")
+"""
+    await write_file(file_path="/tmp/test_edit.py", content=test_content)
+
+    # 编辑:忽略缩进(会使用 IndentationFlexibleReplacer)
+    result = await edit_file(
+        file_path="/tmp/test_edit.py",
+        old_string='def hello():\nprint("Hello")',  # 缩进不同
+        new_string='def hello():\n    print("Hello, World!")'
+    )
+    print(f"✅ {result.title}")
+    print(f"   Diff:\n{result.metadata['diff'][:200]}...")
+
+
+async def demo_advanced_tools():
+    """演示高级工具(Bun 适配器)"""
+
+    print("\n" + "=" * 60)
+    print("高级工具演示(需要 Bun)")
+    print("=" * 60)
+
+    try:
+        from agent.tools.advanced import webfetch, lsp_diagnostics
+
+        # 1. 网页抓取
+        print("\n1. 网页抓取 (HTML -> Markdown)")
+        result = await webfetch(
+            url="https://example.com",
+            format="markdown"
+        )
+        print(f"✅ {result.title}")
+        print(f"   内容长度: {len(result.output)} 字符")
+
+        # 2. LSP 诊断
+        print("\n2. LSP 诊断")
+        result = await lsp_diagnostics(
+            file_path="agent/tools/builtin/edit.py"
+        )
+        print(f"✅ {result.title}")
+        print(f"   诊断结果: {result.output[:200]}...")
+
+    except Exception as e:
+        print(f"⚠️  高级工具需要 Bun 运行时: {e}")
+        print("   安装: curl -fsSL https://bun.sh/install | bash")
+
+
+async def demo_edit_strategies():
+    """演示 edit_file 的 9 种匹配策略"""
+
+    print("\n" + "=" * 60)
+    print("edit_file 策略演示")
+    print("=" * 60)
+
+    test_cases = [
+        {
+            "name": "策略 1: 精确匹配",
+            "content": "DEBUG = True\nVERBOSE = False",
+            "old": "DEBUG = True",
+            "new": "DEBUG = False"
+        },
+        {
+            "name": "策略 2: 忽略行首尾空白",
+            "content": "  DEBUG = True  \nVERBOSE = False",
+            "old": "DEBUG = True",  # 无空白
+            "new": "DEBUG = False"
+        },
+        {
+            "name": "策略 4: 空白归一化",
+            "content": "DEBUG  =   True",
+            "old": "DEBUG = True",  # 单空格
+            "new": "DEBUG = False"
+        },
+        {
+            "name": "策略 5: 灵活缩进",
+            "content": """
+def foo():
+    if True:
+        print("hello")
+""",
+            "old": "if True:\nprint(\"hello\")",  # 无缩进
+            "new": "if True:\n    print(\"world\")"
+        }
+    ]
+
+    for i, test in enumerate(test_cases, 1):
+        print(f"\n{i}. {test['name']}")
+
+        # 创建测试文件
+        test_file = f"/tmp/test_strategy_{i}.py"
+        await write_file(file_path=test_file, content=test["content"])
+
+        # 执行编辑
+        try:
+            result = await edit_file(
+                file_path=test_file,
+                old_string=test["old"],
+                new_string=test["new"]
+            )
+            print(f"   ✅ 成功匹配")
+        except Exception as e:
+            print(f"   ❌ 失败: {e}")
+
+
+async def main():
+    """运行所有演示"""
+
+    print("\n🚀 工具系统完整演示\n")
+
+    # 基础工具
+    await demo_basic_tools()
+
+    # 编辑策略
+    await demo_edit_strategies()
+
+    # 高级工具
+    await demo_advanced_tools()
+
+    print("\n" + "=" * 60)
+    print("演示完成!")
+    print("=" * 60)
+
+
+if __name__ == "__main__":
+    asyncio.run(main())