فهرست منبع

doc: README and examples

Talegorithm 3 هفته پیش
والد
کامیت
11d4c0e8d4

+ 8 - 0
.env.template

@@ -0,0 +1,8 @@
+# 完成配置后,将 .env.template 重命名为 .env
+
+
+# OpenRouter API Key
+OPEN_ROUTER_API_KEY=
+
+# BrowserUse API Key
+BROWSER_USE_API_KEY=

+ 215 - 0
README.md

@@ -0,0 +1,215 @@
+# Reson Agent
+
+可扩展的 Agent 框架。支持多步工具调用、计划管理、子 Agent 协作、回溯重跑和上下文压缩。
+
+## Quick Start
+
+```bash
+pip install -r requirements.txt
+
+# 配置 LLM API Key
+cp .env.example .env  # 编辑填入 API Key
+```
+
+### 最小示例
+
+```python
+import asyncio
+from agent import AgentRunner, RunConfig
+from agent.trace import FileSystemTraceStore
+from agent.llm import create_openrouter_llm_call
+
+runner = AgentRunner(
+    trace_store=FileSystemTraceStore(base_path=".trace"),
+    llm_call=create_openrouter_llm_call(model="anthropic/claude-sonnet-4.5"),
+)
+
+async def main():
+    async for item in runner.run(
+        messages=[{"role": "user", "content": "列出当前目录的文件"}],
+        config=RunConfig(model="anthropic/claude-sonnet-4.5"),
+    ):
+        print(item)
+
+asyncio.run(main())
+```
+
+## 自定义工具
+
+用 `@tool` 装饰器注册。`RunConfig(tools=None)`(默认)时所有已注册工具自动对 LLM 可用,无需额外配置。
+
+```python
+from agent import tool, ToolResult
+
+@tool(description="查询产品库存")
+async def check_inventory(product_id: str, warehouse: str = "default") -> ToolResult:
+    """查询指定仓库的产品库存
+
+    Args:
+        product_id: 产品唯一标识符
+        warehouse: 仓库编码,默认为主仓库
+    """
+    stock = await query_db(product_id, warehouse)
+    return ToolResult(output=f"库存: {stock}")
+
+# 确保此模块在 runner.run() 之前被 import
+```
+
+**注意**: `@tool` 通过副作用注册到全局 registry,必须确保定义工具的模块在调用 `runner.run()` 前被 import。
+
+### 参数 Schema 生成
+
+框架从函数签名和 docstring 自动生成 OpenAI Tool Schema,无需手写 JSON:
+
+- **参数类型**:从类型注解推断(`str`/`int`/`float`/`bool`/`list`/`dict`,支持 `Optional`、`Literal`、`List[T]`)
+- **参数描述**:从 Google 风格 docstring 的 `Args:` 段提取
+- **必填/可选**:有默认值的参数为可选,否则为必填
+- **工具描述**:优先使用 `@tool(description=...)` 参数,其次取 docstring 首行
+- `uid` 和 `context` 参数由框架自动注入,不会出现在 Schema 中
+
+上面的 `check_inventory` 会生成:
+
+```json
+{
+  "type": "function",
+  "function": {
+    "name": "check_inventory",
+    "description": "查询产品库存",
+    "parameters": {
+      "type": "object",
+      "properties": {
+        "product_id": {"type": "string", "description": "产品唯一标识符"},
+        "warehouse": {"type": "string", "description": "仓库编码,默认为主仓库", "default": "default"}
+      },
+      "required": ["product_id"]
+    }
+  }
+}
+```
+
+### 限制工具范围
+
+```python
+# 只启用指定工具(在内置工具基础上追加)
+config = RunConfig(tools=["check_inventory", "another_tool"])
+```
+
+## 自定义 Skills
+
+Skills 是 Markdown 文件,提供领域知识,注入到 system prompt。
+
+```
+my_project/
+└── skills/
+    └── my_domain.md
+```
+
+```markdown
+---
+name: my-domain-skill
+description: 领域专属知识
+---
+
+## Guidelines
+- 规则 1
+- 规则 2
+```
+
+```python
+runner = AgentRunner(
+    llm_call=...,
+    trace_store=...,
+    skills_dir="./skills",  # 指向你的 skills 目录
+)
+```
+
+内置 skills(`agent/memory/skills/`)始终自动加载,`skills_dir` 的内容额外追加。
+
+## AgentRunner 参数
+
+```python
+AgentRunner(
+    llm_call,                # 必需:LLM 调用函数
+    trace_store=None,        # Trace 持久化(推荐 FileSystemTraceStore)
+    tool_registry=None,      # 工具注册表(默认:全局 registry)
+    skills_dir=None,         # 自定义 skills 目录
+    experiences_path="./cache/experiences.md",  # 经验文件路径
+    memory_store=None,       # 记忆存储
+    utility_llm_call=None,   # 轻量 LLM(生成任务标题等)
+)
+```
+
+## RunConfig 参数
+
+```python
+RunConfig(
+    model="gpt-4o",          # 模型标识
+    temperature=0.3,
+    max_iterations=200,       # Agent loop 最大轮数
+    tools=None,               # None=全部已注册工具,List[str]=内置+指定工具
+    system_prompt=None,       # None=从 skills 自动构建
+    agent_type="default",     # 预设类型:default / explore / analyst
+    trace_id=None,            # 续跑/回溯时传入已有 trace ID
+    insert_after=None,        # 回溯插入点(message sequence)
+)
+```
+
+## LLM Providers
+
+框架内置两个 provider:
+
+```python
+from agent.llm import create_openrouter_llm_call, create_gemini_llm_call
+
+# OpenRouter(支持多种模型)
+llm = create_openrouter_llm_call(model="anthropic/claude-sonnet-4.5")
+
+# Google Gemini
+llm = create_gemini_llm_call(model="gemini-2.5-flash")
+```
+
+自定义 provider 只需实现签名:
+
+```python
+async def my_llm_call(messages, model, tools, temperature, **kwargs) -> dict:
+    # 调用你的 LLM
+    return {
+        "content": "...",
+        "tool_calls": [...] or None,
+        "prompt_tokens": 100,
+        "completion_tokens": 50,
+        "cost": 0.001,
+        "finish_reason": "stop",
+    }
+```
+
+## API Server
+
+```bash
+python api_server.py
+```
+
+| 方法 | 路径 | 说明 |
+|------|------|------|
+| GET | `/api/traces` | 列出 Traces |
+| GET | `/api/traces/{id}` | Trace 详情 |
+| GET | `/api/traces/{id}/messages` | 消息列表 |
+| POST | `/api/traces` | 新建并执行 |
+| POST | `/api/traces/{id}/run` | 续跑/回溯 |
+| POST | `/api/traces/{id}/stop` | 停止 |
+| WS | `/api/traces/{id}/watch` | 实时事件 |
+
+需在 `api_server.py` 中配置 Runner 才能启用 POST 端点。
+
+## 项目结构
+
+```
+agent/
+├── core/           # AgentRunner + 预设
+├── tools/          # 工具系统(registry + 内置工具)
+├── trace/          # 执行追踪 + 计划(GoalTree)+ API
+├── memory/         # Skills + Experiences
+└── llm/            # LLM Provider 适配
+```
+
+详细架构文档:[docs/README.md](./docs/README.md)

+ 17 - 13
agent/core/runner.py

@@ -47,7 +47,7 @@ class RunConfig:
     model: str = "gpt-4o"
     model: str = "gpt-4o"
     temperature: float = 0.3
     temperature: float = 0.3
     max_iterations: int = 200
     max_iterations: int = 200
-    tools: Optional[List[str]] = None          # None = 全部内置工具
+    tools: Optional[List[str]] = None          # None = 全部已注册工具
 
 
     # --- 框架层参数 ---
     # --- 框架层参数 ---
     agent_type: str = "default"
     agent_type: str = "default"
@@ -356,12 +356,7 @@ class AgentRunner:
         trace_id = None
         trace_id = None
         message_id = None
         message_id = None
 
 
-        tool_names = BUILTIN_TOOLS.copy()
-        if tools:
-            for tool in tools:
-                if tool not in tool_names:
-                    tool_names.append(tool)
-        tool_schemas = self.tools.get_schemas(tool_names)
+        tool_schemas = self._get_tool_schemas(tools)
 
 
         if trace and self.trace_store:
         if trace and self.trace_store:
             trace_obj = Trace.create(mode="call", uid=uid, model=model, tools=tool_schemas, llm_params=kwargs)
             trace_obj = Trace.create(mode="call", uid=uid, model=model, tools=tool_schemas, llm_params=kwargs)
@@ -872,12 +867,21 @@ class AgentRunner:
     # ===== 辅助方法 =====
     # ===== 辅助方法 =====
 
 
     def _get_tool_schemas(self, tools: Optional[List[str]]) -> List[Dict]:
     def _get_tool_schemas(self, tools: Optional[List[str]]) -> List[Dict]:
-        """获取工具 Schema"""
-        tool_names = BUILTIN_TOOLS.copy()
-        if tools:
-            for tool in tools:
-                if tool not in tool_names:
-                    tool_names.append(tool)
+        """
+        获取工具 Schema
+
+        - tools=None: 使用 registry 中全部已注册工具(含内置 + 外部注册的)
+        - tools=["a", "b"]: 在 BUILTIN_TOOLS 基础上追加指定工具
+        """
+        if tools is None:
+            # 全部已注册工具
+            tool_names = self.tools.get_tool_names()
+        else:
+            # BUILTIN_TOOLS + 显式指定的额外工具
+            tool_names = BUILTIN_TOOLS.copy()
+            for t in tools:
+                if t not in tool_names:
+                    tool_names.append(t)
         return self.tools.get_schemas(tool_names)
         return self.tools.get_schemas(tool_names)
 
 
     async def _build_system_prompt(self, config: RunConfig) -> Optional[str]:
     async def _build_system_prompt(self, config: RunConfig) -> Optional[str]:

+ 10 - 10
api_server.py

@@ -61,16 +61,16 @@ set_ws_trace_store(trace_store)
 # ===== 可选:配置 Runner(启用执行 API)=====
 # ===== 可选:配置 Runner(启用执行 API)=====
 
 
 # 如需启用 POST /api/traces(新建/运行/停止/反思),取消以下注释并配置 LLM:
 # 如需启用 POST /api/traces(新建/运行/停止/反思),取消以下注释并配置 LLM:
-#
-# from agent.core.runner import AgentRunner
-# from agent.llm import create_openrouter_llm_call
-#
-# runner = AgentRunner(
-#     trace_store=trace_store,
-#     llm_call=create_openrouter_llm_call(model="google/gemini-2.5-flash"),
-#     experiences_path="./cache/experiences.md",  # 经验文件路径
-# )
-# set_runner(runner)
+
+from agent.core.runner import AgentRunner
+from agent.llm import create_openrouter_llm_call
+
+runner = AgentRunner(
+    trace_store=trace_store,
+    llm_call=create_openrouter_llm_call(model="anthropic/claude-sonnet-4.5"),
+    experiences_path="./cache/experiences.md",  # 经验文件路径
+)
+set_runner(runner)
 
 
 
 
 # ===== 注册路由 =====
 # ===== 注册路由 =====

+ 1 - 1
docs/README.md

@@ -131,7 +131,7 @@ class RunConfig:
     model: str = "gpt-4o"
     model: str = "gpt-4o"
     temperature: float = 0.3
     temperature: float = 0.3
     max_iterations: int = 200
     max_iterations: int = 200
-    tools: Optional[List[str]] = None          # None = 全部内置工具
+    tools: Optional[List[str]] = None          # None = 全部已注册工具
 
 
     # 框架层参数
     # 框架层参数
     agent_type: str = "default"
     agent_type: str = "default"

+ 0 - 354
examples/cloud_browser_demo_db.py

@@ -1,354 +0,0 @@
-"""
-小红书云浏览器数据获取脚本(数据库配置版)
-从数据库 agent_channel_cookies 获取 Cookie 和 cloud_profile_id
-"""
-
-import sys
-import os
-import asyncio
-import json
-import re
-from datetime import datetime
-from pathlib import Path
-from urllib.parse import quote
-from dotenv import load_dotenv
-
-load_dotenv()
-
-project_root = Path(__file__).parent.parent
-sys.path.insert(0, str(project_root))
-
-from agent.tools.builtin.browser.baseClass import (
-    init_browser_session,
-    cleanup_browser_session,
-    kill_browser_session,
-    browser_navigate_to_url,
-    browser_scroll_page,
-    browser_evaluate,
-    browser_wait,
-    browser_get_page_html,
-    _fetch_cookie_row,
-    _fetch_profile_id,
-    _normalize_cookies,
-    _cookie_domain_for_type,
-    _extract_cookie_value,
-)
-
-
-async def example_xhs_fitness_search(cookie_type: str = "xhs") -> dict:
-    """
-    小红书搜索示例
-
-    Args:
-        cookie_type: Cookie 类型,用于从数据库获取配置
-    """
-    print("\n" + "="*60)
-    print("示例: 小红书云浏览器搜索 - 健身")
-    print("="*60)
-
-    api_key = os.getenv("BROWSER_USE_API_KEY")
-    if not api_key:
-        raise RuntimeError("未找到 BROWSER_USE_API_KEY")
-
-    keyword = "健身"
-    search_url = f"https://www.xiaohongshu.com/search_result?keyword={quote(keyword)}&type=51"
-    last_data: dict = {
-        "success": False,
-        "keyword": keyword,
-        "count": 0,
-        "results": [],
-        "error": "未知错误",
-        "timestamp": datetime.now().isoformat(),
-    }
-
-    # 从数据库获取配置
-    print(f"\n🔍 从数据库获取配置 (type={cookie_type})...")
-    profile_id = _fetch_profile_id(cookie_type)
-    cookie_row = _fetch_cookie_row(cookie_type)
-
-    if profile_id:
-        print(f"✅ 获取到 cloud_profile_id: {profile_id}")
-    else:
-        print("⚠️  未找到 cloud_profile_id,将使用环境变量或默认值")
-        profile_id = os.getenv("XHS_PROFILE_ID")
-
-    if cookie_row:
-        print(f"✅ 获取到 Cookie 配置")
-    else:
-        print("⚠️  未找到 Cookie 配置")
-
-    for attempt in range(3):
-        try:
-            # 确保每次重试都清理旧会话
-            if attempt > 0:
-                try:
-                    await kill_browser_session()
-                except Exception:
-                    pass
-                await asyncio.sleep(2)  # 等待清理完成
-
-            print(f"\n🌐 启动云浏览器 (尝试 {attempt + 1}/3)...")
-            browser, tools = await init_browser_session(
-                headless=False,
-                use_cloud=True,
-                cloud_profile_id=profile_id,
-                user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
-                disable_security=False,
-            )
-            if browser is None or tools is None:
-                raise RuntimeError("浏览器初始化失败")
-
-            print("✅ 云浏览器启动成功")
-
-            # 访问首页
-            print("\n🏠 访问小红书首页...")
-            nav_result = await browser_navigate_to_url("https://www.xiaohongshu.com")
-            if nav_result.error:
-                raise RuntimeError(nav_result.error)
-            await browser_wait(3)
-
-            # 注入 Cookie(如果有)
-            if cookie_row:
-                print("\n🍪 注入 Cookie...")
-                cookie_value = _extract_cookie_value(cookie_row)
-                if cookie_value:
-                    domain, base_url = _cookie_domain_for_type(cookie_type, "https://www.xiaohongshu.com")
-                    cookies = _normalize_cookies(cookie_value, domain, base_url)
-                    if cookies:
-                        await browser._cdp_set_cookies(cookies)
-                        print(f"✅ 成功注入 {len(cookies)} 个 Cookie")
-                        # 刷新页面使 Cookie 生效
-                        await navigate_to_url("https://www.xiaohongshu.com")
-                        await browser_wait(2)
-                    else:
-                        print("⚠️  Cookie 解析失败")
-                else:
-                    print("⚠️  未找到 Cookie 值")
-
-            # 访问搜索页面
-            print(f"\n🔗 访问搜索页面: {keyword}")
-            nav_result = await browser_navigate_to_url(search_url)
-            if nav_result.error:
-                raise RuntimeError(nav_result.error)
-            await browser_wait(8)
-
-            # 滚动页面
-            print("\n📜 滚动页面...")
-            for i in range(3):
-                await browser_scroll_page(down=True, pages=2.0)
-                await browser_wait(2)
-
-            # 提取数据
-            print("\n🔍 提取数据...")
-            html_result = await browser_get_page_html()
-            if html_result.error:
-                raise RuntimeError(html_result.error)
-            html = html_result.metadata.get("html", "")
-            output_dir = project_root / "output"
-            output_dir.mkdir(parents=True, exist_ok=True)
-            output_path = output_dir / "xhs.html"
-            output_path.write_text(html or "", encoding="utf-8")
-            print(f"✅ 已保存页面 HTML: {output_path}")
-
-            extract_js = """
-        (function(){
-            const maxCount = 20;
-            const seen = new Set();
-            const results = [];
-
-            function pushItem(item){
-                if (!item || !item.link || seen.has(item.link)) return;
-                seen.add(item.link);
-                results.push(item);
-            }
-
-            const anchors = document.querySelectorAll('a[href*="/explore/"]');
-            anchors.forEach(a => {
-                if (results.length >= maxCount) return;
-                const link = a.href || '';
-                const img = a.querySelector('img');
-                const title = ((img && img.alt) || a.textContent || '').trim();
-                const cover = (img && img.src) || '';
-                if (link && title) {
-                    pushItem({ title, link, cover });
-                }
-            });
-
-            const scriptNodes = document.querySelectorAll('script[type="application/json"], script#__NEXT_DATA__, script#__NUXT__');
-            const walk = (node) => {
-                if (!node || results.length >= maxCount) return;
-                if (Array.isArray(node)) {
-                    for (const item of node) {
-                        walk(item);
-                        if (results.length >= maxCount) return;
-                    }
-                    return;
-                }
-                if (typeof node === 'object') {
-                    const title = (node.title || node.desc || node.name || node.noteTitle || '').toString().trim();
-                    const id = node.noteId || node.note_id || node.id || node.noteID;
-                    const cover = (node.cover && (node.cover.url || node.cover.urlDefault)) || node.coverUrl || node.image || '';
-                    let link = '';
-                    if (id) {
-                        link = `https://www.xiaohongshu.com/explore/${id}`;
-                    }
-                    if (title && link) {
-                        pushItem({ title, link, cover });
-                    }
-                    for (const key in node) {
-                        if (typeof node[key] === 'object') walk(node[key]);
-                    }
-                }
-            };
-
-            scriptNodes.forEach(node => {
-                if (results.length >= maxCount) return;
-                const text = node.textContent || '';
-                if (!text) return;
-                try {
-                    const data = JSON.parse(text);
-                    walk(data);
-                } catch (e) {}
-            });
-
-            return {
-                success: true,
-                keyword: __KEYWORD__,
-                count: results.length,
-                results: results,
-                timestamp: new Date().toISOString(),
-            };
-        })()
-        """
-            extract_js = extract_js.replace("__KEYWORD__", json.dumps(keyword, ensure_ascii=False))
-
-            async def run_extract() -> dict:
-                result = await browser_evaluate(extract_js)
-                if result.error:
-                    raise RuntimeError(result.error)
-                output = result.output
-                if isinstance(output, str) and output.startswith("Result: "):
-                    output = output[8:]
-                if not output:
-                    return {
-                        "success": False,
-                        "keyword": keyword,
-                        "count": 0,
-                        "results": [],
-                        "error": "可能被登录或验证码拦截",
-                        "timestamp": datetime.now().isoformat(),
-                    }
-
-                try:
-                    data = json.loads(output)
-                except Exception:
-                    data = {
-                        "success": False,
-                        "keyword": keyword,
-                        "count": 0,
-                        "results": [],
-                        "error": "JSON 解析失败",
-                        "raw_output": str(output)[:2000],
-                        "timestamp": datetime.now().isoformat(),
-                    }
-
-                if isinstance(data, dict) and data.get("count", 0) == 0:
-                    html_result = await browser_get_page_html()
-                    if html_result.error:
-                        raise RuntimeError(html_result.error)
-                    html = html_result.metadata.get("html", "")
-                    blocked_markers = ["登录", "验证", "验证码", "请先登录", "异常访问"]
-                    if html and any(marker in html for marker in blocked_markers):
-                        data = {
-                            "success": False,
-                            "keyword": keyword,
-                            "count": 0,
-                            "results": [],
-                            "error": "可能被登录或验证码拦截",
-                            "timestamp": datetime.now().isoformat(),
-                        }
-                    elif html:
-                        results = []
-                        seen = set()
-                        pattern = re.compile(r'"noteId":"(.*?)".*?"title":"(.*?)"', re.S)
-                        for match in pattern.finditer(html):
-                            note_id = match.group(1)
-                            title = match.group(2).encode("utf-8", "ignore").decode("unicode_escape").strip()
-                            link = f"https://www.xiaohongshu.com/explore/{note_id}"
-                            if note_id and link not in seen and title:
-                                seen.add(link)
-                                results.append({"title": title, "link": link})
-                            if len(results) >= 20:
-                                break
-                        if results:
-                            data = {
-                                "success": True,
-                                "keyword": keyword,
-                                "count": len(results),
-                                "results": results,
-                                "timestamp": datetime.now().isoformat(),
-                                "source": "html_fallback",
-                            }
-
-                return data
-
-            data = await run_extract()
-
-            last_data = data if isinstance(data, dict) else last_data
-
-            # 输出结果
-            if isinstance(last_data, dict) and last_data.get("count", 0) > 0:
-                print(f"\n✅ 成功获取 {last_data['count']} 条数据")
-                print(f"数据来源: {last_data.get('source', 'javascript')}")
-                print("\n前 5 条结果:")
-                for i, item in enumerate(last_data["results"][:5], 1):
-                    print(f"{i}. {item['title'][:50]}...")
-
-                # 成功获取数据,清理并返回
-                await cleanup_browser_session()
-                return last_data
-
-            if isinstance(last_data, dict) and last_data.get("error") == "可能被登录或验证码拦截":
-                print("\n⚠️  检测到登录或验证码拦截")
-                print("💡 建议:在数据库中配置有效的 Cookie")
-
-        except Exception as e:
-            err_text = str(e)
-            print(f"⚠️  尝试 {attempt + 1}/3 失败: {err_text}")
-            last_data = {
-                "success": False,
-                "keyword": keyword,
-                "count": 0,
-                "results": [],
-                "error": err_text,
-                "timestamp": datetime.now().isoformat(),
-            }
-        finally:
-            # 清理当前会话
-            try:
-                await cleanup_browser_session()
-            except Exception:
-                pass
-
-        # 如果不是最后一次尝试,等待后继续
-        if attempt < 2:
-            print(f"等待 5 秒后重试...")
-            await asyncio.sleep(5)
-
-    return last_data
-
-
-async def main():
-    # 可以通过命令行参数指定 cookie_type
-    cookie_type = sys.argv[1] if len(sys.argv) > 1 else "xhs"
-
-    data = await example_xhs_fitness_search(cookie_type)
-
-    print("\n" + "="*60)
-    print("📊 最终结果")
-    print("="*60)
-    print(json.dumps(data, ensure_ascii=False, indent=2))
-
-
-if __name__ == "__main__":
-    asyncio.run(main())

+ 0 - 186
examples/research/run.py

@@ -1,186 +0,0 @@
-"""
-浏览器调研示例 (增强版)
-
-功能:
-1. 使用 Agent 模式进行网络调研
-2. 任务结束自动关闭浏览器并杀掉进程
-3. 异常安全:即使程序崩溃也能清理环境
-"""
-
-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()
-
-import logging
-# 配置感知日志
-logging.basicConfig(level=logging.WARNING)  # 默认 WARNING
-logging.getLogger("agent.core.message_manager").setLevel(logging.INFO)  # 开启感知日志
-logging.getLogger("tools").setLevel(logging.INFO)  # 开启工具日志
-
-from agent.llm.prompts import SimplePrompt
-from agent.core.runner import AgentRunner, RunConfig
-from agent.trace import (
-    FileSystemTraceStore,
-    Trace,
-    Message,
-)
-from agent.llm import create_openrouter_llm_call
-
-# 导入浏览器清理工具
-from agent.tools.builtin.browser.baseClass import kill_browser_session
-
-async def main():
-    # 路径配置
-    base_dir = Path(__file__).parent
-    project_root = base_dir.parent.parent
-    prompt_path = base_dir / "test.prompt"
-    output_dir = base_dir / "output"
-    output_dir.mkdir(exist_ok=True)
-
-    # Skills 目录
-    skills_dir = None 
-
-    print("=" * 60)
-    print("🚀 浏览器调研任务 (Agent 模式)")
-    print("=" * 60)
-    print()
-
-    # 1. 加载 prompt
-    print("1. 加载 prompt...")
-    prompt = SimplePrompt(prompt_path)
-
-    # 提取配置
-    system_prompt = prompt._messages.get("system", "")
-    user_task = prompt._messages.get("user", "")
-    model_name = prompt.config.get('model', 'gemini-2.5-flash')
-    temperature = float(prompt.config.get('temperature', 0.3))
-
-    print(f"   - 任务: {user_task[:80]}...")
-    print(f"   - 模型: {model_name}")
-
-    # 2. 构建消息
-    print("2. 构建任务消息...")
-    messages = prompt.build_messages()
-
-    # 3. 创建 Agent Runner
-    print("3. 创建 Agent Runner...")
-    runner = AgentRunner(
-        trace_store=FileSystemTraceStore(base_path=str(trace_dir)),
-        llm_call=create_openrouter_llm_call(model=f"google/{model_name}"),
-        skills_dir=skills_dir,
-        debug=True 
-    )
-
-    final_response = ""
-    current_trace_id = None
-
-    # 4. Agent 模式执行(使用 try...finally 确保清理)
-    try:
-        print(f"4. 启动 Agent 模式执行...")
-        print()
-
-        async for item in runner.run(
-            messages=messages,
-            config=RunConfig(
-                system_prompt=system_prompt,
-                model=f"google/{model_name}",
-                temperature=temperature,
-                max_iterations=20,
-                name=user_task[:50],
-            ),
-        ):
-            # 处理 Trace 对象(整体状态变化)
-            if isinstance(item, Trace):
-                current_trace_id = item.trace_id
-                if item.status == "running":
-                    print(f"[Trace] 开始: {item.trace_id[:8]}")
-                elif item.status == "completed":
-                    print(f"[Trace] 完成")
-                    print(f"  - Total tokens: {item.total_tokens}")
-                    print(f"  - Total cost: ${item.total_cost:.4f}")
-                elif item.status == "failed":
-                    print(f"[Trace] 失败: {item.error_message}")
-
-            # 处理 Message 对象(执行过程)
-            elif isinstance(item, Message):
-                if item.role == "assistant":
-                    content = item.content
-                    if isinstance(content, dict):
-                        text = content.get("text", "")
-                        tool_calls = content.get("tool_calls")
-
-                        if text and not tool_calls:
-                            final_response = text
-                            print(f"[Response] Agent 给出最终回复")
-                        elif text:
-                            # 增加打印长度到 300,方便观察
-                            print(f"[Assistant] {text[:300]}...")
-
-                        if tool_calls:
-                            for tc in tool_calls:
-                                tool_name = tc.get("function", {}).get("name", "unknown")
-                                print(f"[Tool Call] 🛠️ {tool_name}")
-
-                elif item.role == "tool":
-                    content = item.content
-                    if isinstance(content, dict):
-                        tool_name = content.get("tool_name", "unknown")
-                        print(f"[Tool Result] ✅ {tool_name}")
-                    if item.description:
-                        desc = item.description[:80] if len(item.description) > 80 else item.description
-                        print(f"  {desc}...")
-
-        # 5. 输出结果
-        print()
-        print("=" * 60)
-        print("Final Agent Response:")
-        print("=" * 60)
-        print(final_response)
-        print("=" * 60)
-        print()
-
-        # 6. 保存结果
-        output_file = output_dir / "research_result.txt"
-        with open(output_file, 'w', encoding='utf-8') as f:
-            f.write(final_response)
-        print(f"✓ 结果已保存到: {output_file}")
-
-    except Exception as e:
-        print(f"\n❌ 程序运行崩溃: {str(e)}")
-        import traceback
-        traceback.print_exc()
-
-    finally:
-        # --- 核心逻辑:无论成功失败,必须关闭浏览器进程 ---
-        print("\n" + "·" * 40)
-        print("🧹 正在清理浏览器环境,关闭 CDP 会话并终止进程...")
-        try:
-            # 强制杀掉浏览器进程,释放容器或本地端口
-            await kill_browser_session()
-            print("✅ 浏览器已安全关闭。")
-        except Exception as cleanup_err:
-            print(f"⚠️ 清理浏览器时出现错误: {cleanup_err}")
-        print("·" * 40 + "\n")
-
-    # 7. 可视化提示
-    if current_trace_id:
-        print("=" * 60)
-        print("可视化 Step Tree:")
-        print("=" * 60)
-        print("1. 启动 API Server: python3 api_server.py")
-        print(f"2. 访问: http://localhost:8000/api/traces")
-        print(f"3. Trace ID: {current_trace_id}")
-        print("=" * 60)
-
-if __name__ == "__main__":
-    try:
-        asyncio.run(main())
-    except KeyboardInterrupt:
-        print("\n🛑 用户手动终止 (KeyboardInterrupt),正在强制退出...")

+ 0 - 10
examples/research/test.prompt

@@ -1,10 +0,0 @@
----
-model: gemini-2.5-flash
-temperature: 0.3
----
-
-$system$
-你是最顶尖的AI助手,可以拆分并调用工具逐步解决复杂问题。
-
-$user$
-使用浏览器帮我做个调研:一张图片中的构图可以如何表示?我希望寻找一些构图特征的表示方法。尝试查阅一些论文pdf, 网页等资料,最后输出一份调研报告。

+ 0 - 70
examples/subagent_example.py

@@ -1,70 +0,0 @@
-"""
-Sub-Agent 使用示例
-
-演示如何使用 Sub-Agent 机制处理复杂任务。
-
-注意:本示例中的 AgentDefinition 和 get_agent_registry 尚未实现,
-此处仅用于演示未来的设计方向。当前可用的 subagent 功能通过
-runner.run(messages, config=RunConfig(...)) 和工具层的 subagent 工具实现。
-"""
-
-import asyncio
-import os
-from agent import AgentRunner
-from agent.core.runner import RunConfig
-from agent.trace import Trace, Message
-from agent.llm import create_gemini_llm_call
-
-
-async def example_basic_subagent():
-    """示例 1: 使用 Agent 执行任务(通过 subagent 工具自动委托子任务)"""
-    print("=== 示例 1: 基本 Agent 执行 ===\n")
-
-    runner = AgentRunner(
-        llm_call=create_gemini_llm_call(os.getenv("GEMINI_API_KEY")),
-    )
-
-    task = """
-    分析这个 Python 项目的架构:
-    1. 找出所有主要的模块和它们的职责
-    2. 识别核心的数据流
-    3. 列出使用的外部依赖
-
-    请使用 subagent explore 模式来探索代码库。
-    """
-
-    async for item in runner.run(
-        messages=[{"role": "user", "content": task}],
-        config=RunConfig(
-            model="gemini-2.0-flash-exp",
-            max_iterations=20,
-            name="项目架构分析",
-        ),
-    ):
-        if isinstance(item, Trace):
-            if item.status == "running":
-                print(f"[Trace] 开始: {item.trace_id[:8]}")
-            elif item.status == "completed":
-                print(f"[Trace] 完成 (tokens: {item.total_tokens})")
-        elif isinstance(item, Message):
-            if item.role == "assistant":
-                content = item.content
-                if isinstance(content, dict):
-                    text = content.get("text", "")
-                    tool_calls = content.get("tool_calls")
-                    if tool_calls:
-                        for tc in tool_calls:
-                            tool_name = tc.get("function", {}).get("name", "")
-                            if tool_name == "subagent":
-                                print(f"  启动 Sub-Agent...")
-                    elif text:
-                        print(f"\n最终结果:\n{text[:500]}")
-
-
-async def main():
-    """运行示例"""
-    await example_basic_subagent()
-
-
-if __name__ == "__main__":
-    asyncio.run(main())

+ 0 - 129
examples/test_skill.py

@@ -1,129 +0,0 @@
-import json
-import subprocess
-import time
-from pathlib import Path
-
-
-def run_cli(session: str, args: list[str]) -> dict:
-    command = ["browser-use", "--session", session, "--json"] + args
-    result = subprocess.run(command, capture_output=True, text=True)
-    if result.returncode != 0:
-        raise RuntimeError(result.stderr.strip() or "browser-use command failed")
-    payload = result.stdout.strip()
-    if not payload:
-        raise RuntimeError("browser-use returned empty output")
-    data = json.loads(payload)
-    if not data.get("success", False):
-        raise RuntimeError(data.get("error", "browser-use command error"))
-    return data.get("data", {})
-
-
-def stop_session_server(session: str) -> None:
-    subprocess.run(
-        ["browser-use", "--session", session, "server", "stop"],
-        capture_output=True,
-        text=True,
-    )
-
-
-def main():
-    project_root = Path(__file__).resolve().parents[1]
-    output_dir = project_root / "output"
-    output_dir.mkdir(parents=True, exist_ok=True)
-
-    json_file = output_dir / "skill_baidu.json"
-    html_file = output_dir / "skill_baidu_page.html"
-
-    session = "skill_baidu"
-    keyword = "瑜伽美女"
-
-    try:
-        stop_session_server(session)
-        try:
-            run_cli(session, ["open", "https://www.baidu.com"])
-        except RuntimeError:
-            stop_session_server(session)
-            run_cli(session, ["open", "https://www.baidu.com"])
-
-        search_js = (
-            "(function(){"
-            "const input=document.querySelector('#kw');"
-            "const btn=document.querySelector('#su');"
-            "if(input){input.value='" + keyword + "';}"
-            "if(btn){btn.click();}"
-            "else if(input&&input.form){input.form.submit();}"
-            "return {hasInput:!!input,hasButton:!!btn};"
-            "})()"
-        )
-        run_cli(session, ["eval", search_js])
-
-        wait_js = (
-            "(function(){"
-            "const items=document.querySelectorAll('#content_left .result, #content_left .c-container, #content_left .result-op');"
-            "const bodyReady=!!document.body;"
-            "const bodyLen=bodyReady?(document.body.innerText||'').length:0;"
-            "return {count:items.length, bodyReady:bodyReady, bodyLen:bodyLen};"
-            "})()"
-        )
-
-        count = 0
-        for _ in range(12):
-            data = run_cli(session, ["eval", wait_js])
-            result = data.get("result") if isinstance(data, dict) else {}
-            count = int(result.get("count") or 0)
-            body_len = int(result.get("bodyLen") or 0)
-            if count >= 3 or body_len > 1000:
-                break
-            time.sleep(1)
-
-        extract_js = (
-            "(function(){"
-            "const items=Array.from(document.querySelectorAll('#content_left .result, #content_left .c-container, #content_left .result-op'));"
-            "const results=[];"
-            "for(const item of items){"
-            "const a=item.querySelector('h3 a')||item.querySelector('a[data-click]')||item.querySelector('a');"
-            "if(!a) continue;"
-            "const title=(a.textContent||'').trim();"
-            "const link=a.href||'';"
-            "const summaryEl=item.querySelector('.c-abstract, .content-right_8Zs40, .content-right_8Zs40_2gVt2');"
-            "const summary=(summaryEl?summaryEl.textContent:'').trim();"
-            "results.push({index:results.length+1,title,link,summary});"
-            "if(results.length>=10) break;"
-            "}"
-            "return {success:true,keyword:'" + keyword + "',count:results.length,timestamp:new Date().toISOString(),results:results};"
-            "})()"
-        )
-
-        data = run_cli(session, ["eval", extract_js])
-        extracted = data.get("result") if isinstance(data, dict) else data
-
-        if not extracted:
-            extracted = {
-                "success": False,
-                "keyword": keyword,
-                "count": 0,
-                "timestamp": time.strftime("%Y-%m-%dT%H:%M:%S"),
-                "results": [],
-            }
-
-        with open(json_file, "w", encoding="utf-8") as f:
-            json.dump(extracted, f, ensure_ascii=False, indent=2)
-
-        html_data = run_cli(session, ["eval", "document.documentElement.outerHTML"])
-        html_content = html_data.get("result") if isinstance(html_data, dict) else html_data
-
-        with open(html_file, "w", encoding="utf-8") as f:
-            f.write(html_content or "")
-
-        print(f"✅ 数据已保存到: {json_file}")
-        print(f"✅ HTML 已保存到: {html_file}")
-
-    finally:
-        try:
-            run_cli(session, ["close"])
-        except Exception:
-            pass
-
-
-if __name__ == "__main__":
-    main()

+ 0 - 61
examples/test_subagent_real/README.md

@@ -1,61 +0,0 @@
-# Subagent 工具真实测试
-
-本测试用例用于验证 subagent 工具在真实 LLM 环境下的表现。
-
-## 测试目标
-
-测试 subagent 工具的三种核心模式:
-
-1. **delegate 模式** - 委托子任务给专门的 agent 处理
-2. **explore 模式** - 并行探索多个可能的方案
-3. **evaluate 模式** - 评估任务完成情况
-
-## 测试场景
-
-分析 Agent-main 项目的架构,这个任务自然需要:
-- 委托不同模块的分析(delegate)
-- 并行探索改进方案(explore)
-- 评估分析完整性(evaluate)
-
-## 运行方式
-
-```bash
-cd /Users/elksmmx/Desktop/agent_2.9/Agent-main
-python examples/test_subagent_real/run.py
-```
-
-## 前置要求
-
-1. 配置 `.env` 文件,设置 OpenRouter API Key:
-   ```
-   OPENROUTER_API_KEY=your_key_here
-   ```
-
-2. 确保已安装依赖:
-   ```bash
-   pip install -r requirements.txt
-   ```
-
-## 预期结果
-
-Agent 应该:
-1. 使用 delegate 模式委托 2-4 个子任务分析不同模块
-2. 使用 explore 模式并行探索 2-3 个改进方案
-3. 使用 evaluate 模式评估分析的完整性
-4. 生成完整的架构分析报告
-
-## 输出
-
-- 控制台:实时显示 agent 执行过程和 subagent 调用
-- 文件:`output/subagent_test_result.txt` 包含最终结果和统计
-- Trace:`.trace/` 目录保存完整执行记录
-
-## 可视化
-
-启动 API Server 查看 trace tree:
-
-```bash
-python3 api_server.py
-```
-
-访问:http://localhost:8000/api/traces

+ 0 - 218
examples/test_subagent_real/run.py

@@ -1,218 +0,0 @@
-"""
-Subagent 工具真实测试
-
-使用真实 LLM 测试 subagent 工具的三种模式:
-1. delegate - 委托子任务
-2. explore - 并行探索方案
-3. evaluate - 评估结果
-"""
-
-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.llm.prompts import SimplePrompt
-from agent.core.runner import AgentRunner, RunConfig
-from agent.trace import (
-    FileSystemTraceStore,
-    Trace,
-    Message,
-)
-from agent.llm import create_openrouter_llm_call
-
-
-async def main():
-    # 路径配置
-    base_dir = Path(__file__).parent
-    project_root = base_dir.parent.parent
-    prompt_path = base_dir / "test.prompt"
-    output_dir = base_dir / "output"
-    output_dir.mkdir(exist_ok=True)
-
-    print("=" * 60)
-    print("Subagent 工具测试 (真实 LLM)")
-    print("=" * 60)
-    print()
-
-    # 1. 加载 prompt
-    print("1. 加载 prompt...")
-    prompt = SimplePrompt(prompt_path)
-
-    # 提取配置
-    system_prompt = prompt._messages.get("system", "")
-    user_task = prompt._messages.get("user", "")
-    model_name = prompt.config.get('model', 'gemini-2.5-flash')
-    temperature = float(prompt.config.get('temperature', 0.3))
-
-    print(f"   - 任务: {user_task[:80]}...")
-    print(f"   - 模型: {model_name}")
-
-    # 2. 构建消息
-    print("2. 构建任务消息...")
-    messages = prompt.build_messages()
-
-    # 3. 创建 Agent Runner
-    print("3. 创建 Agent Runner...")
-    print(f"   - 模型: {model_name} (via OpenRouter)")
-
-    # Trace 输出到测试目录
-    trace_dir = base_dir / ".trace"
-    trace_dir.mkdir(exist_ok=True)
-    print(f"   - Trace 目录: {trace_dir}")
-
-    runner = AgentRunner(
-        trace_store=FileSystemTraceStore(base_path=str(trace_dir)),
-        llm_call=create_openrouter_llm_call(model=f"google/{model_name}"),
-        skills_dir=None,
-        debug=True
-    )
-
-    # 4. Agent 模式执行
-    print(f"4. 启动 Agent 模式...")
-    print()
-
-    final_response = ""
-    current_trace_id = None
-    subagent_calls = []
-
-    async for item in runner.run(
-        messages=messages,
-        config=RunConfig(
-            system_prompt=system_prompt,
-            model=f"google/{model_name}",
-            temperature=temperature,
-            max_iterations=30,
-            name=user_task[:50],
-        ),
-    ):
-        # 处理 Trace 对象
-        if isinstance(item, Trace):
-            current_trace_id = item.trace_id
-            if item.status == "running":
-                print(f"[Trace] 开始: {item.trace_id[:8]}")
-            elif item.status == "completed":
-                print(f"[Trace] 完成")
-                print(f"  - Total messages: {item.total_messages}")
-                print(f"  - Total tokens: {item.total_tokens}")
-                print(f"  - Total cost: ${item.total_cost:.4f}")
-            elif item.status == "failed":
-                print(f"[Trace] 失败: {item.error_message}")
-
-        # 处理 Message 对象
-        elif isinstance(item, Message):
-            if item.role == "assistant":
-                content = item.content
-                if isinstance(content, dict):
-                    text = content.get("text", "")
-                    tool_calls = content.get("tool_calls")
-
-                    if text and not tool_calls:
-                        final_response = text
-                        print(f"[Response] Agent 完成")
-                    elif text:
-                        print(f"[Assistant] {text[:100]}...")
-
-                    if tool_calls:
-                        for tc in tool_calls:
-                            tool_name = tc.get("function", {}).get("name", "unknown")
-                            print(f"[Tool Call] {tool_name}")
-
-                            # 记录 subagent 调用
-                            if tool_name == "subagent":
-                                import json
-                                args = tc.get("function", {}).get("arguments", {})
-                                # arguments 可能是字符串,需要解析
-                                if isinstance(args, str):
-                                    try:
-                                        args = json.loads(args)
-                                    except:
-                                        args = {}
-                                mode = args.get("mode", "unknown")
-                                subagent_calls.append({
-                                    "mode": mode,
-                                    "task": args.get("task", args.get("background", ""))[:50]
-                                })
-                                print(f"  → mode: {mode}")
-
-            elif item.role == "tool":
-                content = item.content
-                if isinstance(content, dict):
-                    tool_name = content.get("tool_name", "unknown")
-                    print(f"[Tool Result] {tool_name}")
-                if item.description:
-                    desc = item.description[:80] if len(item.description) > 80 else item.description
-                    print(f"  {desc}...")
-
-    # 5. 输出结果
-    print()
-    print("=" * 60)
-    print("Agent 响应:")
-    print("=" * 60)
-    print(final_response)
-    print("=" * 60)
-    print()
-
-    # 6. 统计 subagent 调用
-    print("=" * 60)
-    print("Subagent 调用统计:")
-    print("=" * 60)
-    delegate_count = sum(1 for call in subagent_calls if call["mode"] == "delegate")
-    explore_count = sum(1 for call in subagent_calls if call["mode"] == "explore")
-    evaluate_count = sum(1 for call in subagent_calls if call["mode"] == "evaluate")
-
-    print(f"  - delegate 模式: {delegate_count} 次")
-    print(f"  - explore 模式: {explore_count} 次")
-    print(f"  - evaluate 模式: {evaluate_count} 次")
-    print(f"  - 总计: {len(subagent_calls)} 次")
-    print()
-
-    for i, call in enumerate(subagent_calls, 1):
-        print(f"  {i}. [{call['mode']}] {call['task']}...")
-    print("=" * 60)
-    print()
-
-    # 7. 保存结果
-    output_file = output_dir / "subagent_test_result.txt"
-    with open(output_file, 'w', encoding='utf-8') as f:
-        f.write("=" * 60 + "\n")
-        f.write("Agent 响应\n")
-        f.write("=" * 60 + "\n\n")
-        f.write(final_response)
-        f.write("\n\n" + "=" * 60 + "\n")
-        f.write("Subagent 调用统计\n")
-        f.write("=" * 60 + "\n\n")
-        f.write(f"delegate 模式: {delegate_count} 次\n")
-        f.write(f"explore 模式: {explore_count} 次\n")
-        f.write(f"evaluate 模式: {evaluate_count} 次\n")
-        f.write(f"总计: {len(subagent_calls)} 次\n\n")
-        for i, call in enumerate(subagent_calls, 1):
-            f.write(f"{i}. [{call['mode']}] {call['task']}...\n")
-
-    print(f"✓ 结果已保存到: {output_file}")
-    print()
-
-    # 8. 可视化提示
-    print("=" * 60)
-    print("Trace 信息:")
-    print("=" * 60)
-    print(f"Trace ID: {current_trace_id}")
-    print(f"Trace 目录: {trace_dir}")
-    print()
-    print("查看 trace 文件:")
-    print(f"   ls -la {trace_dir}")
-    print()
-    print("或启动 API Server 可视化:")
-    print("   python3 api_server.py")
-    print("   访问: http://localhost:8000/api/traces")
-    print("=" * 60)
-
-
-if __name__ == "__main__":
-    asyncio.run(main())

+ 0 - 28
examples/test_subagent_real/test.prompt

@@ -1,28 +0,0 @@
----
-model: gemini-2.5-flash
-temperature: 0.3
----
-
-$system$
-你是一个专业的代码分析助手,擅长使用 subagent 工具来分解复杂任务。
-
-你有以下工具可用:
-- subagent: 用于委托子任务、并行探索方案、评估结果
-- read_file, glob_files, grep_content: 用于代码分析
-- goal: 用于任务规划和进度追踪
-
-**重要规则**:
-- 在任务完成前,必须始终保持至少一个活跃的 goal
-- 当所有 goal 完成后,如果任务还未完全结束,必须立即创建新的 goal
-- 只有在确认任务完全完成后,才能让 goal 列表为空
-- goal 为空表示任务已完成,系统将结束执行
-
-$user$
-请分析 /Users/elksmmx/Desktop/agent_2.9/Agent-main 项目的架构,并提出改进建议。
-
-具体要求:
-1. 使用 subagent 的 delegate 模式,委托子 agent 分析不同模块(core、trace、tools、memory)
-2. 使用 subagent 的 explore 模式,并行探索 2-3 个可能的架构改进方案
-3. 使用 subagent 的 evaluate 模式,评估你的分析是否完整
-
-请充分利用 subagent 工具的各种模式来完成这个任务。

+ 0 - 28
examples/test_subagent_real/test_continue.prompt

@@ -1,28 +0,0 @@
----
-model: gemini-2.5-flash
-temperature: 0.3
----
-
-$system$
-你是一个专业的代码分析助手,擅长使用 subagent 工具来分解复杂任务。
-
-你有以下工具可用:
-- subagent: 用于委托子任务、并行探索方案、评估结果
-  - mode="delegate": 委托子任务
-  - mode="explore": 并行探索多个方案
-  - mode="evaluate": 评估结果
-  - continue_from: 继续已有的 trace(用于迭代改进)
-- read_file, glob_files, grep_content: 用于代码分析
-- goal: 用于任务规划和进度追踪
-
-$user$
-请分析 /Users/elksmmx/Desktop/agent_2.9/Agent-main 项目的 core 模块架构,并提出改进建议。
-
-具体要求:
-1. 使用 subagent 的 delegate 模式,委托子 agent 分析 core 模块的基本架构
-2. 使用 subagent 的 delegate 模式 + continue_from 参数,继续深入分析 core 模块的设计模式和最佳实践
-3. 使用 subagent 的 explore 模式,并行探索 2-3 个可能的改进方案
-4. 使用 subagent 的 evaluate 模式,评估你的分析是否完整
-5. 如果评估不通过,使用 continue_from 继续改进分析
-
-**重点测试 continue_from 参数的使用**,展示如何在同一个 trace 上迭代改进任务。

+ 0 - 187
examples/test_subagent_real/visualize_trace.py

@@ -1,187 +0,0 @@
-"""
-Trace 树可视化工具
-
-读取 trace 目录并生成树形结构的可视化输出
-"""
-
-import json
-import sys
-from pathlib import Path
-from datetime import datetime
-
-
-def load_trace_meta(trace_dir):
-    """加载 trace 的 meta.json"""
-    meta_file = trace_dir / "meta.json"
-    if not meta_file.exists():
-        return None
-    with open(meta_file, 'r', encoding='utf-8') as f:
-        return json.load(f)
-
-
-def format_duration(start_str, end_str):
-    """计算并格式化持续时间"""
-    if not start_str or not end_str:
-        return "N/A"
-    try:
-        start = datetime.fromisoformat(start_str)
-        end = datetime.fromisoformat(end_str)
-        duration = (end - start).total_seconds()
-        return f"{duration:.1f}s"
-    except:
-        return "N/A"
-
-
-def extract_mode_from_trace_id(trace_id):
-    """从 trace_id 中提取模式"""
-    if '@delegate-' in trace_id:
-        return 'delegate'
-    elif '@explore-' in trace_id:
-        return 'explore'
-    elif '@evaluate-' in trace_id:
-        return 'evaluate'
-    return 'main'
-
-
-def print_trace_tree(trace_base_path, output_file=None):
-    """打印 trace 树结构"""
-    trace_base = Path(trace_base_path)
-
-    if not trace_base.exists():
-        print(f"错误: Trace 目录不存在: {trace_base}")
-        return
-
-    # 查找所有 trace 目录
-    all_traces = {}
-    main_trace_id = None
-
-    for trace_dir in sorted(trace_base.iterdir()):
-        if not trace_dir.is_dir():
-            continue
-
-        meta = load_trace_meta(trace_dir)
-        if not meta:
-            continue
-
-        trace_id = meta['trace_id']
-        all_traces[trace_id] = {
-            'meta': meta,
-            'dir': trace_dir,
-            'children': []
-        }
-
-        # 找到主 trace
-        if meta.get('parent_trace_id') is None:
-            main_trace_id = trace_id
-
-    if not main_trace_id:
-        print("错误: 未找到主 trace")
-        return
-
-    # 构建树结构
-    for trace_id, trace_info in all_traces.items():
-        parent_id = trace_info['meta'].get('parent_trace_id')
-        if parent_id and parent_id in all_traces:
-            all_traces[parent_id]['children'].append(trace_id)
-
-    # 输出函数
-    def output(text):
-        print(text)
-        if output_file:
-            output_file.write(text + '\n')
-
-    # 打印树
-    output("=" * 80)
-    output("Trace 执行树")
-    output("=" * 80)
-    output("")
-
-    def print_node(trace_id, prefix="", is_last=True):
-        trace_info = all_traces[trace_id]
-        meta = trace_info['meta']
-
-        # 树形连接符
-        connector = "└── " if is_last else "├── "
-
-        # 提取信息
-        mode = extract_mode_from_trace_id(trace_id)
-        task = meta.get('task', 'N/A')
-        if len(task) > 60:
-            task = task[:60] + "..."
-        status = meta.get('status', 'unknown')
-        messages = meta.get('total_messages', 0)
-        tokens = meta.get('total_tokens', 0)
-        duration = format_duration(
-            meta.get('created_at'),
-            meta.get('completed_at')
-        )
-
-        # 状态符号
-        status_symbol = {
-            'completed': '✓',
-            'failed': '✗',
-            'running': '⟳',
-        }.get(status, '?')
-
-        # 打印节点
-        output(f"{prefix}{connector}[{mode}] {status_symbol} {trace_id[:8]}")
-        output(f"{prefix}{'    ' if is_last else '│   '}Task: {task}")
-        output(f"{prefix}{'    ' if is_last else '│   '}Stats: {messages} msgs, {tokens:,} tokens, {duration}")
-
-        # 打印子节点
-        children = trace_info['children']
-        for i, child_id in enumerate(children):
-            is_last_child = (i == len(children) - 1)
-            child_prefix = prefix + ("    " if is_last else "│   ")
-            print_node(child_id, child_prefix, is_last_child)
-
-    # 从主 trace 开始打印
-    print_node(main_trace_id)
-
-    output("")
-    output("=" * 80)
-    output("统计信息")
-    output("=" * 80)
-
-    # 统计各模式的数量
-    mode_counts = {}
-    total_messages = 0
-    total_tokens = 0
-
-    for trace_info in all_traces.values():
-        meta = trace_info['meta']
-        mode = extract_mode_from_trace_id(meta['trace_id'])
-        mode_counts[mode] = mode_counts.get(mode, 0) + 1
-        total_messages += meta.get('total_messages', 0)
-        total_tokens += meta.get('total_tokens', 0)
-
-    output(f"总 Trace 数: {len(all_traces)}")
-    output(f"  - main: {mode_counts.get('main', 0)}")
-    output(f"  - delegate: {mode_counts.get('delegate', 0)}")
-    output(f"  - explore: {mode_counts.get('explore', 0)}")
-    output(f"  - evaluate: {mode_counts.get('evaluate', 0)}")
-    output(f"")
-    output(f"总消息数: {total_messages}")
-    output(f"总 Token 数: {total_tokens:,}")
-    output("=" * 80)
-
-
-if __name__ == "__main__":
-    if len(sys.argv) < 2:
-        print("用法: python visualize_trace.py <trace_directory> [output_file]")
-        print("示例: python visualize_trace.py .trace")
-        sys.exit(1)
-
-    trace_dir = sys.argv[1]
-    output_path = sys.argv[2] if len(sys.argv) > 2 else None
-
-    output_file = None
-    if output_path:
-        output_file = open(output_path, 'w', encoding='utf-8')
-
-    try:
-        print_trace_tree(trace_dir, output_file)
-    finally:
-        if output_file:
-            output_file.close()
-            print(f"\n✓ 输出已保存到: {output_path}")

+ 0 - 141
examples/test_tools_baidu.py

@@ -1,141 +0,0 @@
-import asyncio
-import json
-import os
-import sys
-from datetime import datetime
-from pathlib import Path
-from urllib.parse import quote
-
-sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
-
-from agent.tools.builtin.browser.baseClass import (
-    init_browser_session,
-    browser_navigate_to_url,
-    browser_wait,
-    browser_get_page_html,
-    browser_evaluate,
-    browser_scroll_page,
-    cleanup_browser_session,
-)
-
-
-async def run_task():
-    project_root = Path(__file__).resolve().parents[1]
-    output_dir = project_root / "output"
-    output_dir.mkdir(parents=True, exist_ok=True)
-
-    json_file = output_dir / "baidu.json"
-    html_file = output_dir / "baidu_page.html"
-
-    try:
-        await init_browser_session(headless=False, profile_name="baidu_profile")
-
-        await browser_navigate_to_url("https://www.baidu.com")
-        await browser_wait(seconds=2)
-
-        keyword = "Python 教程"
-        search_url = f"https://www.baidu.com/s?wd={quote(keyword)}"
-        await browser_navigate_to_url(search_url)
-        await browser_wait(seconds=3)
-        await browser_scroll_page(down=True, pages=1.0)
-        await browser_wait(seconds=2)
-
-        extract_js = """
-        (function(){
-            try {
-                const results = [];
-                const resultItems = document.querySelectorAll('#content_left > div[class*="result"]');
-                resultItems.forEach((item, index) => {
-                    if (index >= 10) return;
-                    try {
-                        const titleEl = item.querySelector('h3 a, .t a');
-                        const title = titleEl ? titleEl.textContent.trim() : '';
-                        const link = titleEl ? titleEl.href : '';
-                        const summaryEl = item.querySelector('.c-abstract, .content-right_8Zs40');
-                        const summary = summaryEl ? summaryEl.textContent.trim() : '';
-                        const sourceEl = item.querySelector('.c-color-gray, .source_1Vdff');
-                        const source = sourceEl ? sourceEl.textContent.trim() : '';
-                        if (title || link) {
-                            results.push({
-                                index: index + 1,
-                                title: title,
-                                link: link,
-                                summary: summary.substring(0, 200),
-                                source: source
-                            });
-                        }
-                    } catch (e) {
-                    }
-                });
-                return {
-                    success: true,
-                    count: results.length,
-                    keyword: 'Python 教程',
-                    timestamp: new Date().toISOString(),
-                    results: results
-                };
-            } catch (e) {
-                return {
-                    success: false,
-                    error: e.message,
-                    stack: e.stack
-                };
-            }
-        })()
-        """
-
-        result = await browser_evaluate(code=extract_js)
-        output = result.output
-        if output.startswith("Result: "):
-            output = output[8:]
-
-        try:
-            data = json.loads(output)
-        except json.JSONDecodeError:
-            data = {
-                "success": False,
-                "error": "JSON解析失败",
-                "raw_output": output[:1000],
-                "keyword": keyword,
-                "timestamp": datetime.now().isoformat(),
-            }
-
-        with open(json_file, "w", encoding="utf-8") as f:
-            json.dump(data, f, ensure_ascii=False, indent=2)
-
-        html_result = await browser_get_page_html()
-        html_content = html_result.metadata.get("html", "")
-        page_url = html_result.metadata.get("url", "")
-        page_title = html_result.metadata.get("title", "")
-        meta_info = (
-            "\n".join(
-                [
-                    "<!--",
-                    f"    页面标题: {page_title}",
-                    f"    页面URL: {page_url}",
-                    f"    保存时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
-                    f"    搜索关键词: {keyword}",
-                    "-->",
-                    "",
-                ]
-            )
-            + "\n"
-        )
-
-        with open(html_file, "w", encoding="utf-8") as f:
-            f.write(meta_info)
-            f.write(html_content)
-
-        print(f"✅ 数据已保存到: {json_file}")
-        print(f"✅ HTML 已保存到: {html_file}")
-
-    finally:
-        await cleanup_browser_session()
-
-
-def main():
-    asyncio.run(run_task())
-
-
-if __name__ == "__main__":
-    main()

+ 0 - 247
examples/test_xhs_container.py

@@ -1,247 +0,0 @@
-"""
-小红书容器测试脚本
-演示容器浏览器的使用:
-1. 初始化容器浏览器(自动创建容器并连接)
-2. 搜索健身
-3. 随机进入一个详情页
-4. 获取详情页的HTML和iframe并保存到output
-"""
-
-import sys
-import os
-import asyncio
-import json
-import random
-from datetime import datetime
-from pathlib import Path
-from urllib.parse import quote
-from dotenv import load_dotenv
-
-load_dotenv()
-
-project_root = Path(__file__).parent.parent
-sys.path.insert(0, str(project_root))
-
-from agent.tools.builtin.browser.baseClass import (
-    init_browser_session,
-    cleanup_browser_session,
-    browser_navigate_to_url,
-    browser_scroll_page,
-    browser_evaluate,
-    browser_wait,
-    browser_get_page_html,
-    browser_switch_tab,
-)
-
-
-async def test_xhs_container():
-    """
-    测试小红书容器功能
-    """
-    print("\n" + "="*60)
-    print("小红书容器测试")
-    print("="*60)
-
-    keyword = "健身"
-    search_url = f"https://www.xiaohongshu.com/search_result?keyword={quote(keyword)}&type=51"
-
-    # 创建输出目录
-    output_dir = project_root / "output"
-    output_dir.mkdir(parents=True, exist_ok=True)
-
-    try:
-        # 初始化容器浏览器(一步完成)
-        print(f"\n🚀 初始化容器浏览器...")
-        browser, tools = await init_browser_session(
-            browser_type="container",
-            url="https://www.xiaohongshu.com",  # 容器启动时访问的URL
-            headless=True
-        )
-
-        print("✅ 容器浏览器初始化成功")
-
-        # 等待页面完全加载
-        await browser_wait(3)
-
-        # 步骤1: 搜索健身
-        print(f"\n🔍 搜索关键词: {keyword}")
-        try:
-            nav_result = await browser_navigate_to_url(search_url)
-            if nav_result.error:
-                print(f"⚠️  导航警告: {nav_result.error[:100]}")
-        except Exception as e:
-            print(f"⚠️  导航异常: {str(e)[:100]}")
-
-        await browser_wait(10)
-
-        # 滚动页面加载更多内容
-        print("\n📜 滚动页面...")
-        for i in range(2):
-            await browser_scroll_page(down=True, pages=2.0)
-            await browser_wait(2)
-
-        # 提取搜索结果
-        print("\n🔍 提取搜索结果...")
-
-        # 先保存HTML看看页面内容
-        html_result = await browser_get_page_html()
-        if not html_result.error:
-            html = html_result.metadata.get("html", "")
-            debug_html_path = output_dir / "search_page_debug.html"
-            debug_html_path.write_text(html or "", encoding="utf-8")
-            print(f"   💾 已保存搜索页HTML用于调试: {debug_html_path}")
-
-        extract_js = """
-        (function(){
-            const results = [];
-            const seen = new Set();
-
-            const anchors = document.querySelectorAll('a[href*="/explore/"]');
-            anchors.forEach(a => {
-                const link = a.href || '';
-                if (link && !seen.has(link)) {
-                    seen.add(link);
-                    const img = a.querySelector('img');
-                    const title = ((img && img.alt) || a.textContent || '').trim();
-                    results.push({ title, link });
-                }
-            });
-
-            return results;
-        })()
-        """
-
-        eval_result = await browser_evaluate(extract_js)
-        if eval_result.error:
-            raise RuntimeError(f"提取搜索结果失败: {eval_result.error}")
-
-        output = eval_result.output
-        if isinstance(output, str) and output.startswith("Result: "):
-            output = output[8:]
-
-        posts = json.loads(output) if isinstance(output, str) else output
-
-        if not posts or len(posts) == 0:
-            raise RuntimeError("未找到任何帖子")
-
-        print(f"✅ 找到 {len(posts)} 个帖子")
-
-        # 步骤2: 随机进入一个详情页
-        selected_post = random.choice(posts)
-        post_url = selected_post["link"]
-
-        print(f"\n🎲 随机选择帖子: {selected_post['title'][:50]}...")
-        print(f"🔗 访问帖子详情页: {post_url}")
-
-        try:
-            nav_result = await browser_navigate_to_url(post_url)
-            if nav_result.error:
-                print(f"⚠️  导航警告: {nav_result.error[:100]}")
-        except Exception as e:
-            print(f"⚠️  导航异常: {str(e)[:100]}")
-
-        await browser_wait(8)
-
-        # 滚动详情页
-        print("\n📜 滚动详情页...")
-        for i in range(3):
-            await browser_scroll_page(down=True, pages=1.5)
-            await browser_wait(2)
-
-        # 步骤3: 保存详情页HTML
-        print("\n💾 保存详情页 HTML...")
-        html_result = await browser_get_page_html()
-        if html_result.error:
-            print(f"⚠️  获取HTML失败: {html_result.error}")
-        else:
-            html = html_result.metadata.get("html", "")
-            html_path = output_dir / "container_post_detail.html"
-            html_path.write_text(html or "", encoding="utf-8")
-            print(f"✅ 已保存详情页 HTML: {html_path}")
-
-        # 查找并保存iframe
-        print("\n🔍 查找页面中的iframe...")
-        iframe_js = """
-        (function(){
-            const iframes = document.querySelectorAll('iframe');
-            const results = [];
-            iframes.forEach((iframe, index) => {
-                results.push({
-                    index: index,
-                    src: iframe.src || '',
-                    id: iframe.id || '',
-                    name: iframe.name || ''
-                });
-            });
-            return results;
-        })()
-        """
-
-        iframe_result = await browser_evaluate(iframe_js)
-        if not iframe_result.error:
-            iframe_output = iframe_result.output
-            if isinstance(iframe_output, str) and iframe_output.startswith("Result: "):
-                iframe_output = iframe_output[8:]
-
-            try:
-                iframes = json.loads(iframe_output) if isinstance(iframe_output, str) else iframe_output
-
-                if iframes and len(iframes) > 0:
-                    print(f"✅ 找到 {len(iframes)} 个iframe")
-
-                    for idx, iframe_info in enumerate(iframes):
-                        print(f"\n📄 处理iframe {idx + 1}/{len(iframes)}")
-                        print(f"   src: {iframe_info.get('src', 'N/A')[:80]}")
-
-                        # 获取iframe HTML
-                        get_iframe_html_js = f"""
-                        (function(){{
-                            const iframe = document.querySelectorAll('iframe')[{idx}];
-                            if (!iframe) return null;
-                            try {{
-                                const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
-                                return iframeDoc.documentElement.outerHTML;
-                            }} catch(e) {{
-                                return 'Error: ' + e.message;
-                            }}
-                        }})()
-                        """
-
-                        iframe_html_result = await browser_evaluate(get_iframe_html_js)
-                        if not iframe_html_result.error:
-                            iframe_html = iframe_html_result.output
-                            if isinstance(iframe_html, str) and iframe_html.startswith("Result: "):
-                                iframe_html = iframe_html[8:]
-
-                            if iframe_html and not iframe_html.startswith("Error:"):
-                                iframe_path = output_dir / f"container_iframe_{idx}.html"
-                                iframe_path.write_text(iframe_html, encoding="utf-8")
-                                print(f"   ✅ 已保存iframe HTML: {iframe_path}")
-                            else:
-                                print(f"   ⚠️  iframe内容为空或无法访问")
-                else:
-                    print("⚠️  页面中没有找到iframe")
-            except Exception as e:
-                print(f"⚠️  处理iframe失败: {str(e)}")
-
-        print("\n✅ 测试完成!")
-
-    except Exception as e:
-        print(f"\n❌ 发生错误: {str(e)}")
-        import traceback
-        traceback.print_exc()
-
-    finally:
-        # 清理浏览器会话
-        try:
-            await cleanup_browser_session()
-        except Exception:
-            pass
-
-
-async def main():
-    await test_xhs_container()
-
-
-if __name__ == "__main__":
-    asyncio.run(main())