Browse Source

feat:完整实现了Subagent

elksmmx 4 weeks ago
parent
commit
9f7e53d894

+ 0 - 3
.env.template

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

+ 154 - 39
agent/core/runner.py

@@ -17,7 +17,6 @@ from typing import AsyncIterator, Optional, Dict, Any, List, Callable, Literal,
 from agent.trace.models import Trace, Message
 from agent.trace.models import Trace, Message
 from agent.trace.protocols import TraceStore
 from agent.trace.protocols import TraceStore
 from agent.trace.goal_models import GoalTree
 from agent.trace.goal_models import GoalTree
-from agent.trace.goal_tool import goal_tool
 from agent.memory.models import Experience, Skill
 from agent.memory.models import Experience, Skill
 from agent.memory.protocols import MemoryStore, StateStore
 from agent.memory.protocols import MemoryStore, StateStore
 from agent.memory.skill_loader import load_skills_from_dir
 from agent.memory.skill_loader import load_skills_from_dir
@@ -62,6 +61,7 @@ BUILTIN_TOOLS = [
     "skill",
     "skill",
     "list_skills",
     "list_skills",
     "goal",
     "goal",
+    "subagent",
 
 
     # 搜索工具
     # 搜索工具
     "search_posts",
     "search_posts",
@@ -186,7 +186,6 @@ class AgentRunner:
             for tool in tools:
             for tool in tools:
                 if tool not in tool_names:
                 if tool not in tool_names:
                     tool_names.append(tool)
                     tool_names.append(tool)
-
         tool_schemas = self.tools.get_schemas(tool_names)
         tool_schemas = self.tools.get_schemas(tool_names)
 
 
         # 创建 Trace
         # 创建 Trace
@@ -244,6 +243,78 @@ class AgentRunner:
 
 
     # ===== Agent 模式 =====
     # ===== Agent 模式 =====
 
 
+    async def run_result(
+        self,
+        task: str,
+        messages: Optional[List[Dict]] = None,
+        system_prompt: Optional[str] = None,
+        model: str = "gpt-4o",
+        tools: Optional[List[str]] = None,
+        agent_type: Optional[str] = None,
+        uid: Optional[str] = None,
+        max_iterations: Optional[int] = None,
+        enable_memory: Optional[bool] = None,
+        auto_execute_tools: Optional[bool] = None,
+        trace_id: Optional[str] = None,
+        **kwargs
+    ) -> Dict[str, Any]:
+        """
+        Agent 结果模式执行。
+
+        消费 run() 的流式事件,返回结构化结果(最后一条有文本的 assistant + trace 统计)。
+        """
+        last_assistant_text = ""
+        final_trace: Optional[Trace] = None
+
+        async for item in self.run(
+            task=task,
+            messages=messages,
+            system_prompt=system_prompt,
+            model=model,
+            tools=tools,
+            agent_type=agent_type,
+            uid=uid,
+            max_iterations=max_iterations,
+            enable_memory=enable_memory,
+            auto_execute_tools=auto_execute_tools,
+            trace_id=trace_id,
+            **kwargs
+        ):
+            if isinstance(item, Message) and item.role == "assistant":
+                content = item.content
+                text = ""
+                if isinstance(content, dict):
+                    text = content.get("text", "") or ""
+                elif isinstance(content, str):
+                    text = content
+                if text and text.strip():
+                    last_assistant_text = text
+            elif isinstance(item, Trace):
+                final_trace = item
+
+        if not final_trace and trace_id and self.trace_store:
+            final_trace = await self.trace_store.get_trace(trace_id)
+
+        status = final_trace.status if final_trace else "unknown"
+        error = final_trace.error_message if final_trace else None
+        summary = last_assistant_text
+
+        if not summary:
+            status = "failed"
+            error = error or "Sub-Agent 没有产生 assistant 文本结果"
+
+        return {
+            "status": status,
+            "summary": summary,
+            "trace_id": final_trace.trace_id if final_trace else trace_id,
+            "error": error,
+            "stats": {
+                "total_messages": final_trace.total_messages if final_trace else 0,
+                "total_tokens": final_trace.total_tokens if final_trace else 0,
+                "total_cost": final_trace.total_cost if final_trace else 0.0,
+            },
+        }
+
     async def run(
     async def run(
         self,
         self,
         task: str,
         task: str,
@@ -256,6 +327,7 @@ class AgentRunner:
         max_iterations: Optional[int] = None,
         max_iterations: Optional[int] = None,
         enable_memory: Optional[bool] = None,
         enable_memory: Optional[bool] = None,
         auto_execute_tools: Optional[bool] = None,
         auto_execute_tools: Optional[bool] = None,
+        trace_id: Optional[str] = None,
         **kwargs
         **kwargs
     ) -> AsyncIterator[Union[Trace, Message]]:
     ) -> AsyncIterator[Union[Trace, Message]]:
         """
         """
@@ -272,6 +344,7 @@ class AgentRunner:
             max_iterations: 最大迭代次数
             max_iterations: 最大迭代次数
             enable_memory: 是否启用记忆
             enable_memory: 是否启用记忆
             auto_execute_tools: 是否自动执行工具
             auto_execute_tools: 是否自动执行工具
+            trace_id: Trace ID(可选,传入时复用已有 Trace)
             **kwargs: 其他参数
             **kwargs: 其他参数
 
 
         Yields:
         Yields:
@@ -294,26 +367,44 @@ class AgentRunner:
                     tool_names.append(tool)
                     tool_names.append(tool)
         tool_schemas = self.tools.get_schemas(tool_names)
         tool_schemas = self.tools.get_schemas(tool_names)
 
 
-        # 创建 Trace
-        trace_id = self._generate_id()
-        trace_obj = Trace(
-            trace_id=trace_id,
-            mode="agent",
-            task=task,
-            agent_type=agent_type,
-            uid=uid,
-            model=model,
-            tools=tool_schemas,  # 保存工具定义
-            llm_params=kwargs,  # 保存 LLM 参数
-            status="running"
-        )
+        # 创建或复用 Trace
+        if trace_id:
+            if self.trace_store:
+                trace_obj = await self.trace_store.get_trace(trace_id)
+                if not trace_obj:
+                    raise ValueError(f"Trace not found: {trace_id}")
+            else:
+                trace_obj = Trace(
+                    trace_id=trace_id,
+                    mode="agent",
+                    task=task,
+                    agent_type=agent_type,
+                    uid=uid,
+                    model=model,
+                    tools=tool_schemas,
+                    llm_params=kwargs,
+                    status="running"
+                )
+        else:
+            trace_id = self._generate_id()
+            trace_obj = Trace(
+                trace_id=trace_id,
+                mode="agent",
+                task=task,
+                agent_type=agent_type,
+                uid=uid,
+                model=model,
+                tools=tool_schemas,  # 保存工具定义
+                llm_params=kwargs,  # 保存 LLM 参数
+                status="running"
+            )
 
 
-        if self.trace_store:
-            await self.trace_store.create_trace(trace_obj)
+            if self.trace_store:
+                await self.trace_store.create_trace(trace_obj)
 
 
-            # 初始化 GoalTree
-            goal_tree = self.goal_tree or GoalTree(mission=task)
-            await self.trace_store.update_goal_tree(trace_id, goal_tree)
+                # 初始化 GoalTree
+                goal_tree = self.goal_tree or GoalTree(mission=task)
+                await self.trace_store.update_goal_tree(trace_id, goal_tree)
 
 
         # 返回 Trace(表示开始)
         # 返回 Trace(表示开始)
         yield trace_obj
         yield trace_obj
@@ -339,13 +430,34 @@ class AgentRunner:
                     logger.info(f"加载 {len(skills)} 个内置 skills")
                     logger.info(f"加载 {len(skills)} 个内置 skills")
 
 
             # 构建初始消息
             # 构建初始消息
+            sequence = 1
             if messages is None:
             if messages is None:
-                messages = []
-
+                if trace_id and self.trace_store:
+                    existing_messages = await self.trace_store.get_trace_messages(trace_id)
+                    messages = []
+                    for msg in existing_messages:
+                        msg_dict = {"role": msg.role}
+                        if isinstance(msg.content, dict):
+                            if msg.content.get("text"):
+                                msg_dict["content"] = msg.content["text"]
+                            if msg.content.get("tool_calls"):
+                                msg_dict["tool_calls"] = msg.content["tool_calls"]
+                        else:
+                            msg_dict["content"] = msg.content
+
+                        if msg.role == "tool" and msg.tool_call_id:
+                            msg_dict["tool_call_id"] = msg.tool_call_id
+                            msg_dict["name"] = msg.description or "unknown"
+
+                        messages.append(msg_dict)
+
+                    if existing_messages:
+                        sequence = existing_messages[-1].sequence + 1
+                else:
+                    messages = []
             # 记录初始 system 和 user 消息到 trace
             # 记录初始 system 和 user 消息到 trace
-            sequence = 1
 
 
-            if system_prompt:
+            if system_prompt and not any(m.get("role") == "system" for m in messages):
                 # 注入记忆和 skills 到 system prompt
                 # 注入记忆和 skills 到 system prompt
                 full_system = system_prompt
                 full_system = system_prompt
                 if skills_text:
                 if skills_text:
@@ -368,21 +480,22 @@ class AgentRunner:
                     yield system_msg
                     yield system_msg
                     sequence += 1
                     sequence += 1
 
 
-            # 添加任务描述
-            messages.append({"role": "user", "content": task})
+            # 添加任务描述(支持 continue_from 场景再次追加)
+            if task:
+                messages.append({"role": "user", "content": task})
 
 
-            # 保存 user 消息(任务描述)
-            if self.trace_store:
-                user_msg = Message.create(
-                    trace_id=trace_id,
-                    role="user",
-                    sequence=sequence,
-                    goal_id=None,  # 初始消息没有 goal
-                    content=task,
-                )
-                await self.trace_store.add_message(user_msg)
-                yield user_msg
-                sequence += 1
+                # 保存 user 消息(任务描述)
+                if self.trace_store:
+                    user_msg = Message.create(
+                        trace_id=trace_id,
+                        role="user",
+                        sequence=sequence,
+                        goal_id=None,  # 初始消息没有 goal
+                        content=task,
+                    )
+                    await self.trace_store.add_message(user_msg)
+                    yield user_msg
+                    sequence += 1
 
 
             # 获取 GoalTree
             # 获取 GoalTree
             goal_tree = None
             goal_tree = None
@@ -471,7 +584,9 @@ class AgentRunner:
                             uid=uid or "",
                             uid=uid or "",
                             context={
                             context={
                                 "store": self.trace_store,
                                 "store": self.trace_store,
-                                "trace_id": trace_id
+                                "trace_id": trace_id,
+                                "goal_id": current_goal_id,
+                                "runner": self,
                             }
                             }
                         )
                         )
 
 

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

@@ -17,6 +17,7 @@ from agent.tools.builtin.file.grep import grep_content
 # 系统工具
 # 系统工具
 from agent.tools.builtin.bash import bash_command
 from agent.tools.builtin.bash import bash_command
 from agent.tools.builtin.skill import skill, list_skills
 from agent.tools.builtin.skill import skill, list_skills
+from agent.tools.builtin.subagent import subagent
 from agent.tools.builtin.search import search_posts, get_search_suggestions
 from agent.tools.builtin.search import search_posts, get_search_suggestions
 from agent.tools.builtin.sandbox import (sandbox_create_environment, sandbox_run_shell,
 from agent.tools.builtin.sandbox import (sandbox_create_environment, sandbox_run_shell,
                                          sandbox_rebuild_with_ports,sandbox_destroy_environment)
                                          sandbox_rebuild_with_ports,sandbox_destroy_environment)
@@ -35,6 +36,7 @@ __all__ = [
     "bash_command",
     "bash_command",
     "skill",
     "skill",
     "list_skills",
     "list_skills",
+    "subagent",
     "search_posts",
     "search_posts",
     "get_search_suggestions",
     "get_search_suggestions",
     "sandbox_create_environment",
     "sandbox_create_environment",

+ 606 - 0
agent/tools/builtin/subagent.py

@@ -0,0 +1,606 @@
+"""
+Sub-Agent 工具 - 统一 explore/delegate/evaluate
+
+作为普通工具运行:创建(或继承)子 Trace,执行并返回结构化结果。
+"""
+
+import asyncio
+from datetime import datetime
+from typing import Any, Dict, List, Optional
+
+from agent.tools import tool
+from agent.trace.models import Trace
+from agent.trace.trace_id import generate_sub_trace_id
+from agent.trace.goal_models import GoalTree
+from agent.trace.websocket import broadcast_sub_trace_started, broadcast_sub_trace_completed
+
+
+def _build_explore_prompt(branches: List[str], background: Optional[str]) -> str:
+    lines = ["# 探索任务", ""]
+    if background:
+        lines.extend([background, ""])
+    lines.append("请探索以下方案:")
+    for i, branch in enumerate(branches, 1):
+        lines.append(f"{i}. {branch}")
+    return "\n".join(lines)
+
+
+async def _build_evaluate_prompt(
+    store,
+    trace_id: str,
+    target_goal_id: str,
+    evaluation_input: Dict[str, Any],
+    requirements: Optional[str],
+) -> str:
+    goal_tree = await store.get_goal_tree(trace_id)
+    target_desc = ""
+    if goal_tree:
+        target_goal = goal_tree.find(target_goal_id)
+        if target_goal:
+            target_desc = target_goal.description
+
+    goal_description = evaluation_input.get("goal_description") or target_desc or f"Goal {target_goal_id}"
+    actual_result = evaluation_input.get("actual_result", "(无执行结果)")
+
+    lines = [
+        "# 评估任务",
+        "",
+        "请评估以下任务的执行结果是否满足要求。",
+        "",
+        "## 目标描述",
+        "",
+        str(goal_description),
+        "",
+        "## 执行结果",
+        "",
+        str(actual_result),
+        "",
+    ]
+
+    if requirements:
+        lines.extend(["## 评估要求", "", requirements, ""])
+
+    lines.extend(
+        [
+            "## 输出格式",
+            "",
+            "## 评估结论",
+            "[通过/不通过]",
+            "",
+            "## 评估理由",
+            "[详细说明通过或不通过原因]",
+            "",
+            "## 修改建议(如果不通过)",
+            "1. [建议1]",
+            "2. [建议2]",
+        ]
+    )
+    return "\n".join(lines)
+
+
+# ===== 辅助函数 =====
+
+async def _update_goal_start(
+    store, trace_id: str, goal_id: str, mode: str, sub_trace_ids: List[str]
+) -> None:
+    """标记 Goal 开始执行"""
+    if not goal_id:
+        return
+    await store.update_goal(
+        trace_id, goal_id,
+        type="agent_call",
+        agent_call_mode=mode,
+        status="in_progress",
+        sub_trace_ids=sub_trace_ids
+    )
+
+
+async def _update_goal_complete(
+    store, trace_id: str, goal_id: str,
+    status: str, summary: str, sub_trace_ids: List[str]
+) -> None:
+    """标记 Goal 完成"""
+    if not goal_id:
+        return
+    await store.update_goal(
+        trace_id, goal_id,
+        status=status,
+        summary=summary,
+        sub_trace_ids=sub_trace_ids
+    )
+
+
+def _format_explore_results(
+    branches: List[str], results: List[Dict[str, Any]]
+) -> str:
+    """格式化 explore 模式的汇总结果(Markdown)"""
+    lines = ["## 探索结果\n"]
+
+    successful = 0
+    failed = 0
+    total_tokens = 0
+    total_cost = 0.0
+
+    for i, (branch, result) in enumerate(zip(branches, results)):
+        branch_name = chr(ord('A') + i)  # A, B, C...
+        lines.append(f"### 方案 {branch_name}: {branch}")
+
+        if isinstance(result, dict):
+            status = result.get("status", "unknown")
+            if status == "completed":
+                lines.append("**状态**: ✓ 完成")
+                successful += 1
+            else:
+                lines.append("**状态**: ✗ 失败")
+                failed += 1
+
+            summary = result.get("summary", "")
+            if summary:
+                lines.append(f"**摘要**: {summary[:200]}...")  # 限制长度
+
+            stats = result.get("stats", {})
+            if stats:
+                messages = stats.get("total_messages", 0)
+                tokens = stats.get("total_tokens", 0)
+                cost = stats.get("total_cost", 0.0)
+                lines.append(f"**统计**: {messages} messages, {tokens} tokens, ${cost:.4f}")
+                total_tokens += tokens
+                total_cost += cost
+        else:
+            lines.append("**状态**: ✗ 异常")
+            failed += 1
+
+        lines.append("")
+
+    lines.append("---\n")
+    lines.append("## 总结")
+    lines.append(f"- 总分支数: {len(branches)}")
+    lines.append(f"- 成功: {successful}")
+    lines.append(f"- 失败: {failed}")
+    lines.append(f"- 总 tokens: {total_tokens}")
+    lines.append(f"- 总成本: ${total_cost:.4f}")
+
+    return "\n".join(lines)
+
+
+def _format_delegate_result(result: Dict[str, Any]) -> str:
+    """格式化 delegate 模式的详细结果"""
+    lines = ["## 委托任务完成\n"]
+
+    summary = result.get("summary", "")
+    if summary:
+        lines.append(summary)
+        lines.append("")
+
+    lines.append("---\n")
+    lines.append("**执行统计**:")
+
+    stats = result.get("stats", {})
+    if stats:
+        lines.append(f"- 消息数: {stats.get('total_messages', 0)}")
+        lines.append(f"- Tokens: {stats.get('total_tokens', 0)}")
+        lines.append(f"- 成本: ${stats.get('total_cost', 0.0):.4f}")
+
+    return "\n".join(lines)
+
+
+def _format_evaluate_result(result: Dict[str, Any]) -> str:
+    """格式化 evaluate 模式的评估结果"""
+    summary = result.get("summary", "")
+    return summary  # evaluate 的 summary 已经是格式化的评估结果
+
+
+def _get_allowed_tools_for_mode(mode: str, context: dict) -> Optional[List[str]]:
+    """获取模式对应的允许工具列表"""
+    if mode == "explore":
+        return ["read_file", "grep_content", "glob_files"]
+    elif mode in ["delegate", "evaluate"]:
+        # 获取所有工具,排除 subagent
+        runner = context.get("runner")
+        if runner and hasattr(runner, "tools") and hasattr(runner.tools, "registry"):
+            all_tools = list(runner.tools.registry.keys())
+            return [t for t in all_tools if t != "subagent"]
+    return None  # 使用默认(所有工具)
+
+
+def _aggregate_stats(results: List[Dict[str, Any]]) -> Dict[str, Any]:
+    """聚合多个结果的统计信息"""
+    total_messages = 0
+    total_tokens = 0
+    total_cost = 0.0
+
+    for result in results:
+        if isinstance(result, dict) and "stats" in result:
+            stats = result["stats"]
+            total_messages += stats.get("total_messages", 0)
+            total_tokens += stats.get("total_tokens", 0)
+            total_cost += stats.get("total_cost", 0.0)
+
+    return {
+        "total_messages": total_messages,
+        "total_tokens": total_tokens,
+        "total_cost": total_cost
+    }
+
+
+# ===== 模式处理函数 =====
+
+async def _handle_explore_mode(
+    branches: List[str],
+    background: Optional[str],
+    continue_from: Optional[str],
+    store, current_trace_id: str, current_goal_id: str, runner
+) -> Dict[str, Any]:
+    """Explore 模式:并行探索多个方案"""
+
+    # 1. 检查 continue_from(不支持)
+    if continue_from:
+        return {
+            "status": "failed",
+            "error": "explore mode does not support continue_from parameter"
+        }
+
+    # 2. 创建所有 Sub-Traces
+    sub_trace_ids = []
+    tasks = []
+
+    for i, branch in enumerate(branches):
+        # 生成唯一的 sub_trace_id
+        sub_trace_id = generate_sub_trace_id(current_trace_id, f"explore-{i+1:03d}")
+        sub_trace_ids.append(sub_trace_id)
+
+        # 创建 Sub-Trace
+        parent_trace = await store.get_trace(current_trace_id)
+        sub_trace = Trace(
+            trace_id=sub_trace_id,
+            mode="agent",
+            task=branch,
+            parent_trace_id=current_trace_id,
+            parent_goal_id=current_goal_id,
+            agent_type="explore",
+            uid=parent_trace.uid if parent_trace else None,
+            model=parent_trace.model if parent_trace else None,
+            status="running",
+            context={"subagent_mode": "explore", "created_by_tool": "subagent"},
+            created_at=datetime.now(),
+        )
+        await store.create_trace(sub_trace)
+        await store.update_goal_tree(sub_trace_id, GoalTree(mission=branch))
+
+        # 广播 sub_trace_started
+        await broadcast_sub_trace_started(
+            current_trace_id, sub_trace_id, current_goal_id or "",
+            "explore", branch
+        )
+
+        # 创建执行任务
+        task_coro = runner.run_result(
+            task=branch,
+            trace_id=sub_trace_id,
+            agent_type="explore",
+            tools=["read_file", "grep_content", "glob_files"]
+        )
+        tasks.append(task_coro)
+
+    # 3. 更新主 Goal 为 in_progress
+    await _update_goal_start(store, current_trace_id, current_goal_id, "explore", sub_trace_ids)
+
+    # 4. 并行执行所有分支
+    results = await asyncio.gather(*tasks, return_exceptions=True)
+
+    # 5. 处理结果并广播完成事件
+    processed_results = []
+
+    for i, result in enumerate(results):
+        if isinstance(result, Exception):
+            # 异常处理
+            error_result = {
+                "status": "failed",
+                "summary": f"执行出错: {str(result)}",
+                "stats": {"total_messages": 0, "total_tokens": 0, "total_cost": 0.0}
+            }
+            processed_results.append(error_result)
+            await broadcast_sub_trace_completed(
+                current_trace_id, sub_trace_ids[i],
+                "failed", str(result), {}
+            )
+        else:
+            processed_results.append(result)
+            await broadcast_sub_trace_completed(
+                current_trace_id, sub_trace_ids[i],
+                result.get("status", "completed"),
+                result.get("summary", ""),
+                result.get("stats", {})
+            )
+
+    # 6. 格式化汇总结果
+    aggregated_summary = _format_explore_results(branches, processed_results)
+
+    # 7. 更新主 Goal 为 completed
+    overall_status = "completed" if any(
+        r.get("status") == "completed" for r in processed_results if isinstance(r, dict)
+    ) else "failed"
+
+    await _update_goal_complete(
+        store, current_trace_id, current_goal_id,
+        overall_status, aggregated_summary, sub_trace_ids
+    )
+
+    # 8. 返回结果
+    return {
+        "mode": "explore",
+        "status": overall_status,
+        "summary": aggregated_summary,
+        "sub_trace_ids": sub_trace_ids,
+        "branches": branches,
+        "stats": _aggregate_stats(processed_results)
+    }
+
+
+async def _handle_delegate_mode(
+    task: str,
+    continue_from: Optional[str],
+    store, current_trace_id: str, current_goal_id: str, runner, context: dict
+) -> Dict[str, Any]:
+    """Delegate 模式:委托单个任务"""
+
+    # 1. 处理 continue_from 或创建新 Sub-Trace
+    if continue_from:
+        existing_trace = await store.get_trace(continue_from)
+        if not existing_trace:
+            return {"status": "failed", "error": f"Continue-from trace not found: {continue_from}"}
+        sub_trace_id = continue_from
+        sub_trace_ids = [sub_trace_id]
+    else:
+        parent_trace = await store.get_trace(current_trace_id)
+        sub_trace_id = generate_sub_trace_id(current_trace_id, "delegate")
+        sub_trace = Trace(
+            trace_id=sub_trace_id,
+            mode="agent",
+            task=task,
+            parent_trace_id=current_trace_id,
+            parent_goal_id=current_goal_id,
+            agent_type="delegate",
+            uid=parent_trace.uid if parent_trace else None,
+            model=parent_trace.model if parent_trace else None,
+            status="running",
+            context={"subagent_mode": "delegate", "created_by_tool": "subagent"},
+            created_at=datetime.now(),
+        )
+        await store.create_trace(sub_trace)
+        await store.update_goal_tree(sub_trace_id, GoalTree(mission=task))
+        sub_trace_ids = [sub_trace_id]
+
+        # 广播 sub_trace_started
+        await broadcast_sub_trace_started(
+            current_trace_id, sub_trace_id, current_goal_id or "",
+            "delegate", task
+        )
+
+    # 2. 更新主 Goal 为 in_progress
+    await _update_goal_start(store, current_trace_id, current_goal_id, "delegate", sub_trace_ids)
+
+    # 3. 执行任务
+    try:
+        allowed_tools = _get_allowed_tools_for_mode("delegate", context)
+        result = await runner.run_result(
+            task=task,
+            trace_id=sub_trace_id,
+            agent_type="delegate",
+            tools=allowed_tools
+        )
+
+        # 4. 广播 sub_trace_completed
+        await broadcast_sub_trace_completed(
+            current_trace_id, sub_trace_id,
+            result.get("status", "completed"),
+            result.get("summary", ""),
+            result.get("stats", {})
+        )
+
+        # 5. 格式化结果
+        formatted_summary = _format_delegate_result(result)
+
+        # 6. 更新主 Goal 为 completed
+        await _update_goal_complete(
+            store, current_trace_id, current_goal_id,
+            result.get("status", "completed"), formatted_summary, sub_trace_ids
+        )
+
+        # 7. 返回结果
+        return {
+            "mode": "delegate",
+            "sub_trace_id": sub_trace_id,
+            "continue_from": bool(continue_from),
+            **result,
+            "summary": formatted_summary
+        }
+
+    except Exception as e:
+        # 错误处理
+        error_msg = str(e)
+        await broadcast_sub_trace_completed(
+            current_trace_id, sub_trace_id,
+            "failed", error_msg, {}
+        )
+
+        await _update_goal_complete(
+            store, current_trace_id, current_goal_id,
+            "failed", f"委托任务失败: {error_msg}", sub_trace_ids
+        )
+
+        return {
+            "mode": "delegate",
+            "status": "failed",
+            "error": error_msg,
+            "sub_trace_id": sub_trace_id
+        }
+
+
+async def _handle_evaluate_mode(
+    target_goal_id: str,
+    evaluation_input: Dict[str, Any],
+    requirements: Optional[str],
+    continue_from: Optional[str],
+    store, current_trace_id: str, current_goal_id: str, runner, context: dict
+) -> Dict[str, Any]:
+    """Evaluate 模式:评估任务结果"""
+
+    # 1. 构建评估 prompt
+    task_prompt = await _build_evaluate_prompt(
+        store, current_trace_id, target_goal_id,
+        evaluation_input, requirements
+    )
+
+    # 2. 处理 continue_from 或创建新 Sub-Trace
+    if continue_from:
+        existing_trace = await store.get_trace(continue_from)
+        if not existing_trace:
+            return {"status": "failed", "error": f"Continue-from trace not found: {continue_from}"}
+        sub_trace_id = continue_from
+        sub_trace_ids = [sub_trace_id]
+    else:
+        parent_trace = await store.get_trace(current_trace_id)
+        sub_trace_id = generate_sub_trace_id(current_trace_id, "evaluate")
+        sub_trace = Trace(
+            trace_id=sub_trace_id,
+            mode="agent",
+            task=task_prompt,
+            parent_trace_id=current_trace_id,
+            parent_goal_id=current_goal_id,
+            agent_type="evaluate",
+            uid=parent_trace.uid if parent_trace else None,
+            model=parent_trace.model if parent_trace else None,
+            status="running",
+            context={"subagent_mode": "evaluate", "created_by_tool": "subagent"},
+            created_at=datetime.now(),
+        )
+        await store.create_trace(sub_trace)
+        await store.update_goal_tree(sub_trace_id, GoalTree(mission=task_prompt))
+        sub_trace_ids = [sub_trace_id]
+
+        # 广播 sub_trace_started
+        await broadcast_sub_trace_started(
+            current_trace_id, sub_trace_id, current_goal_id or "",
+            "evaluate", task_prompt
+        )
+
+    # 3. 更新主 Goal 为 in_progress
+    await _update_goal_start(store, current_trace_id, current_goal_id, "evaluate", sub_trace_ids)
+
+    # 4. 执行评估
+    try:
+        allowed_tools = _get_allowed_tools_for_mode("evaluate", context)
+        result = await runner.run_result(
+            task=task_prompt,
+            trace_id=sub_trace_id,
+            agent_type="evaluate",
+            tools=allowed_tools
+        )
+
+        # 5. 广播 sub_trace_completed
+        await broadcast_sub_trace_completed(
+            current_trace_id, sub_trace_id,
+            result.get("status", "completed"),
+            result.get("summary", ""),
+            result.get("stats", {})
+        )
+
+        # 6. 格式化结果
+        formatted_summary = _format_evaluate_result(result)
+
+        # 7. 更新主 Goal 为 completed
+        await _update_goal_complete(
+            store, current_trace_id, current_goal_id,
+            result.get("status", "completed"), formatted_summary, sub_trace_ids
+        )
+
+        # 8. 返回结果
+        return {
+            "mode": "evaluate",
+            "sub_trace_id": sub_trace_id,
+            "continue_from": bool(continue_from),
+            **result,
+            "summary": formatted_summary
+        }
+
+    except Exception as e:
+        # 错误处理
+        error_msg = str(e)
+        await broadcast_sub_trace_completed(
+            current_trace_id, sub_trace_id,
+            "failed", error_msg, {}
+        )
+
+        await _update_goal_complete(
+            store, current_trace_id, current_goal_id,
+            "failed", f"评估任务失败: {error_msg}", sub_trace_ids
+        )
+
+        return {
+            "mode": "evaluate",
+            "status": "failed",
+            "error": error_msg,
+            "sub_trace_id": sub_trace_id
+        }
+
+
+@tool(description="创建 Sub-Agent 执行任务(evaluate/delegate/explore)")
+async def subagent(
+    mode: str,
+    task: Optional[str] = None,
+    target_goal_id: Optional[str] = None,
+    evaluation_input: Optional[Dict[str, Any]] = None,
+    requirements: Optional[str] = None,
+    branches: Optional[List[str]] = None,
+    background: Optional[str] = None,
+    continue_from: Optional[str] = None,
+    context: Optional[dict] = None,
+) -> Dict[str, Any]:
+    # 1. 验证 context
+    if not context:
+        return {"status": "failed", "error": "context is required"}
+
+    store = context.get("store")
+    current_trace_id = context.get("trace_id")
+    current_goal_id = context.get("goal_id")
+    runner = context.get("runner")
+
+    missing = []
+    if not store:
+        missing.append("store")
+    if not current_trace_id:
+        missing.append("trace_id")
+    if not runner:
+        missing.append("runner")
+    if missing:
+        return {"status": "failed", "error": f"Missing required context: {', '.join(missing)}"}
+
+    # 2. 验证 mode
+    if mode not in {"evaluate", "delegate", "explore"}:
+        return {"status": "failed", "error": "Invalid mode: must be evaluate/delegate/explore"}
+
+    # 3. 验证模式特定参数
+    if mode == "delegate" and not task:
+        return {"status": "failed", "error": "delegate mode requires task"}
+    if mode == "explore" and not branches:
+        return {"status": "failed", "error": "explore mode requires branches"}
+    if mode == "evaluate" and (not target_goal_id or evaluation_input is None):
+        return {"status": "failed", "error": "evaluate mode requires target_goal_id and evaluation_input"}
+
+    # 4. 路由到模式处理函数
+    if mode == "explore":
+        return await _handle_explore_mode(
+            branches, background, continue_from,
+            store, current_trace_id, current_goal_id, runner
+        )
+    elif mode == "delegate":
+        return await _handle_delegate_mode(
+            task, continue_from,
+            store, current_trace_id, current_goal_id, runner, context
+        )
+    else:  # evaluate
+        return await _handle_evaluate_mode(
+            target_goal_id, evaluation_input, requirements, continue_from,
+            store, current_trace_id, current_goal_id, runner, context
+        )

+ 0 - 176
agent/trace/delegate.py

@@ -1,176 +0,0 @@
-"""
-Delegate 工具 - 委托任务给子 Agent
-
-将大任务委托给独立的 Sub-Trace 执行,获得完整权限。
-"""
-
-from typing import Optional, Dict, Any
-from datetime import datetime
-
-from .models import Trace, Message
-from .trace_id import generate_sub_trace_id
-from .goal_models import Goal
-
-
-async def delegate_tool(
-    current_trace_id: str,
-    current_goal_id: str,
-    task: str,
-    store=None,
-    run_agent=None
-) -> str:
-    """
-    将任务委托给独立的 Sub-Agent
-
-    Args:
-        current_trace_id: 当前主 Trace ID
-        current_goal_id: 当前 Goal ID
-        task: 委托的任务描述
-        store: TraceStore 实例
-        run_agent: 运行 Agent 的函数
-
-    Returns:
-        任务执行结果摘要
-
-    Example:
-        >>> result = await delegate_tool(
-        ...     current_trace_id="abc123",
-        ...     current_goal_id="3",
-        ...     task="实现用户登录功能",
-        ...     store=store,
-        ...     run_agent=run_agent_func
-        ... )
-    """
-    if not store:
-        raise ValueError("store parameter is required")
-    if not run_agent:
-        raise ValueError("run_agent parameter is required")
-
-    # 1. 创建 agent_call Goal
-    await store.update_goal(current_trace_id, current_goal_id,
-                           type="agent_call",
-                           agent_call_mode="delegate",
-                           status="in_progress")
-
-    # 2. 生成 Sub-Trace ID
-    sub_trace_id = generate_sub_trace_id(current_trace_id, "delegate")
-
-    # 3. 创建 Sub-Trace
-    sub_trace = Trace(
-        trace_id=sub_trace_id,
-        mode="agent",
-        task=task,
-        parent_trace_id=current_trace_id,
-        parent_goal_id=current_goal_id,
-        agent_type="delegate",
-        context={
-            # delegate 模式:完整权限
-            "allowed_tools": None,  # None = 所有工具
-            "max_turns": 50
-        },
-        status="running",
-        created_at=datetime.now()
-    )
-
-    # 保存 Sub-Trace
-    await store.create_trace(sub_trace)
-
-    # 更新主 Goal 的 sub_trace_ids
-    await store.update_goal(current_trace_id, current_goal_id, sub_trace_ids=[sub_trace_id])
-
-    # 推送 sub_trace_started 事件
-    await store.append_event(current_trace_id, "sub_trace_started", {
-        "trace_id": sub_trace_id,
-        "parent_trace_id": current_trace_id,
-        "parent_goal_id": current_goal_id,
-        "agent_type": "delegate",
-        "task": task
-    })
-
-    # 4. 执行 Sub-Trace
-    try:
-        result = await run_agent(sub_trace)
-
-        # 获取 Sub-Trace 的最终状态
-        updated_trace = await store.get_trace(sub_trace_id)
-
-        if isinstance(result, dict):
-            summary = result.get("summary", "任务完成")
-        else:
-            summary = "任务完成"
-
-        # 推送 sub_trace_completed 事件
-        await store.append_event(current_trace_id, "sub_trace_completed", {
-            "trace_id": sub_trace_id,
-            "status": "completed",
-            "summary": summary,
-            "stats": {
-                "total_messages": updated_trace.total_messages if updated_trace else 0,
-                "total_tokens": updated_trace.total_tokens if updated_trace else 0,
-                "total_cost": updated_trace.total_cost if updated_trace else 0
-            }
-        })
-
-        # 5. 完成主 Goal
-        await store.update_goal(current_trace_id, current_goal_id,
-                               status="completed",
-                               summary=f"已委托完成: {task}")
-
-        # 格式化返回结果
-        return f"""## 委托任务完成
-
-**任务**: {task}
-
-**结果**: {summary}
-
-**统计**:
-- 消息数: {updated_trace.total_messages if updated_trace else 0}
-- Tokens: {updated_trace.total_tokens if updated_trace else 0}
-- 成本: ${updated_trace.total_cost if updated_trace else 0:.4f}
-"""
-
-    except Exception as e:
-        # 推送失败事件
-        await store.append_event(current_trace_id, "sub_trace_completed", {
-            "trace_id": sub_trace_id,
-            "status": "failed",
-            "error": str(e)
-        })
-
-        # 更新主 Goal 为失败
-        await store.update_goal(current_trace_id, current_goal_id,
-                               status="failed",
-                               summary=f"委托任务失败: {str(e)}")
-
-        return f"""## 委托任务失败
-
-**任务**: {task}
-
-**错误**: {str(e)}
-"""
-
-
-def create_delegate_tool_schema() -> Dict[str, Any]:
-    """
-    创建 delegate 工具的 JSON Schema
-
-    Returns:
-        工具的 JSON Schema
-    """
-    return {
-        "type": "function",
-        "function": {
-            "name": "delegate",
-            "description": "将大任务委托给独立的 Sub-Agent 执行。Sub-Agent 拥有完整权限,适合执行复杂的、需要多步骤的任务。",
-            "parameters": {
-                "type": "object",
-                "properties": {
-                    "task": {
-                        "type": "string",
-                        "description": "要委托的任务描述,应该清晰具体"
-                    }
-                },
-                "required": ["task"]
-            }
-        }
-    }

+ 0 - 248
agent/trace/explore.py

@@ -1,248 +0,0 @@
-"""
-Explore 工具 - 并行探索多个方案
-
-启动多个 Sub-Trace 并行执行不同的探索方向,汇总结果返回。
-"""
-
-import asyncio
-from typing import List, Optional, Dict, Any
-from datetime import datetime
-
-from .models import Trace, Message
-from .trace_id import generate_sub_trace_id
-from .goal_models import Goal
-
-
-async def explore_tool(
-    current_trace_id: str,
-    current_goal_id: str,
-    branches: List[str],
-    background: Optional[str] = None,
-    store=None,
-    run_agent=None
-) -> str:
-    """
-    并行探索多个方向,汇总结果
-
-    Args:
-        current_trace_id: 当前主 Trace ID
-        current_goal_id: 当前 Goal ID
-        branches: 探索方向列表(每个元素是一个探索任务描述)
-        background: 可选,背景信息(如果提供则用作各 Sub-Trace 的初始 context)
-        store: TraceStore 实例
-        run_agent: 运行 Agent 的函数
-
-    Returns:
-        汇总结果字符串
-
-    Example:
-        >>> result = await explore_tool(
-        ...     current_trace_id="abc123",
-        ...     current_goal_id="2",
-        ...     branches=["JWT 方案", "Session 方案"],
-        ...     store=store,
-        ...     run_agent=run_agent_func
-        ... )
-    """
-    if not store:
-        raise ValueError("store parameter is required")
-    if not run_agent:
-        raise ValueError("run_agent parameter is required")
-
-    # 1. 创建 agent_call Goal
-    goal = Goal(
-        id=current_goal_id,
-        type="agent_call",
-        description=f"并行探索 {len(branches)} 个方案",
-        reason="探索多个可行方案",
-        agent_call_mode="explore",
-        sub_trace_ids=[],
-        status="in_progress"
-    )
-
-    # 更新 Goal(标记为 agent_call)
-    await store.update_goal(current_trace_id, current_goal_id,
-                           type="agent_call",
-                           agent_call_mode="explore",
-                           status="in_progress")
-
-    # 2. 为每个分支创建 Sub-Trace
-    sub_traces = []
-    sub_trace_ids = []
-
-    for i, desc in enumerate(branches):
-        # 生成 Sub-Trace ID
-        sub_trace_id = generate_sub_trace_id(current_trace_id, "explore")
-
-        # 创建 Sub-Trace
-        sub_trace = Trace(
-            trace_id=sub_trace_id,
-            mode="agent",
-            task=desc,
-            parent_trace_id=current_trace_id,
-            parent_goal_id=current_goal_id,
-            agent_type="explore",
-            context={
-                "allowed_tools": ["read", "grep", "glob"],  # 探索模式:只读权限
-                "max_turns": 20,
-                "background": background
-            },
-            status="running",
-            created_at=datetime.now()
-        )
-
-        # 保存 Sub-Trace
-        await store.create_trace(sub_trace)
-
-        sub_traces.append(sub_trace)
-        sub_trace_ids.append(sub_trace_id)
-
-        # 推送 sub_trace_started 事件
-        await store.append_event(current_trace_id, "sub_trace_started", {
-            "trace_id": sub_trace_id,
-            "parent_trace_id": current_trace_id,
-            "parent_goal_id": current_goal_id,
-            "agent_type": "explore",
-            "task": desc
-        })
-
-    # 更新主 Goal 的 sub_trace_ids
-    await store.update_goal(current_trace_id, current_goal_id, sub_trace_ids=sub_trace_ids)
-
-    # 3. 并行执行所有 Sub-Traces
-    results = await asyncio.gather(
-        *[run_agent(st, background=background) for st in sub_traces],
-        return_exceptions=True
-    )
-
-    # 4. 收集元数据并汇总结果
-    sub_trace_metadata = {}
-    summary_parts = ["## 探索结果\n"]
-
-    for i, (sub_trace, result) in enumerate(zip(sub_traces, results), 1):
-        branch_name = chr(ord('A') + i - 1)  # A, B, C...
-
-        if isinstance(result, Exception):
-            # 处理异常情况
-            summary_parts.append(f"### 方案 {branch_name}: {sub_trace.task}")
-            summary_parts.append(f"⚠️ 执行出错: {str(result)}\n")
-
-            sub_trace_metadata[sub_trace.trace_id] = {
-                "task": sub_trace.task,
-                "status": "failed",
-                "summary": f"执行出错: {str(result)}",
-                "last_message": None,
-                "stats": {
-                    "message_count": 0,
-                    "total_tokens": 0,
-                    "total_cost": 0.0
-                }
-            }
-        else:
-            # 获取 Sub-Trace 的最终状态
-            updated_trace = await store.get_trace(sub_trace.trace_id)
-
-            # 获取最后一条 assistant 消息
-            messages = await store.get_trace_messages(sub_trace.trace_id)
-            last_message = None
-            for msg in reversed(messages):
-                if msg.role == "assistant":
-                    last_message = msg
-                    break
-
-            # 构建元数据
-            # 优先使用 result 中的 summary,否则使用最后一条消息的内容
-            summary_text = None
-            if isinstance(result, dict) and result.get("summary"):
-                summary_text = result.get("summary")
-            elif last_message and last_message.content:
-                # 使用最后一条消息的内容作为 summary(截断至 200 字符)
-                content_text = last_message.content
-                if isinstance(content_text, dict) and "text" in content_text:
-                    content_text = content_text["text"]
-                elif not isinstance(content_text, str):
-                    content_text = str(content_text)
-                summary_text = content_text[:200] if content_text else "执行完成"
-            else:
-                summary_text = "执行完成"
-
-            sub_trace_metadata[sub_trace.trace_id] = {
-                "task": sub_trace.task,
-                "status": updated_trace.status if updated_trace else "unknown",
-                "summary": summary_text,
-                "last_message": {
-                    "role": last_message.role,
-                    "description": last_message.description,
-                    "content": last_message.content[:500] if last_message.content else None,
-                    "created_at": last_message.created_at.isoformat()
-                } if last_message else None,
-                "stats": {
-                    "message_count": updated_trace.total_messages if updated_trace else 0,
-                    "total_tokens": updated_trace.total_tokens if updated_trace else 0,
-                    "total_cost": updated_trace.total_cost if updated_trace else 0.0
-                }
-            }
-
-            # 组装摘要文本
-            summary_parts.append(f"### 方案 {branch_name}: {sub_trace.task}")
-
-            if updated_trace and updated_trace.status == "completed":
-                summary_parts.append(f"{summary_text}\n")
-                summary_parts.append(f"📊 统计: {updated_trace.total_messages} 条消息, "
-                                   f"{updated_trace.total_tokens} tokens, "
-                                   f"成本 ${updated_trace.total_cost:.4f}\n")
-            else:
-                summary_parts.append(f"未完成\n")
-
-        # 推送 sub_trace_completed 事件
-        await store.append_event(current_trace_id, "sub_trace_completed", {
-            "trace_id": sub_trace.trace_id,
-            "status": "completed" if not isinstance(result, Exception) else "failed",
-            "summary": result.get("summary", "") if isinstance(result, dict) else ""
-        })
-
-    summary_parts.append("\n---")
-    summary_parts.append(f"已完成 {len(branches)} 个方案的探索,请根据结果选择继续的方向。")
-
-    summary = "\n".join(summary_parts)
-
-    # 5. 完成主 Goal,保存元数据
-    await store.update_goal(current_trace_id, current_goal_id,
-                           status="completed",
-                           summary=f"探索了 {len(branches)} 个方案",
-                           sub_trace_metadata=sub_trace_metadata)
-
-    return summary
-
-
-def create_explore_tool_schema() -> Dict[str, Any]:
-    """
-    创建 explore 工具的 JSON Schema
-
-    Returns:
-        工具的 JSON Schema
-    """
-    return {
-        "type": "function",
-        "function": {
-            "name": "explore",
-            "description": "并行探索多个方向,汇总结果。用于需要对比多个方案或尝试不同实现方式的场景。",
-            "parameters": {
-                "type": "object",
-                "properties": {
-                    "branches": {
-                        "type": "array",
-                        "items": {"type": "string"},
-                        "description": "探索方向列表,每个元素是一个探索任务的描述",
-                        "minItems": 2,
-                        "maxItems": 5
-                    },
-                    "background": {
-                        "type": "string",
-                        "description": "可选的背景信息,用于初始化各 Sub-Trace 的上下文"
-                    }
-                },
-                "required": ["branches"]
-            }
-        }
-    }

+ 61 - 47
docs/README.md

@@ -1,6 +1,5 @@
 # Agent 功能需求与架构设计文档
 # Agent 功能需求与架构设计文档
 
 
-
 ## 文档维护规范
 ## 文档维护规范
 
 
 0. **先改文档,再动代码** - 新功能或重大修改需先完成文档更新、并完成审阅后,再进行代码实现;除非改动较小、不被文档涵盖
 0. **先改文档,再动代码** - 新功能或重大修改需先完成文档更新、并完成审阅后,再进行代码实现;除非改动较小、不被文档涵盖
@@ -16,7 +15,7 @@
 | 类型 | 创建方式 | 父子关系 | 状态 |
 | 类型 | 创建方式 | 父子关系 | 状态 |
 |------|---------|---------|------|
 |------|---------|---------|------|
 | 主 Agent | 直接调用 `runner.run()` | 无 parent | 正常执行 |
 | 主 Agent | 直接调用 `runner.run()` | 无 parent | 正常执行 |
-| 子 Agent | 通过 `task` 工具 | `parent_trace_id` 指向父 | 正常执行 |
+| 子 Agent | 通过 `subagent` 工具 | `parent_trace_id` / `parent_goal_id` 指向父 | 正常执行 |
 | 人类协助 | 通过 `ask_human` 工具 | `parent_trace_id` 指向父 | 阻塞等待 |
 | 人类协助 | 通过 `ask_human` 工具 | `parent_trace_id` 指向父 | 阻塞等待 |
 
 
 ---
 ---
@@ -37,8 +36,6 @@ agent/
 │   ├── protocols.py       # TraceStore 接口
 │   ├── protocols.py       # TraceStore 接口
 │   ├── store.py           # FileSystemTraceStore 实现
 │   ├── store.py           # FileSystemTraceStore 实现
 │   ├── goal_tool.py       # goal 工具(计划管理)
 │   ├── goal_tool.py       # goal 工具(计划管理)
-│   ├── explore.py         # 探索式子任务
-│   ├── delegate.py        # 委派式子任务
 │   ├── compaction.py      # Context 压缩
 │   ├── compaction.py      # Context 压缩
 │   ├── api.py             # REST API
 │   ├── api.py             # REST API
 │   ├── websocket.py       # WebSocket API
 │   ├── websocket.py       # WebSocket API
@@ -56,7 +53,7 @@ agent/
 │       ├── search.py      # 网络搜索
 │       ├── search.py      # 网络搜索
 │       ├── webfetch.py    # 网页抓取
 │       ├── webfetch.py    # 网页抓取
 │       ├── skill.py       # 技能加载
 │       ├── skill.py       # 技能加载
-│       └── ask_human.py   # 人类协助
+│       └── subagent.py    # 子 Agent 统一入口(evaluate/delegate/explore)
 ├── memory/                # 跨会话记忆
 ├── memory/                # 跨会话记忆
 │   ├── models.py          # Experience, Skill
 │   ├── models.py          # Experience, Skill
@@ -77,7 +74,7 @@ agent/
 | 模块 | 职责 |
 | 模块 | 职责 |
 |-----|------|
 |-----|------|
 | **core/** | Agent 执行引擎 + 预设配置 |
 | **core/** | Agent 执行引擎 + 预设配置 |
-| **trace/** | 执行追踪 + 计划管理 + 子任务(goal, explore, delegate) |
+| **trace/** | 执行追踪 + 计划管理 |
 | **tools/** | 与外部世界交互(文件、命令、网络、浏览器) |
 | **tools/** | 与外部世界交互(文件、命令、网络、浏览器) |
 | **memory/** | 跨会话知识(Skills、Experiences) |
 | **memory/** | 跨会话知识(Skills、Experiences) |
 | **llm/** | LLM Provider 适配 |
 | **llm/** | LLM Provider 适配 |
@@ -173,6 +170,13 @@ async def run(task: str, agent_type: str = "default") -> AsyncIterator[Union[Tra
 
 
 **实现**:`agent/core/runner.py:AgentRunner`
 **实现**:`agent/core/runner.py:AgentRunner`
 
 
+### Runner 两种调用形态
+
+- `run(...)`:流式事件模式,返回 `AsyncIterator[Union[Trace, Message]]`
+- `run_result(...)`:结果模式,内部消费 `run(...)`,返回结构化结果(`status/summary/trace_id/stats/error`)
+
+`subagent` 工具默认使用 `run_result(...)`,并通过 `trace_id` 复用已创建或继承的子 Trace。
+
 ---
 ---
 
 
 ## 数据模型
 ## 数据模型
@@ -251,7 +255,7 @@ class Goal:
 
 
     # agent_call 特有(启动 Sub-Trace)
     # agent_call 特有(启动 Sub-Trace)
     sub_trace_ids: Optional[List[str]] = None
     sub_trace_ids: Optional[List[str]] = None
-    agent_call_mode: Optional[str] = None    # explore | delegate | sequential
+    agent_call_mode: Optional[str] = None    # explore | delegate | evaluate
     sub_trace_metadata: Optional[Dict] = None
     sub_trace_metadata: Optional[Dict] = None
 
 
     # 统计
     # 统计
@@ -261,6 +265,17 @@ class Goal:
     created_at: datetime
     created_at: datetime
 ```
 ```
 
 
+**Goal 类型**:
+- `normal` - 普通目标,由 Agent 直接执行
+- `agent_call` - 通过 subagent 工具创建的目标,会启动 Sub-Trace
+
+**agent_call 类型的 Goal**:
+- 调用 subagent 工具时自动设置
+- `agent_call_mode` 记录使用的模式(explore/delegate/evaluate)
+- `sub_trace_ids` 记录创建的所有 Sub-Trace ID
+- 状态转换:pending → in_progress(Sub-Trace 启动)→ completed(Sub-Trace 完成)
+- `summary` 包含格式化的汇总结果(explore 模式会汇总所有分支)
+
 **Goal 操作**(通过 goal 工具):
 **Goal 操作**(通过 goal 工具):
 - `add` - 添加顶层目标
 - `add` - 添加顶层目标
 - `under` - 在指定目标下添加子目标
 - `under` - 在指定目标下添加子目标
@@ -347,56 +362,43 @@ AGENT_PRESETS = {
 
 
 ## 子 Trace 机制
 ## 子 Trace 机制
 
 
-### explore 模式
+通过 `subagent` 工具创建子 Agent 执行任务,支持三种模式。
 
 
-并行探索多个分支,汇总结果返回。
+### explore 模式
 
 
-```python
-# 创建多个探索分支
-child_traces = await create_explore_traces(
-    parent_trace_id=ctx.trace_id,
-    prompts=["分析 A 方案", "分析 B 方案", "分析 C 方案"]
-)
-# 并行执行
-results = await asyncio.gather(*[run_trace(t) for t in child_traces])
-```
+并行探索多个分支,适合技术选型、方案对比等场景。
 
 
-**实现**:`agent/trace/explore.py`
+- 使用 `asyncio.gather()` 并行执行所有分支
+- 每个分支创建独立的 Sub-Trace
+- 只读工具权限(read_file, grep_content, glob_files)
+- 汇总所有分支结果返回
 
 
 ### delegate 模式
 ### delegate 模式
 
 
-委派子任务给专门的 Agent 执行
+委派单个任务给子 Agent 执行,适合代码分析、文档生成等场景
 
 
-```python
-child_trace = await create_delegate_trace(
-    parent_trace_id=ctx.trace_id,
-    task=prompt,
-    agent_type="analyst"  # 使用 analyst 预设
-)
-result = await run_trace(child_trace)
-```
+- 创建单个 Sub-Trace
+- 完整工具权限(除 subagent 外,防止递归)
+- 支持 `continue_from` 参数继续执行
+
+### evaluate 模式
+
+评估指定 Goal 的执行结果,提供质量评估和改进建议。
+
+- 访问目标 Goal 的执行结果
+- 完整工具权限
+- 返回评估结论和建议
+
+**实现位置**:`agent/tools/builtin/subagent.py`
 
 
-**实现**:`agent/trace/delegate.py`
+**详细文档**:[工具系统 - Subagent 工具](./tools.md#subagent-工具)
 
 
 ### ask_human 工具
 ### ask_human 工具
 
 
 创建阻塞式 Trace,等待人类通过 IM/邮件等渠道回复。
 创建阻塞式 Trace,等待人类通过 IM/邮件等渠道回复。
 
 
-```python
-@tool(name="ask_human")
-async def ask_human_tool(question: str, channel: str, ctx: ToolContext) -> ToolResult:
-    human_trace = await ctx.store.create_trace(
-        task=question,
-        parent_trace_id=ctx.trace_id,
-        blocked=True,
-        blocked_reason=f"等待人类通过 {channel} 回复"
-    )
-    await send_to_channel(channel, question)
-    response = await wait_for_human_response(human_trace.id)
-    return ToolResult(output=response)
-```
-
-**实现**:`agent/tools/builtin/ask_human.py`
+**注意**:此功能规划中,暂未实现。
+**注意**:此功能规划中,暂未实现。
 
 
 ---
 ---
 
 
@@ -425,7 +427,7 @@ async def my_tool(arg: str, ctx: ToolContext) -> ToolResult:
 | 目录 | 工具 | 说明 |
 | 目录 | 工具 | 说明 |
 |-----|------|------|
 |-----|------|------|
 | `trace/` | goal | Agent 内部计划管理 |
 | `trace/` | goal | Agent 内部计划管理 |
-| `trace/` | explore, delegate | 子 Trace 创建 |
+| `builtin/` | subagent | 子 Trace 创建(explore/delegate/evaluate) |
 | `builtin/file/` | read, write, edit, glob, grep | 文件操作 |
 | `builtin/file/` | read, write, edit, glob, grep | 文件操作 |
 | `builtin/browser/` | browser actions | 浏览器自动化 |
 | `builtin/browser/` | browser actions | 浏览器自动化 |
 | `builtin/` | bash, sandbox, search, webfetch, skill, ask_human | 其他工具 |
 | `builtin/` | bash, sandbox, search, webfetch, skill, ask_human | 其他工具 |
@@ -559,14 +561,26 @@ class TraceStore(Protocol):
 ├── {trace_id}/
 ├── {trace_id}/
 │   ├── meta.json        # Trace 元数据(含 tools 定义)
 │   ├── meta.json        # Trace 元数据(含 tools 定义)
 │   ├── goal.json        # GoalTree(mission + goals 列表)
 │   ├── goal.json        # GoalTree(mission + goals 列表)
+│   ├── events.jsonl     # 事件流(goal 变更、sub_trace 生命周期等)
 │   └── messages/        # Messages
 │   └── messages/        # Messages
-│       ├── 0001.json
+│       ├── {trace_id}-0001.json
 │       └── ...
 │       └── ...
-└── {trace_id}@explore-{timestamp}-001/  # 子 Trace
+└── {trace_id}@explore-{序号}-{timestamp}-001/  # 子 Trace
     └── ...
     └── ...
 ```
 ```
 
 
+**events.jsonl 说明**:
+- 记录 Trace 执行过程中的关键事件
+- 每行一个 JSON 对象,包含 event_id、event 类型、时间戳等
+- 主要事件类型:goal_added, goal_updated, sub_trace_started, sub_trace_completed
+- 用于实时监控和历史回放
+
+**Sub-Trace 目录命名**:
+- Explore: `{parent}@explore-{序号:03d}-{timestamp}-001`
+- Delegate: `{parent}@delegate-{timestamp}-001`
+- Evaluate: `{parent}@evaluate-{timestamp}-001`
+
 **meta.json 示例**:
 **meta.json 示例**:
 ```json
 ```json
 {
 {

+ 0 - 1476
docs/context-management.md

@@ -1,1476 +0,0 @@
-# Context 管理与执行计划
-
-> 本文档描述 Agent 的 Context 管理、执行计划和 Sub-Agent 机制。
-
----
-
-## 设计目标
-
-1. **自主长程执行**:Agent 能独立执行复杂任务,无需人工频繁干预
-2. **有效的 Context 管理**:长任务中保持关键信息,压缩次要细节
-3. **支持探索和回溯**:能尝试多种方案,失败时能有效回溯
-4. **统一的 Agent 模型**:主 Agent 和 Sub-Agent 使用相同的 Trace 结构
-5. **简单的工具接口**:LLM 只需理解少量简单工具,复杂逻辑由系统处理
-
----
-
-## 参考方案:OpenCode 的 Context 管理
-
-### 核心架构
-
-```
-┌─────────────────┐
-│   plan.md       │  ← 文本格式的计划(TODO 列表)
-└─────────────────┘
-         ↓
-┌─────────────────┐
-│  线性 Message   │  ← 对话历史
-│     List        │
-└─────────────────┘
-         ↓
-┌─────────────────┐
-│  Prune + Full   │  ← 两阶段压缩
-│   Compaction    │
-└─────────────────┘
-         ↓
-┌─────────────────┐
-│   Sub-Agent     │  ← 隔离大任务
-└─────────────────┘
-```
-
-### 1. Message 管理
-
-**数据结构**:
-- User Message: 用户输入,包含 TextPart, FilePart, CompactionPart, SubtaskPart 等
-- Assistant Message: LLM 输出,包含 TextPart, ToolPart, ReasoningPart 等
-- 每个 Message 包含多个 Part,支持流式处理
-
-**存储**:
-```
-Storage Key:
-["message", sessionID, messageID] -> MessageV2.Info
-["part", messageID, partID] -> MessageV2.Part
-```
-
-### 2. Context 压缩机制
-
-**两阶段压缩**:
-
-**阶段 1: Prune(清理旧工具输出)**
-```
-参数:
-- PRUNE_MINIMUM = 20,000 tokens(最少删除量)
-- PRUNE_PROTECT = 40,000 tokens(保护阈值)
-- PRUNE_PROTECTED_TOOLS = ["skill"](不删除的工具)
-
-流程:
-1. 从后向前遍历 messages
-2. 跳过最后 2 轮 turns(保护最近交互)
-3. 跳过已有 summary 标记的 assistant 消息
-4. 收集已完成工具调用的输出
-5. 当累计 > PRUNE_PROTECT 时,标记为已 compacted
-6. 当删除量 > PRUNE_MINIMUM 时,执行删除
-```
-
-**阶段 2: Full Compaction(上下文总结)**
-```
-流程:
-1. 创建新的 assistant 消息(summary=true)
-2. 调用 "compaction" 专用 agent
-3. 提示词: "Provide a detailed prompt for continuing our conversation..."
-4. 返回 "continue" 时自动创建新的 user 消息继续
-```
-
-### 3. Plan/Todo 机制
-
-**数据结构**:
-```typescript
-Todo.Info = {
-  id: string
-  content: string      // 任务描述
-  status: string       // pending | in_progress | completed | cancelled
-  priority: string     // high | medium | low
-}
-```
-
-**存储**:文件系统(.opencode/plans/xxx.md)或 Storage
-
-### 4. Sub-Agent 机制
-
-**Agent Mode**:
-- `primary`: 主代理,执行工具
-- `subagent`: 子代理,独立 context,结果汇总回主会话
-
-**内置 Sub-Agents**:
-- `general`: 通用代理,可并行执行多个任务
-- `explore`: 代码探索专用,仅允许查询工具
-- `compaction`: 上下文总结专用
-
-**Subtask 执行**:
-1. 创建 SubtaskPart
-2. 子代理独立处理(独立 message list)
-3. 结果通过 "The following tool was executed by the user" 汇总
-
-### 5. 优缺点分析
-
-**优点**:
-- 简单成熟,经过大量验证
-- Plan 和执行分离,用户可直接编辑 plan.md
-- Sub-agent 有效隔离大任务的 context
-
-**局限**:
-- Plan 是纯文本,与执行记录无结构化关联
-- 压缩是"事后"的,等满了再压缩
-- 回溯能力有限,无法精确回到某个状态
-- 不支持并行探索-合并的模式
-
----
-
-## 我们的方案
-
-### 核心思路
-
-```
-基于 OpenCode 方案,但采用更统一的架构:
-1. 结构化 Plan(goal 工具)
-2. 统一的 Trace 模型(主 Agent 和 Sub-Agent 都是 Trace)
-3. 并行探索-合并(explore 工具,启动独立的 Sub-Traces)
-4. 精确回溯(abandon + context 压缩)
-```
-
-### 架构
-
-```
-┌─────────────────────────────────────────────┐
-│            每个 Agent = 一个 Trace            │
-│  主 Trace 和 Sub-Trace 使用相同的数据结构      │
-└─────────────────────────────────────────────┘
-                      │
-         ┌────────────┴────────────┐
-         ↓                         ↓
-┌─────────────────┐      ┌─────────────────┐
-│   GoalTree      │      │   Messages      │
-│   (层级目标)     │      │   (扁平列表)     │
-│   goal 工具维护  │      │   goal_id 关联   │
-└─────────────────┘      └─────────────────┘
-         │                         │
-         ↓                         ↓
-┌─────────────────┐      ┌─────────────────┐
-│  工具调用        │      │  Sub-Traces     │
-│  explore/       │      │  完全独立的      │
-│  delegate       │──────▶  Trace 实例      │
-└─────────────────┘      └─────────────────┘
-         │
-         ↓
-┌─────────────────────────────────────────────┐
-│              DAG 可视化(派生视图)             │
-│  从 GoalTree + Messages 生成                  │
-│  节点 = 结果/里程碑,边 = 动作/执行过程         │
-│  边可展开/折叠,Sub-Trace 作为折叠边显示        │
-└─────────────────────────────────────────────┘
-```
-
-### 核心概念:统一的 Trace 模型
-
-**关键设计**:每个 Agent(主 Agent 或 Sub-Agent)都是一个完整的 Trace。
-
-```python
-# 主 Agent
-Trace(trace_id="2f8d3a1c", mode="agent", task="实现用户认证")
-
-# Sub-Agent(并行探索)
-Trace(trace_id="2f8d3a1c-4b6e-4f9a-8c2d-1e5b7a9f3c4d@explore-20260204220012-001", parent_trace_id="2f8d3a1c-4b6e-4f9a-8c2d-1e5b7a9f3c4d", agent_type="explore", task="JWT 方案")
-Trace(trace_id="2f8d3a1c-4b6e-4f9a-8c2d-1e5b7a9f3c4d@explore-20260204220012-002", parent_trace_id="2f8d3a1c-4b6e-4f9a-8c2d-1e5b7a9f3c4d", agent_type="explore", task="Session 方案")
-
-# Sub-Agent(单线委托)
-Trace(trace_id="2f8d3a1c-4b6e-4f9a-8c2d-1e5b7a9f3c4d@delegate-20260204220030-001", parent_trace_id="2f8d3a1c-4b6e-4f9a-8c2d-1e5b7a9f3c4d", agent_type="delegate", task="实现具体功能")
-```
-
-**优点**:
-- **概念统一**:不需要额外的 SubAgentContext/BranchContext 概念
-- **ID 简洁**:每个 Trace 内部独立编号(1, 2, 3),不需要前缀
-- **完全隔离**:每个 Trace 有独立的 GoalTree、Message List、LLM Context
-- **自然分布式**:每个 Trace 可以独立运行、迁移、存储
-- **层级清晰**:从 trace_id 可以直接解析出父子关系(`@` 前是父 ID)
-
-### 数据结构
-
-后端存储两类数据,可视化的 DAG 是派生视图:
-
-1. **GoalTree**:层级目标,注入 LLM
-2. **Messages**:执行记录,通过 `goal_id` 关联 Goal
-
-不存在独立的"边"数据结构,边在可视化时从 Messages 聚合生成。
-
-#### Trace
-
-```python
-@dataclass
-class Trace:
-    """
-    执行轨迹 - 一次完整的 Agent 运行
-
-    主 Agent 和 Sub-Agent 使用相同的 Trace 结构
-    """
-    trace_id: str                        # 主 Trace: UUID,Sub-Trace: parent@mode-timestamp-seq
-    mode: Literal["call", "agent"]
-    task: Optional[str] = None
-
-    # 父子关系
-    parent_trace_id: Optional[str] = None     # 父 Trace ID
-    parent_goal_id: Optional[str] = None      # 哪个 Goal 启动的
-
-    # 类型标记(语义)
-    agent_type: Optional[str] = None          # "main", "explore", "delegate", "compaction"
-
-    # 权限和配置
-    context: Dict[str, Any] = field(default_factory=dict)
-    # 可以包含:
-    # - allowed_tools: ["read", "grep"]    # 工具白名单
-    # - denied_tools: ["bash"]             # 工具黑名单
-    # - max_turns: 10                      # 最大对话轮数
-    # - timeout: 300                       # 超时(秒)
-
-    status: Literal["running", "completed", "failed"] = "running"
-
-    # 统计
-    total_messages: int = 0
-    total_tokens: int = 0
-    total_cost: float = 0.0
-
-    created_at: datetime
-    completed_at: Optional[datetime] = None
-```
-
-**实现**:`agent/execution/models.py:Trace`
-
-**Trace ID 命名规则**:
-
-```
-主 Trace(用户直接触发):
-  {uuid}
-  例如: 2f8d3a1c-4b6e-4f9a-8c2d-1e5b7a9f3c4d
-
-Sub-Trace(Agent 启动的子任务):
-  {parent_id}@{mode}-{timestamp}-{seq}
-  例如: 2f8d3a1c-4b6e-4f9a-8c2d-1e5b7a9f3c4d@explore-20260204220012-001
-  例如: 2f8d3a1c-4b6e-4f9a-8c2d-1e5b7a9f3c4d@delegate-20260204220015-002
-```
-
-**格式说明**:
-- `parent_id`:父 Trace 的**完整 UUID**(不截断,避免冲突)
-- `@`:父子关系分隔符
-- `mode`:运行模式(`explore`, `delegate`, `compaction` 等)
-- `timestamp`:创建时间戳(`YYYYMMDDHHmmss`)
-- `seq`:同一秒内的序号(`001`, `002`, ...)
-
-**优点**:
-1. **零碰撞风险**:使用完整 UUID,完全避免 ID 冲突
-2. **可精确追溯**:从 Sub-Trace ID 直接看到完整父 Trace ID
-3. **无需冲突检测**:实现简单,不依赖外部状态
-4. **信息完整**:一眼看出触发者、模式、时间
-5. **层级清晰**:`@` 分隔符明确表示父子关系
-
-**从 trace_id 解析父子关系**:
-```python
-def parse_parent_trace_id(trace_id: str) -> Optional[str]:
-    """从 trace_id 解析出 parent_trace_id"""
-    if '@' in trace_id:
-        return trace_id.split('@')[0]
-    return None
-
-# 示例
-parse_parent_trace_id("2f8d3a1c-4b6e-4f9a-8c2d-1e5b7a9f3c4d@explore-20260204220012-001")
-# → "2f8d3a1c-4b6e-4f9a-8c2d-1e5b7a9f3c4d"
-
-parse_parent_trace_id("2f8d3a1c-4b6e-4f9a-8c2d-1e5b7a9f3c4d")
-# → None
-```
-
-**生成 Sub-Trace ID**:
-```python
-from datetime import datetime
-from threading import Lock
-from typing import Dict
-
-# 全局计数器(线程安全)
-_seq_lock = Lock()
-_seq_counter: Dict[str, int] = {}  # key: "{parent_id}@{mode}-{timestamp}"
-
-def generate_sub_trace_id(parent_id: str, mode: str) -> str:
-    """
-    生成 Sub-Trace ID
-
-    格式: {parent_id}@{mode}-{timestamp}-{seq}
-
-    Args:
-        parent_id: 父 Trace ID(完整 UUID,不截断)
-        mode: 运行模式(explore, delegate, compaction)
-
-    Returns:
-        Sub-Trace ID(完整格式,无碰撞风险)
-    """
-    # 直接使用完整 UUID,不截断
-    timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
-
-    # 生成序号(同一秒内递增)
-    prefix = f"{parent_id}@{mode}-{timestamp}"
-    with _seq_lock:
-        seq = _seq_counter.get(prefix, 0) + 1
-        _seq_counter[prefix] = seq
-
-    return f"{prefix}-{seq:03d}"
-
-# 示例
-generate_sub_trace_id("2f8d3a1c-4b6e-4f9a-8c2d-1e5b7a9f3c4d", "explore")
-# → "2f8d3a1c-4b6e-4f9a-8c2d-1e5b7a9f3c4d@explore-20260204220012-001"
-
-generate_sub_trace_id("2f8d3a1c-4b6e-4f9a-8c2d-1e5b7a9f3c4d", "delegate")
-# → "2f8d3a1c-4b6e-4f9a-8c2d-1e5b7a9f3c4d@delegate-20260204220030-001"
-```
-
-#### Goal
-
-```python
-@dataclass
-class GoalStats:
-    message_count: int = 0               # 消息数量
-    total_tokens: int = 0                # Token 总数
-    total_cost: float = 0.0              # 总成本
-    preview: Optional[str] = None        # 工具调用摘要,如 "read_file → edit_file → bash"
-
-GoalStatus = Literal["pending", "in_progress", "completed", "abandoned"]
-GoalType = Literal["normal", "agent_call"]  # agent_call: 启动了 Sub-Agent
-
-@dataclass
-class Goal:
-    id: str                              # 内部唯一 ID,纯自增("1", "2", "3"...)
-    parent_id: Optional[str] = None      # 父 Goal ID(层级关系)
-    type: GoalType = "normal"            # Goal 类型
-
-    description: str                     # 目标描述(做什么)
-    reason: str                          # 创建理由(为什么做)
-    status: GoalStatus                   # pending | in_progress | completed | abandoned
-    summary: Optional[str] = None        # 完成/放弃时的总结
-
-    # agent_call 特有
-    sub_trace_ids: Optional[List[str]] = None  # 启动的 Sub-Trace IDs
-    agent_call_mode: Optional[str] = None      # "explore" | "delegate" | "sequential"
-    sub_trace_metadata: Optional[Dict[str, Dict[str, Any]]] = None  # Sub-Trace 元数据
-
-    # 统计(后端维护,用于可视化边的数据)
-    self_stats: GoalStats                # 自身统计(仅直接关联的 messages)
-    cumulative_stats: GoalStats          # 累计统计(自身 + 所有后代)
-```
-
-**实现**:`agent/goal/models.py:Goal`
-
-**ID 设计**:
-- **内部 ID**:每个 Trace 独立编号("1", "2", "3"),简单自增
-- **层级关系**:通过 `parent_id` 字段维护
-- **显示序号**:`to_prompt()` 时动态生成连续有意义的编号("1", "2", "2.1", "2.2"...)
-
-**sub_trace_metadata 字段**(`agent_call` 类型 Goal 专用):
-存储各 Sub-Trace 的关键信息,用于辅助决策和可视化:
-```python
-{
-    "sub_trace_id_1": {
-        "task": "JWT 方案",                    # Sub-Trace 任务描述
-        "status": "completed",                  # Sub-Trace 状态
-        "summary": "实现完成,使用 JWT token",   # Sub-Trace 总结
-        "last_message": {                       # 最后一条 assistant 消息
-            "role": "assistant",
-            "description": "生成 JWT token 并返回",
-            "content": "...",                   # 截断至 500 字符
-            "created_at": "2026-02-05T10:30:00"
-        },
-        "stats": {                              # 统计信息
-            "message_count": 8,
-            "total_tokens": 4000,
-            "total_cost": 0.05
-        }
-    },
-    # ... 其他 Sub-Trace
-}
-```
-
-**用途**:
-- 帮助主 Agent 决策:基于各分支的最终输出,决定是否需要展开查看详细信息
-- 前端可视化:在折叠视图中显示关键信息,用户快速判断是否需要展开
-- 调试追踪:快速了解每个分支的执行结果
-
-**统计更新逻辑**:
-- 每次添加 Message 时,更新对应 Goal 的 `self_stats`,并沿祖先链向上更新所有祖先的 `cumulative_stats`
-- 可视化中,折叠边使用 target Goal 的 `cumulative_stats`,展开边使用 `self_stats`
-
-```python
-@dataclass
-class GoalTree:
-    mission: str                         # 总任务描述
-    current_id: Optional[str] = None     # 当前焦点(内部 ID)
-    goals: List[Goal]                    # 扁平列表,通过 parent_id 构建层级
-    _next_id: int = 1                    # 内部 ID 计数器
-```
-
-**实现**:`agent/goal/models.py:GoalTree`
-
-#### Message
-
-Message 对应 LLM API 的消息,加上元数据。每条 Message 通过 `goal_id` 关联所属 Goal。
-
-```python
-@dataclass
-class Message:
-    message_id: str
-    trace_id: str                        # 所属 Trace ID
-    role: Literal["assistant", "tool"]   # 和 LLM API 一致
-    sequence: int                        # 当前 Trace 内的顺序
-    goal_id: Optional[str] = None        # 关联的 Goal 内部 ID(None = 还没有创建 Goal)
-    tool_call_id: Optional[str]          # tool 消息关联对应的 tool_call
-    content: Any                         # 消息内容(和 LLM API 格式一致)
-    description: str                     # 消息描述(系统自动生成)
-
-    # 元数据
-    tokens: Optional[int] = None
-    cost: Optional[float] = None
-    duration_ms: Optional[int] = None
-    created_at: datetime
-```
-
-**description 字段**(系统自动生成):
-- `assistant` 消息:优先取 content 中的 text,若无 text 则生成 "tool call: XX, XX"
-- `tool` 消息:使用 tool name
-
-**goal_id 说明**:
-- 通常关联到某个 Goal 的内部 ID(如 "1", "2")
-- 可以为 None:在 Trace 开始、还没有创建任何 Goal 时
-- 前端通过虚拟 START 节点展示 goal_id=None 的 messages
-
-**实现**:`agent/execution/models.py:Message`
-
-**Message 类型说明**:
-- `role="assistant"`:模型的一次返回,可能同时包含文本和多个 tool_calls
-- `role="tool"`:一个工具的执行结果,通过 `tool_call_id` 关联对应的 tool_call
-
-**查询 Message**:
-```python
-# 查询主 Trace 的 Messages
-GET /api/traces/2f8d3a1c-4b6e-4f9a-8c2d-1e5b7a9f3c4d/messages?goal_id=2
-
-# 查询 Sub-Trace 的 Messages
-GET /api/traces/2f8d3a1c-4b6e-4f9a-8c2d-1e5b7a9f3c4d@explore-20260204220012-001/messages?goal_id=1
-```
-
-### 工具设计
-
-#### goal 工具:计划管理
-
-```python
-@tool
-def goal(
-    add: Optional[str] = None,       # 添加目标(逗号分隔多个)
-    reason: Optional[str] = None,    # 创建理由(逗号分隔多个,与 add 一一对应)
-    after: Optional[str] = None,     # 在指定目标后面添加(同层级)
-    under: Optional[str] = None,     # 为指定目标添加子目标
-    done: Optional[str] = None,      # 完成当前目标,值为 summary
-    abandon: Optional[str] = None,   # 放弃当前目标,值为原因
-    focus: Optional[str] = None,     # 切换焦点到指定 ID
-) -> str:
-    """管理执行计划。"""
-```
-
-**实现**:`agent/goal/tool.py:goal_tool`
-
-**位置控制**:
-- 不指定 `after`/`under`:添加到当前 focus 的 goal 下作为子目标(无 focus 时添加到顶层)
-- `after="X"`:在目标 X 后面添加兄弟节点(同层级)
-- `under="X"`:为目标 X 添加子目标(如已有子目标,追加到最后)
-- `after` 和 `under` 互斥,不可同时指定
-
-**设计原则**:优先使用 `after` 明确指定位置,`under` 用于首次拆解或追加子任务。
-
-**示例**:
-
-```python
-# 1. 默认行为:添加顶层目标(无 focus)
-goal(add="分析代码, 实现功能, 测试")
-# [ ] 1. 分析代码
-# [ ] 2. 实现功能
-# [ ] 3. 测试
-
-# 2. 拆解子任务
-goal(add="设计接口, 实现代码", under="2")
-# [ ] 1. 分析代码
-# [ ] 2. 实现功能
-#     [ ] 2.1 设计接口
-#     [ ] 2.2 实现代码
-# [ ] 3. 测试
-
-# 3. 追加同层级任务
-goal(add="编写文档", after="3")
-# [ ] 1. 分析代码
-# [ ] 2. 实现功能
-#     [ ] 2.1 设计接口
-#     [ ] 2.2 实现代码
-# [ ] 3. 测试
-# [ ] 4. 编写文档
-
-# 4. 追加子任务(目标2已有子任务)
-goal(add="编写单元测试", under="2")
-# [ ] 1. 分析代码
-# [ ] 2. 实现功能
-#     [ ] 2.1 设计接口
-#     [ ] 2.2 实现代码
-#     [ ] 2.3 编写单元测试
-# [ ] 3. 测试
-# [ ] 4. 编写文档
-
-# 5. 使用 after 在子任务中间插入
-goal(add="代码审查", after="2.2")
-# [ ] 1. 分析代码
-# [ ] 2. 实现功能
-#     [ ] 2.1 设计接口
-#     [ ] 2.2 实现代码
-#     [ ] 2.3 代码审查
-#     [ ] 2.4 编写单元测试
-# [ ] 3. 测试
-# [ ] 4. 编写文档
-```
-
-**状态流转**:
-```
-pending ──focus──→ in_progress ──done──→ completed
-                        │                    ↓
-                        │              (压缩 context)
-                        │              (级联:若所有兄弟都 completed,父 goal 自动 completed)
-                        │
-                     abandon
-                        ↓
-                   abandoned
-                        ↓
-                  (压缩 context)
-```
-
-#### explore 工具:并行探索
-
-启动多个独立的 Sub-Traces 并行执行。
-
-```python
-@tool
-def explore(
-    branches: List[str],                  # 探索方向列表
-    background: Optional[str] = None,     # 背景概括(可选)
-) -> str:
-    """
-    并行探索多个方向,汇总结果。
-
-    每个方向会启动一个独立的 Sub-Trace(agent_type="explore")。
-
-    - background 有值:用它初始化各 Sub-Trace 的 context
-    - background 为空:继承主 Trace 的 message list
-
-    返回:各分支的汇总结果
-    """
-```
-
-**内部实现**:
-```python
-async def explore_tool(branches: List[str], background: Optional[str] = None) -> str:
-    current_trace_id = get_current_trace_id()
-    current_goal_id = get_current_goal_id()
-
-    # 1. 创建 agent_call Goal
-    goal = Goal(
-        id=next_id(),
-        type="agent_call",
-        description=f"并行探索 {len(branches)} 个方案",
-        agent_call_mode="explore",
-        sub_trace_ids=[],
-    )
-
-    # 2. 为每个分支创建 Sub-Trace
-    sub_traces = []
-    for i, desc in enumerate(branches):
-        suffix = chr(ord('A') + i)  # A, B, C...
-        sub_trace = Trace.create(
-            trace_id=f"{current_trace_id}.{suffix}",
-            parent_trace_id=current_trace_id,
-            parent_goal_id=current_goal_id,
-            agent_type="explore",
-            task=desc,
-            context=get_explore_context(),  # 从配置获取权限设置
-        )
-        sub_traces.append(sub_trace)
-        goal.sub_trace_ids.append(sub_trace.trace_id)
-
-    # 3. 并行执行
-    results = await asyncio.gather(
-        *[run_agent(st, background) for st in sub_traces]
-    )
-
-    # 4. 收集元数据并汇总
-    sub_trace_metadata = {}
-    summary_parts = []
-
-    for sub_trace, result in zip(sub_traces, results):
-        # 获取 Sub-Trace 最新状态
-        updated_trace = await store.get_trace(sub_trace.trace_id)
-
-        # 获取最后一条 assistant 消息
-        messages = await store.get_messages(sub_trace.trace_id)
-        last_message = None
-        for msg in reversed(messages):
-            if msg.role == "assistant":
-                last_message = msg
-                break
-
-        # 构建元数据
-        sub_trace_metadata[sub_trace.trace_id] = {
-            "task": sub_trace.task,
-            "status": updated_trace.status if updated_trace else "unknown",
-            "summary": result.get("summary", "") if isinstance(result, dict) else "",
-            "last_message": {
-                "role": last_message.role,
-                "description": last_message.description,
-                "content": last_message.content[:500] if last_message.content else None,
-                "created_at": last_message.created_at.isoformat()
-            } if last_message else None,
-            "stats": {
-                "message_count": updated_trace.total_messages,
-                "total_tokens": updated_trace.total_tokens,
-                "total_cost": updated_trace.total_cost
-            }
-        }
-
-        # 组装摘要文本
-        summary_parts.append(f"### {sub_trace.task}")
-        summary_parts.append(result.get("summary", "执行完成"))
-
-    # 5. 更新 Goal,包含元数据
-    await store.update_goal(
-        current_trace_id,
-        current_goal_id,
-        status="completed",
-        summary=f"探索了 {len(branches)} 个方案",
-        sub_trace_metadata=sub_trace_metadata  # 保存元数据
-    )
-
-    # 6. 返回文本摘要(给 LLM)
-    return "\n".join(summary_parts)
-```
-
-**权限配置**:
-```python
-# 示例:系统配置中定义 Sub-Trace 的默认权限
-# 可以根据 agent_type 或具体场景调整
-
-def get_explore_context() -> Dict[str, Any]:
-    """获取 explore 模式的 context 配置"""
-    return {
-        # 工具权限:根据实际需求配置
-        # 选项 1:受限权限(只读,快速探索)
-        # "allowed_tools": ["read", "grep", "glob"],
-
-        # 选项 2:完整权限(需要实际实现来评估)
-        # "allowed_tools": None,  # None = 所有工具
-
-        # 步数限制
-        "max_turns": 20,
-    }
-```
-
-#### delegate 工具:单线委托
-
-将大任务委托给独立的 Sub-Trace 执行。
-
-```python
-@tool
-def delegate(task: str) -> str:
-    """
-    将大任务委托给独立的 sub-agent。
-
-    创建一个独立的 Sub-Trace(agent_type="delegate"),
-    有独立的 context,权限由配置决定。
-
-    返回:任务执行结果摘要
-    """
-```
-
-**内部实现**:
-```python
-async def delegate_tool(task: str) -> str:
-    current_trace_id = get_current_trace_id()
-    current_goal_id = get_current_goal_id()
-
-    # 1. 创建 agent_call Goal
-    goal = Goal(
-        id=next_id(),
-        type="agent_call",
-        description=f"委托任务: {task}",
-        agent_call_mode="delegate",
-        sub_trace_ids=[],
-    )
-
-    # 2. 创建 Sub-Trace
-    suffix = f"task{next_task_num()}"  # task1, task2...
-    sub_trace = Trace.create(
-        trace_id=f"{current_trace_id}.{suffix}",
-        parent_trace_id=current_trace_id,
-        parent_goal_id=current_goal_id,
-        agent_type="delegate",
-        task=task,
-        context=get_delegate_context(),  # 从配置获取权限设置
-    )
-    goal.sub_trace_ids.append(sub_trace.trace_id)
-
-    # 3. 执行
-    result = await run_agent(sub_trace)
-
-    # 4. 返回摘要
-    return result.summary
-```
-
-**权限配置**:
-```python
-def get_delegate_context() -> Dict[str, Any]:
-    """获取 delegate 模式的 context 配置"""
-    return {
-        # 工具权限:根据实际需求配置
-        # 通常 delegate 用于完整任务,给完整权限
-        "allowed_tools": None,  # None = 所有工具
-
-        # 或者也可以限制:
-        # "allowed_tools": ["read", "write", "edit", "bash"],
-        # "denied_tools": ["危险工具"],
-
-        # 步数限制
-        "max_turns": 50,
-    }
-```
-
-**注意**:
-- `explore` 和 `delegate` 的权限配置是独立的,可以根据需求调整
-- 不是工具本身决定权限,而是通过 `Trace.context` 配置
-- 典型配置:
-  - `explore`:可能受限(快速探索)或完整(需要实现验证)
-  - `delegate`:通常完整权限(执行完整任务)
-- 但这些都不是固定的,可以根据具体场景调整
-
-### Context 管理
-
-#### 1. Plan 注入
-
-每次 LLM 调用时,在 system prompt 末尾注入当前计划状态。注入时过滤掉 abandoned 目标,使用连续的显示序号。
-
-**展示策略**:
-- **完整展示**:所有顶层目标、当前 focus 目标的完整父链及其子树
-- **折叠其他**:非关键路径的子目标可折叠显示(显示节点数和状态)
-- **连续编号**:显示 ID 连续且有意义("1", "2", "2.1", "2.2")
-
-```markdown
-## Current Plan
-
-**Mission**: 实现用户认证功能
-**Current**: 2.2 实现登录接口
-
-**Progress**:
-[✓] 1. 分析代码
-    → 用户模型在 models/user.py,使用 bcrypt 加密
-[→] 2. 实现功能
-    [✓] 2.1 设计接口
-        → API 设计文档完成,使用 REST 风格
-    [→] 2.2 实现登录接口  ← current
-    [ ] 2.3 实现注册接口
-[ ] 3. 测试
-    (3 subtasks)
-```
-
-**实现**:`agent/goal/models.py:GoalTree.to_prompt`
-
-**注意**:当前 focus 目标的所有父目标及其子目标都会完整展示,确保 LLM 理解当前上下文。这样在使用 `after` 或 `under` 参数时,LLM 可以准确引用目标 ID。
-
-#### 2. 完成时压缩
-
-当调用 `goal(done="...")` 时:
-1. 找到该 goal 关联的所有 messages(通过 goal_id)
-2. 将详细 messages 替换为一条 summary message
-3. 更新 goal 状态为 completed
-
-#### 3. 回溯(Abandon)
-
-两种模式:
-
-**模式 1:需要修改的计划还没有执行**
-
-直接修改计划并继续执行。Goal 状态为 pending 时,可以直接修改 description 或删除。
-
-**模式 2:需要修改的计划已经执行**
-
-1. 将原 Goal 标记为 `abandoned`(保留在 GoalTree 数据中,但 `to_prompt()` 不展示)
-2. 将废弃分支关联的 messages 做 summary
-3. 将 summary 累积到新分支的第一条消息中(供 LLM 参考历史失败原因)
-4. 创建新的 Goal 继续执行
-
-**Before 回溯**:
-```
-GoalTree 数据:
-  [✓] 1. 分析代码               (内部ID: 1)
-  [→] 2. 实现方案 A              (内部ID: 2)
-  [ ] 3. 测试                    (内部ID: 3)
-
-Messages:
-  [分析代码的 20 条 message...]
-  [实现方案 A 的 30 条 message...]
-  [测试失败的 message...]
-```
-
-**After 回溯**:
-```
-GoalTree 数据(含废弃):
-  [✓] 1. 分析代码               (内部ID: 1)
-  [✗] 2. 实现方案 A              (内部ID: 2, abandoned)
-  [→] 3. 实现方案 B              (内部ID: 4, 新建)
-  [ ] 4. 测试                    (内部ID: 3)
-
-to_prompt() 输出(给 LLM,连续编号):
-  [✓] 1. 分析代码
-  [→] 2. 实现方案 B  ← current
-  [ ] 3. 测试
-
-Messages:
-  [分析代码的 20 条 message...]
-  [Summary: "尝试方案 A,因依赖问题失败"]     ← 原 messages 压缩为 1 条
-  [方案 B 第一条消息,包含废弃分支的 summary]  ← 供 LLM 参考
-  [方案 B 的后续 message...]
-```
-
-**实现**:`agent/goal/compaction.py`
-
-#### 4. 用户编辑计划
-
-用户可以通过编辑 Goal 内的 goal() 工具调用来修改执行计划。
-
-**核心思路**:利用现有的 GoalTree 结构和 Message sequence,无需额外追踪字段。
-
-**编辑流程**:
-1. 用户选择某个 Goal,查看其内所有 messages
-2. 找到 goal() 调用,编辑其参数
-3. 系统废弃该 message 之后的所有内容:
-   - 废弃 sequence >= 该 message 的所有 messages
-   - 根据 GoalTree 结构,废弃受影响的 Goals(当前 Goal 的子孙、后续同级 Goals)
-4. 从该 Goal 重新执行
-
-**废弃判断**:
-- 基于 Message sequence:废弃 seq >= 编辑点的所有 messages
-- 基于 GoalTree 结构:废弃当前 Goal 的所有子节点、以及后续创建的 Goals
-- 不需要追踪"哪个 message 创建了哪个 Goal",结构本身就能判断
-
-**UI 展示**:
-- Goal 详情显示其内所有 messages
-- 突出显示 goal() 调用(assistant message 中包含 tool_calls)
-- 提供"编辑并重新执行"按钮
-
-**API**:
-```
-GET /api/traces/{trace_id}/messages?goal_id=X
-  # 返回 Goal X 的所有 messages
-
-PATCH /api/traces/{trace_id}/replay
-{
-  "from_message_id": "msg_123",
-  "modified_tool_call": {
-    "tool_call_id": "call_abc",
-    "new_arguments": {"add": "新任务列表", "under": "2"}
-  }
-}
-```
-
-**实现**:`agent/goal/replay.py`
-
-**注意**:这是最通用、最简单的编辑方式,完全基于现有数据结构。
-
-### 可视化
-
-#### DAG 模型
-
-可视化展示为 DAG(有向无环图),不是树。
-
-**核心概念**:
-- **节点** = Goal 完成后的结果/里程碑
-- **边** = 从一个结果到下一个结果的执行过程(动作/策略)
-- 每个节点对应一条入边,入边的数据从该 Goal 关联的 Messages 聚合
-
-**展开/折叠**:对边操作,对应目标的层级展开。
-
-```
-折叠视图(只看顶层 Goals):
-[START] ──→ [1:分析完成] ──→ [2:实现完成] ──→ [3:测试完成]
-                              逻辑边
-
-展开 [1]→[2] 的边(显示 Goal 2 的子目标):
-[START] ──→ [1:分析完成] ──→ [2.1:设计完成] ──→ [2.2:代码完成] ──→ [3:测试完成]
-                              执行边             执行边
-```
-
-展开时,父节点 [2] 被子节点 [2.1], [2.2] **替代**。
-折叠时,子节点合并回父节点 [2]。
-
-嵌套展开:如果 2.1 也有子目标,可以继续展开 [1]→[2.1] 的边。
-
-**废弃分支**:在可视化中以灰色样式展示废弃分支。
-
-```
-[1:分析完成] ──→ [2:方案A(废弃)] ──→ ...     ← 灰色
-             ──→ [4:方案B] ──→ [3:测试]       ← 正常
-```
-
-#### API
-
-后端提供 GoalTree 数据,前端负责生成 DAG 视图。
-
-**REST 端点**:
-```
-GET /api/traces/{trace_id}           # 获取 Trace + GoalTree
-GET /api/traces/{trace_id}/messages?goal_id=2.1  # 获取 Messages(边详情)
-```
-
-**响应**(GoalTree 部分):
-```json
-{
-  "goal_tree": {
-    "mission": "实现用户认证功能",
-    "current_id": "4",
-    "goals": [
-      {
-        "id": "1",
-        "parent_id": null,
-        "branch_id": null,
-        "type": "normal",
-        "description": "分析代码",
-        "reason": "了解现有结构",
-        "status": "completed",
-        "summary": "用户模型在 models/user.py",
-        "self_stats": {"message_count": 5, "total_tokens": 2300, "total_cost": 0.03, "preview": "glob → read × 2"},
-        "cumulative_stats": {"message_count": 5, "total_tokens": 2300, "total_cost": 0.03, "preview": "glob → read × 2"}
-      },
-      {
-        "id": "2",
-        "parent_id": null,
-        "branch_id": null,
-        "type": "normal",
-        "description": "实现功能",
-        "reason": "核心任务",
-        "status": "in_progress",
-        "self_stats": {"message_count": 0, ...},
-        "cumulative_stats": {"message_count": 11, "total_tokens": 5700, ...}
-      },
-      {
-        "id": "3",
-        "parent_id": "2",
-        "branch_id": null,
-        "type": "normal",
-        "description": "设计接口",
-        "status": "completed",
-        "self_stats": {...}
-      },
-      {
-        "id": "4",
-        "parent_id": "2",
-        "branch_id": null,
-        "type": "normal",
-        "description": "实现代码",
-        "status": "in_progress",
-        "self_stats": {...}
-      },
-      {
-        "id": "5",
-        "parent_id": null,
-        "branch_id": null,
-        "type": "normal",
-        "description": "测试",
-        "status": "pending",
-        ...
-      }
-    ]
-  }
-}
-```
-
-**DAG 生成逻辑**(前端实现):
-1. 根据用户展开状态,确定可见 Goal 序列
-2. 相邻 Goal 之间形成边
-3. 边的统计数据从 target Goal 的 stats 获取(折叠用 `cumulative_stats`,展开用 `self_stats`)
-4. 边的详细内容通过 Messages API 查询
-
-**实现**:见 [frontend/API.md](../frontend/API.md)
-
-#### WebSocket 实时推送
-
-```
-ws://localhost:8000/api/traces/{trace_id}/watch?since_event_id=0
-```
-
-**事件类型**:
-
-| 事件 | 触发时机 | payload |
-|------|---------|---------|
-| `connected` | WebSocket 连接成功 | trace_id, current_event_id, goal_tree(完整 GoalTree) |
-| `goal_added` | 新增 Goal | goal 完整数据(含 self_stats, cumulative_stats) |
-| `goal_updated` | Goal 状态变化(含级联完成) | goal_id, updates(含 cumulative_stats),affected_goals |
-| `message_added` | 新 Message | message 数据(含 goal_id),affected_goals |
-| `trace_completed` | 执行完成 | 统计信息 |
-
-**事件详情**:
-
-**`connected`** - 连接时推送完整 GoalTree,前端据此初始化 DAG:
-```json
-{
-  "event": "connected",
-  "trace_id": "xxx",
-  "current_event_id": 42,
-  "goal_tree": { "mission": "...", "goals": [...] }
-}
-```
-
-**`message_added`** - 新 Message 时,后端更新统计并推送受影响的 Goals:
-```json
-{
-  "event": "message_added",
-  "message": { "message_id": "...", "role": "assistant", "goal_id": "2.1", "..." : "..." },
-  "affected_goals": [
-    { "goal_id": "2.1", "self_stats": {"message_count": 4, "total_tokens": 2000, "total_cost": 0.03, "preview": "read → edit × 2"}, "cumulative_stats": {"message_count": 4, "total_tokens": 2000, "total_cost": 0.03, "preview": "read → edit × 2"} },
-    { "goal_id": "2", "cumulative_stats": {"message_count": 12, "total_tokens": 6800, "total_cost": 0.08, "preview": "glob → read → edit × 5 → bash"} }
-  ]
-}
-```
-
-`affected_goals` 包含该 Message 直接关联的 Goal(更新 self_stats + cumulative_stats)以及所有祖先 Goal(仅更新 cumulative_stats)。前端根据当前展开状态选择使用哪个 stats 渲染边。
-
-**`goal_updated`** - Goal 状态变化时推送,包含级联完成场景:
-```json
-{
-  "event": "goal_updated",
-  "goal_id": "2.1",
-  "updates": { "status": "completed", "summary": "接口设计完成" },
-  "affected_goals": [
-    { "goal_id": "2.1", "cumulative_stats": {"message_count": 4, "total_tokens": 2000, "total_cost": 0.03, "preview": "read → edit × 2"} },
-    { "goal_id": "2", "status": "completed", "summary": "功能实现完成", "cumulative_stats": {"message_count": 12, "total_tokens": 6800, "total_cost": 0.08, "preview": "..."} }
-  ]
-}
-```
-
-当所有子 Goal 完成时,后端自动级联完成父 Goal,并在 `affected_goals` 中包含所有状态变更的祖先。前端收到后直接更新对应节点,无需自行计算。
-
-**`goal_added`** - 新增 Goal,携带完整 Goal 数据:
-```json
-{
-  "event": "goal_added",
-  "goal": { "id": "2.1", "description": "设计接口", "reason": "需要先定义 API", "status": "pending", "self_stats": {}, "cumulative_stats": {} },
-  "parent_id": "2"
-}
-```
-
-**实现**:`agent/execution/websocket.py`
-
-### 存储结构
-
-```
-.trace/
-├── 2f8d3a1c-4b6e-4f9a-8c2d-1e5b7a9f3c4d/                             # 主 Trace
-│   ├── meta.json                # Trace 元数据
-│   ├── goal.json                # GoalTree
-│   ├── messages/                # Messages
-│   │   ├── {message_id}.json
-│   │   └── ...
-│   └── events.jsonl             # 事件流
-│
-├── 2f8d3a1c-4b6e-4f9a-8c2d-1e5b7a9f3c4d@explore-20260204220012-001/ # Sub-Trace A(并行探索)
-│   ├── meta.json                # parent_trace_id: "2f8d3a1c-4b6e-4f9a-8c2d-1e5b7a9f3c4d"
-│   ├── goal.json                # 独立的 GoalTree
-│   ├── messages/
-│   └── events.jsonl
-│
-├── 2f8d3a1c-4b6e-4f9a-8c2d-1e5b7a9f3c4d@explore-20260204220012-002/ # Sub-Trace B(并行探索)
-│   └── ...
-│
-└── 2f8d3a1c-4b6e-4f9a-8c2d-1e5b7a9f3c4d@delegate-20260204220030-001/ # Sub-Trace task1(单线委托)
-    └── ...
-```
-
-**关键点**:
-- 每个 Trace(主/子)都是完全独立的目录
-- 从 trace_id 可以直接找到对应目录
-- 通过 `parent_trace_id` 可以追溯父子关系
-
----
-
-## 并行探索设计(explore 工具)
-
-### 场景
-
-```
-主 Trace (2f8d3a1c-4b6e-4f9a-8c2d-1e5b7a9f3c4d):
-  [1] 分析问题
-  [2] 并行探索认证方案 (type=agent_call, mode=explore)
-      → 启动 Sub-Traces(完整 ID)
-  [3] 完善实现
-
-Sub-Trace A (2f8d3a1c...@explore-20260204220012-001):
-  [1] JWT 设计
-  [2] JWT 实现
-  → 返回摘要:"JWT 方案实现完成"
-
-Sub-Trace B (2f8d3a1c...@explore-20260204220012-002):
-  [1] Session 设计
-  [2] Session 实现
-  [3] Session 测试
-  → 返回摘要:"Session 方案实现完成"
-
-explore 工具返回:
-  汇总两个分支的结果,主 Agent 继续决策
-```
-
-**核心原则**:
-- 每个分支是独立的 Trace,有自己的 GoalTree 和 Message List
-- 分支内的 Goal ID 简单编号("1", "2", "3"),独立于主 Trace
-- 主 Trace 的 Goal 通过 `sub_trace_ids` 字段关联分支
-- 分支完全独立存储,可以并行运行
-- explore 工具自动汇总各分支 summary
-
-### 数据结构
-
-**主 Trace 的 GoalTree**:
-
-```python
-# Trace: 2f8d3a1c-4b6e-4f9a-8c2d-1e5b7a9f3c4d
-goals = [
-    Goal(id="1", type="normal", description="分析问题"),
-    Goal(
-        id="2",
-        type="agent_call",
-        description="并行探索认证方案",
-        agent_call_mode="explore",
-        sub_trace_ids=[
-            "2f8d3a1c-4b6e-4f9a-8c2d-1e5b7a9f3c4d@explore-20260204220012-001",
-            "2f8d3a1c-4b6e-4f9a-8c2d-1e5b7a9f3c4d@explore-20260204220012-002"
-        ],
-    ),
-    Goal(id="3", type="normal", description="完善实现"),
-]
-```
-
-**Sub-Trace A 的 GoalTree**(独立编号):
-
-```python
-# Trace: 2f8d3a1c-4b6e-4f9a-8c2d-1e5b7a9f3c4d@explore-20260204220012-001
-goals = [
-    Goal(id="1", type="normal", description="JWT 设计"),
-    Goal(id="2", type="normal", description="JWT 实现"),
-]
-```
-
-**Sub-Trace B 的 GoalTree**(独立编号):
-
-```python
-# Trace: 2f8d3a1c-4b6e-4f9a-8c2d-1e5b7a9f3c4d@explore-20260204220012-002
-goals = [
-    Goal(id="1", type="normal", description="Session 设计"),
-    Goal(id="2", type="normal", description="Session 实现"),
-    Goal(id="3", type="normal", description="Session 测试"),
-]
-```
-
-### 存储结构
-
-```
-.trace/
-├── 2f8d3a1c-4b6e-4f9a-8c2d-1e5b7a9f3c4d/                             # 主 Trace
-│   ├── meta.json
-│   ├── goal.json
-│   └── messages/
-│
-├── 2f8d3a1c-4b6e-4f9a-8c2d-1e5b7a9f3c4d@explore-20260204220012-001/ # Sub-Trace A
-│   ├── meta.json            # parent_trace_id: "2f8d3a1c-4b6e-4f9a-8c2d-1e5b7a9f3c4d"
-│   ├── goal.json            # 独立的 GoalTree
-│   └── messages/
-│
-└── 2f8d3a1c-4b6e-4f9a-8c2d-1e5b7a9f3c4d@explore-20260204220012-002/ # Sub-Trace B
-    └── ...
-```
-
-### DAG 可视化
-
-**折叠视图**(Sub-Trace 作为单个节点):
-```
-[1:分析] ──→ [2:并行探索] ──→ [3:完善]
-                 │
-          (启动2个Sub-Trace)
-```
-
-**展开分支视图**(显示并行路径):
-```
-                  ┌──→ [A:JWT方案] ────┐
-[1:分析] ──→ [2] ─┤                    ├──→ [3:完善]
-                  └──→ [B:Session方案] ┘
-```
-
-**继续展开分支 A 内部**(加载 Sub-Trace 的 GoalTree):
-```
-                  ┌──→ [A.1:JWT设计] → [A.2:JWT实现] ──┐
-[1:分析] ──→ [2] ─┤                                    ├──→ [3:完善]
-                  └──→ [B:Session方案] ────────────────┘
-```
-
-**注意**:
-- `[A:JWT方案]` 是折叠视图,代表整个 Sub-Trace
-- `[A.1]`, `[A.2]` 是展开后显示 Sub-Trace 内部的 Goals
-- 前端显示为 "A.1",但后端查询使用完整 trace_id:
-  `GET /api/traces/2f8d3a1c-4b6e-4f9a-8c2d-1e5b7a9f3c4d@explore-20260204220012-001/messages?goal_id=1`
-
-### 前端 API
-
-**REST**:返回主 Trace 的 GoalTree + Sub-Trace 列表(元数据)。
-
-```http
-GET /api/traces/2f8d3a1c-4b6e-4f9a-8c2d-1e5b7a9f3c4d
-```
-
-响应:
-```json
-{
-  "trace_id": "2f8d3a1c-4b6e-4f9a-8c2d-1e5b7a9f3c4d",
-  "status": "running",
-  "goal_tree": {
-    "goals": [
-      {"id": "1", "type": "normal", "description": "分析问题"},
-      {
-        "id": "2",
-        "type": "agent_call",
-        "description": "并行探索认证方案",
-        "agent_call_mode": "explore",
-        "sub_trace_ids": [
-          "2f8d3a1c-4b6e-4f9a-8c2d-1e5b7a9f3c4d@explore-20260204220012-001",
-          "2f8d3a1c-4b6e-4f9a-8c2d-1e5b7a9f3c4d@explore-20260204220012-002"
-        ],
-        "sub_trace_metadata": {
-          "2f8d3a1c-4b6e-4f9a-8c2d-1e5b7a9f3c4d@explore-20260204220012-001": {
-            "task": "JWT 方案",
-            "status": "completed",
-            "summary": "实现完成,使用 JWT token",
-            "last_message": {
-              "role": "assistant",
-              "description": "生成 JWT token 并返回",
-              "content": "JWT 方案实现完成。使用 jsonwebtoken 库生成 token,包含 user_id 和过期时间...",
-              "created_at": "2026-02-05T10:30:00"
-            },
-            "stats": {
-              "message_count": 8,
-              "total_tokens": 4000,
-              "total_cost": 0.05
-            }
-          },
-          "2f8d3a1c-4b6e-4f9a-8c2d-1e5b7a9f3c4d@explore-20260204220012-002": {
-            "task": "Session 方案",
-            "status": "completed",
-            "summary": "实现完成,使用 Redis 存储 session",
-            "last_message": {
-              "role": "assistant",
-              "description": "配置 Redis 并实现 session 管理",
-              "content": "Session 方案实现完成。使用 Redis 存储 session,支持过期自动清理...",
-              "created_at": "2026-02-05T10:32:00"
-            },
-            "stats": {
-              "message_count": 12,
-              "total_tokens": 4000,
-              "total_cost": 0.05
-            }
-          }
-        }
-      },
-      {"id": "3", "type": "normal", "description": "完善实现"}
-    ]
-  },
-  "sub_traces": {
-    "2f8d3a1c-4b6e-4f9a-8c2d-1e5b7a9f3c4d@explore-20260204220012-001": {
-      "trace_id": "2f8d3a1c-4b6e-4f9a-8c2d-1e5b7a9f3c4d@explore-20260204220012-001",
-      "parent_trace_id": "2f8d3a1c-4b6e-4f9a-8c2d-1e5b7a9f3c4d",
-      "parent_goal_id": "2",
-      "agent_type": "explore",
-      "task": "JWT 方案",
-      "status": "completed",
-      "total_messages": 8,
-      "total_tokens": 4000,
-      "total_cost": 0.05
-    },
-    "2f8d3a1c-4b6e-4f9a-8c2d-1e5b7a9f3c4d@explore-20260204220012-002": {
-      "trace_id": "2f8d3a1c-4b6e-4f9a-8c2d-1e5b7a9f3c4d@explore-20260204220012-002",
-      "agent_type": "explore",
-      "task": "Session 方案",
-      "status": "completed",
-      ...
-    }
-  }
-}
-```
-
-**按需加载 Sub-Trace 详情**:
-
-```http
-GET /api/traces/2f8d3a1c-4b6e-4f9a-8c2d-1e5b7a9f3c4d@explore-20260204220012-001
-```
-
-响应:
-```json
-{
-  "trace_id": "2f8d3a1c-4b6e-4f9a-8c2d-1e5b7a9f3c4d@explore-20260204220012-001",
-  "parent_trace_id": "2f8d3a1c-4b6e-4f9a-8c2d-1e5b7a9f3c4d",
-  "parent_goal_id": "2",
-  "agent_type": "explore",
-  "task": "JWT 方案",
-  "status": "completed",
-  "goal_tree": {
-    "goals": [
-      {"id": "1", "description": "JWT 设计", ...},
-      {"id": "2", "description": "JWT 实现", ...}
-    ]
-  },
-  "total_tokens": 4000,
-  "total_cost": 0.05
-}
-```
-
-**WebSocket 事件**:
-
-| 事件 | 触发时机 | payload |
-|------|---------|---------|
-| `sub_trace_started` | Sub-Trace 开始 | trace_id, parent_trace_id, parent_goal_id, agent_type |
-| `goal_added` | 任何 Trace 新增 Goal | trace_id, goal |
-| `message_added` | 任何 Trace 新 Message | trace_id, message, affected_goals |
-| `sub_trace_completed` | Sub-Trace 完成 | trace_id, summary, stats |
-| `trace_completed` | 主 Trace 完成 | trace_id, stats |
-
-### explore 工具执行流程
-
-详细流程见前面"工具设计"部分,这里展示关键步骤:
-
-```python
-async def explore_tool(branches: List[str]) -> str:
-    current_trace_id = "abc123"
-    current_goal_id = "2"
-
-    # 1. 创建 agent_call Goal
-    goal = Goal(
-        id="2",
-        type="agent_call",
-        description=f"并行探索 {len(branches)} 个方案",
-        agent_call_mode="explore",
-        sub_trace_ids=[],
-    )
-
-    # 2. 创建多个 Sub-Traces
-    sub_traces = []
-    for i, desc in enumerate(branches):
-        sub_trace = Trace.create(
-            trace_id=generate_sub_trace_id(current_trace_id, "explore"),
-            parent_trace_id=current_trace_id,
-            parent_goal_id=current_goal_id,
-            agent_type="explore",
-            task=desc,
-            context={"allowed_tools": ["read", "grep", "glob"]},
-        )
-        sub_traces.append(sub_trace)
-        goal.sub_trace_ids.append(sub_trace.trace_id)
-
-    # 3. 并行执行
-    results = await asyncio.gather(
-        *[run_agent(st) for st in sub_traces]
-    )
-
-    # 4. 汇总返回
-    return format_results(results)
-```
-
-**汇总结果示例**:
-```markdown
-## 探索结果
-
-### 方案 A: JWT 方案
-实现完成。优点:无状态,易扩展。缺点:token 较大,无法主动失效。
-
-### 方案 B: Session 方案
-实现完成。优点:token 小,可主动失效。缺点:需要 Redis 存储。
-
----
-两种方案都已实现,请选择一种继续。
-```
-
-### Context 压缩
-
-Sub-Trace 完成后的压缩策略:
-
-1. **Sub-Trace 完成时**:Sub-Trace 的详细 context 压缩为 summary(存储在 Trace.summary)
-2. **explore 完成后**:所有 Sub-Traces 的 summary 汇总到主 Trace 的 tool result
-3. **主 Trace context**:explore 工具调用被压缩为一条包含汇总结果的 tool message
-
----
-
-## 与 OpenCode 方案的对比
-
-| 方面 | OpenCode | 我们的方案 |
-|------|----------|-----------|
-| Plan 格式 | 纯文本 (plan.md) | 结构化 (GoalTree JSON) |
-| Plan 与执行关联 | 无 | 通过 goal_id 关联 |
-| 执行记录 | Message List | Message List(加 goal_id 元数据) |
-| Sub-Agent 模型 | SubagentPart(嵌入式) | 独立 Trace(统一模型) |
-| Sub-Agent ID | 复杂(需要前缀) | 简单(每个 Trace 独立编号) |
-| 压缩时机 | 事后(context 满时) | 增量(goal 完成/放弃时) |
-| 并行探索 | Sub-agent(手动管理) | explore 工具(自动汇总) |
-| 权限控制 | 在 Sub-Agent 类型上 | 在 Trace.context 上(更灵活) |
-| 回溯能力 | 有限 | 精确(基于 goal 压缩 + 废弃分支 summary) |
-| 可视化 | 无 | DAG(边可展开/折叠) |
-| 分布式支持 | 困难 | 自然支持(每个 Trace 独立) |
-
----
-
-## 实现位置
-
-| 功能 | 文件路径 | 状态 |
-|------|---------|------|
-| Trace 数据模型 | `agent/execution/models.py` | 待调整(增加父子关系、移除 branch_id) |
-| Goal 数据模型 | `agent/goal/models.py` | 待调整(移除 branch_id、BranchContext) |
-| Trace ID 生成 | `agent/execution/trace_id.py` | 待实现(generate_sub_trace_id) |
-| goal 工具 | `agent/goal/tool.py` | 待调整(新增 after/under 参数) |
-| explore 工具 | `agent/goal/explore.py` | 待实现 |
-| delegate 工具 | `agent/goal/delegate.py` | 待实现 |
-| Context 压缩 | `agent/goal/compaction.py` | 待调整 |
-| TraceStore 协议 | `agent/execution/protocols.py` | 待调整(移除 branch 相关接口) |
-| FileSystem Store | `agent/execution/fs_store.py` | 待调整(移除 branches/ 目录) |
-| DAG 可视化 API | `agent/execution/api.py` | 待调整(支持 Sub-Trace) |
-| WebSocket 推送 | `agent/execution/websocket.py` | 待调整(统一事件格式) |
-| Plan 注入 | `agent/core/runner.py` | 待调整 |
-
----
-
-## 渐进式实现计划
-
-### Phase 1: 基础 goal 工具
-- GoalTree 数据结构(含 ID 映射)
-- goal 工具(add, after, under, done, focus)
-- 位置控制逻辑:after(同层级)、under(子任务)
-- Plan 注入到 system prompt(含显示序号生成、完整展示策略)
-- Message 模型(替代 Step)
-
-### Phase 2: 回溯支持
-- abandon 操作(两种模式)
-- 废弃分支 summary 累积到新分支
-- 基于 goal 的 context 压缩
-
-### Phase 3: 可视化
-- DAG 视图 API
-- WebSocket goal/message 事件
-- 展开/折叠逻辑
-
-### Phase 4: 并行探索
-- explore 工具
-- 独立 message list 管理
-- 结果汇总机制

+ 33 - 0
docs/decisions.md

@@ -634,3 +634,36 @@ execution trace v2.0 引入了 Blob 存储系统用于处理大输出和图片
 3. **命名准确**:meta 比 context 更准确表达"静态元信息"
 3. **命名准确**:meta 比 context 更准确表达"静态元信息"
 
 
 ---
 ---
+
+## Subagent 工具设计
+
+### 决策
+
+**统一工具,三种模式**:
+- 单一工具 `subagent` 支持三种模式:`explore`(并行探索)、`delegate`(委托执行)、`evaluate`(结果评估)
+- 实现位置:`agent/tools/builtin/subagent.py`
+
+**Explore 模式的并行执行**:
+- 使用 `asyncio.gather()` 实现真并行
+- 每个 branch 创建独立的 Sub-Trace
+- 仅允许只读工具(`read_file`, `grep_content`, `glob_files`)
+
+**权限隔离**:
+- Explore 模式:只读权限,防止副作用
+- Delegate/Evaluate 模式:除 `subagent` 外的所有工具
+- 子 Agent 不能调用 `subagent`,防止无限递归
+
+**Sub-Trace 信息存储**:
+- Sub-Trace 的元信息存储在自己的 `meta.json` 中
+- 父 Trace 的 `events.jsonl` 记录 `sub_trace_started` 和 `sub_trace_completed` 事件
+- Goal 的 `sub_trace_ids` 字段记录关联关系
+
+### 理由
+
+1. **统一接口**:三种模式共享相同的 Sub-Trace 创建和管理逻辑,减少代码重复
+2. **真并行**:Explore 模式使用 `asyncio.gather()` 充分利用 I/O 等待时间,提升效率
+3. **安全性**:只读权限确保探索不会修改系统状态;禁止递归调用防止资源耗尽
+4. **可追溯**:事件记录和 Goal 关联确保完整的执行历史,支持可视化和调试
+
+---
+

+ 41 - 0
docs/tools.md

@@ -712,6 +712,47 @@ print(f"Success rate: {stats['success_rate']:.1%}")
 | `bash_command` | 执行 shell 命令 | opencode bash.ts |
 | `bash_command` | 执行 shell 命令 | opencode bash.ts |
 | `glob_files` | 文件模式匹配 | opencode glob.ts |
 | `glob_files` | 文件模式匹配 | opencode glob.ts |
 | `grep_content` | 内容搜索(正则表达式) | opencode grep.ts |
 | `grep_content` | 内容搜索(正则表达式) | opencode grep.ts |
+| `subagent` | 统一子 Agent 调用(evaluate/delegate/explore) | main 自研 |
+
+### Subagent 工具
+
+创建子 Agent 执行任务,支持三种模式:
+
+| 模式 | 用途 | 并行执行 | 工具权限 |
+|------|------|---------|---------|
+| **explore** | 并行探索多个方案 | ✅ | 只读(read_file, grep_content, glob_files) |
+| **delegate** | 委托单个任务 | ❌ | 完整(除 subagent 外) |
+| **evaluate** | 评估任务结果 | ❌ | 完整(除 subagent 外) |
+
+**Explore 模式**:
+- 适合对比多个方案(如技术选型、架构设计)
+- 使用 `asyncio.gather()` 并行执行,显著提升效率
+- 每个分支创建独立的 Sub-Trace,互不干扰
+- 只读权限,防止副作用
+
+**Delegate 模式**:
+- 适合委托专门任务(如代码分析、文档生成)
+- 完整工具权限,可执行复杂操作
+- 支持 `continue_from` 参数继续执行
+
+**Evaluate 模式**:
+- 适合评估任务完成质量
+- 可访问目标 Goal 的执行结果
+- 提供评估结论和改进建议
+
+**Sub-Trace 结构**:
+- 每个 subagent 调用创建独立的 Sub-Trace
+- Sub-Trace ID 格式:`{parent_id}@{mode}-{序号}-{timestamp}-001`
+- 通过 `parent_trace_id` 和 `parent_goal_id` 建立父子关系
+- Sub-Trace 信息存储在独立的 trace 目录中
+
+**Goal 集成**:
+- Subagent 调用会将 Goal 标记为 `type: "agent_call"`
+- `agent_call_mode` 记录使用的模式
+- `sub_trace_ids` 记录所有创建的 Sub-Trace
+- Goal 完成后,`summary` 包含格式化的汇总结果
+
+**实现位置**:`agent/tools/builtin/subagent.py`
 
 
 ### 快速使用
 ### 快速使用
 
 

+ 23 - 0
examples/integration_real_env/README.md

@@ -0,0 +1,23 @@
+# 真实环境集成测试(Agent-main)
+
+该测试用于在真实 `conda` 环境中验证 `Agent-main` 的核心功能链路是否正常。
+
+## 覆盖范围
+
+1. 工具注册链路(含 browser 工具注册可见性)
+2. 文件工具链:`write_file` / `read_file` / `edit_file` / `glob_files` / `grep_content`
+3. 命令工具:`bash_command`
+4. `AgentRunner.call`
+5. `AgentRunner.run`
+6. 统一 `subagent`:`delegate` / `explore` / `evaluate` / `continue_from`
+
+## 运行
+
+```bash
+BROWSER_USE_CONFIG_DIR=/tmp/browseruse-test conda run -n Agent \
+  python Agent-main/examples/integration_real_env/run.py
+```
+
+说明:
+- 测试默认使用内置 mock LLM,不依赖外部 API Key。
+- 目的是验证真实环境中的框架链路、工具执行与 trace 行为。

+ 320 - 0
examples/integration_real_env/run.py

@@ -0,0 +1,320 @@
+"""
+真实环境集成测试(Agent-main)。
+"""
+
+import asyncio
+import json
+import os
+import sys
+from dataclasses import dataclass
+from pathlib import Path
+from tempfile import TemporaryDirectory
+from typing import Any, Dict, List
+
+
+# 避免 browser_use 在受限环境写 ~/.config 触发权限错误
+os.environ.setdefault("BROWSER_USE_CONFIG_DIR", "/tmp/browseruse-test")
+
+PROJECT_ROOT = Path(__file__).resolve().parents[2]
+sys.path.insert(0, str(PROJECT_ROOT))
+
+
+@dataclass
+class CheckResult:
+    name: str
+    ok: bool
+    detail: str
+
+
+def record(results: List[CheckResult], name: str, ok: bool, detail: str) -> None:
+    results.append(CheckResult(name=name, ok=ok, detail=detail))
+    mark = "PASS" if ok else "FAIL"
+    print(f"[{mark}] {name}: {detail}")
+
+
+async def mock_llm_call(messages, model="gpt-4o", tools=None, **kwargs):
+    """
+    测试专用 mock LLM:
+    - 当有工具可用时:第一轮触发 bash_command,第二轮返回文本结论
+    - 当是 subagent 任务时:按 prompt 类型返回固定文本
+    """
+    state = kwargs.get("_test_state")
+    if isinstance(state, dict):
+        call_no = state.get("call_no", 0)
+        state["call_no"] = call_no + 1
+    else:
+        call_no = 0
+
+    last_user = ""
+    for msg in reversed(messages):
+        if msg.get("role") == "user":
+            last_user = str(msg.get("content", ""))
+            break
+
+    if "# 评估任务" in last_user:
+        return {
+            "content": "## 评估结论\n通过\n\n## 评估理由\n结果满足要求。",
+            "tool_calls": None,
+            "prompt_tokens": 10,
+            "completion_tokens": 10,
+            "finish_reason": "stop",
+            "cost": 0.0,
+        }
+
+    if "# 探索任务" in last_user:
+        return {
+            "content": "探索结论:优先采用方案 1。",
+            "tool_calls": None,
+            "prompt_tokens": 10,
+            "completion_tokens": 10,
+            "finish_reason": "stop",
+            "cost": 0.0,
+        }
+
+    if "委托" in last_user or "实现" in last_user or "继续" in last_user or "优化" in last_user:
+        return {
+            "content": "委托任务执行完成。",
+            "tool_calls": None,
+            "prompt_tokens": 10,
+            "completion_tokens": 10,
+            "finish_reason": "stop",
+            "cost": 0.0,
+        }
+
+    if call_no == 0 and tools:
+        return {
+            "content": "",
+            "tool_calls": [
+                {
+                    "id": "tc_1",
+                    "type": "function",
+                    "function": {
+                        "name": "bash_command",
+                        "arguments": json.dumps(
+                            {
+                                "command": "echo runner_run_ok",
+                                "description": "integration",
+                            }
+                        ),
+                    },
+                }
+            ],
+            "prompt_tokens": 12,
+            "completion_tokens": 8,
+            "finish_reason": "tool_calls",
+            "cost": 0.0,
+        }
+
+    return {
+        "content": "run_fallback_ok",
+        "tool_calls": None,
+        "prompt_tokens": 8,
+        "completion_tokens": 6,
+        "finish_reason": "stop",
+        "cost": 0.0,
+    }
+
+
+def check_tool_registry(results: List[CheckResult]) -> None:
+    from agent.tools import get_tool_registry
+
+    registry = get_tool_registry()
+    names = set(registry.get_tool_names())
+
+    core_required = {
+        "read_file",
+        "edit_file",
+        "write_file",
+        "glob_files",
+        "grep_content",
+        "bash_command",
+        "skill",
+        "list_skills",
+        "subagent",
+    }
+
+    core_missing = sorted(core_required - names)
+    record(
+        results,
+        "tool_registry_core",
+        len(core_missing) == 0,
+        "all core tools registered" if not core_missing else f"missing: {core_missing}",
+    )
+
+    browser_subset = {
+        "browser_search_web",
+        "browser_navigate_to_url",
+        "browser_screenshot",
+    }
+    browser_missing = sorted(browser_subset - names)
+    record(
+        results,
+        "tool_registry_browser",
+        len(browser_missing) == 0,
+        "browser tools visible" if not browser_missing else f"missing: {browser_missing}",
+    )
+
+
+async def check_file_tools(results: List[CheckResult]) -> None:
+    from agent.tools.builtin.file.write import write_file
+    from agent.tools.builtin.file.read import read_file
+    from agent.tools.builtin.file.edit import edit_file
+    from agent.tools.builtin.file.glob import glob_files
+    from agent.tools.builtin.file.grep import grep_content
+    from agent.tools.builtin.bash import bash_command
+
+    with TemporaryDirectory(prefix="agent-main-int-") as tmp:
+        tmp_path = Path(tmp)
+        target = tmp_path / "notes.txt"
+
+        wr = await write_file(file_path=str(target), content="hello\npython\nagent\n")
+        record(results, "write_file", wr.error is None, wr.error or "write success")
+
+        rd = await read_file(file_path=str(target))
+        read_ok = (rd.error is None) and ("python" in rd.output)
+        record(results, "read_file", read_ok, rd.error or "content contains python")
+
+        ed = await edit_file(file_path=str(target), old_string="python", new_string="python3")
+        record(results, "edit_file", ed.error is None, ed.error or "edit success")
+
+        gp = await grep_content(pattern="python3", path=str(tmp_path))
+        grep_ok = gp.error is None and "notes.txt" in gp.output
+        record(results, "grep_content", grep_ok, gp.error or "pattern found")
+
+        gb = await glob_files(pattern="**/*.txt", path=str(tmp_path))
+        glob_ok = gb.error is None and "notes.txt" in gb.output
+        record(results, "glob_files", glob_ok, gb.error or "glob matched")
+
+        bs = await bash_command(
+            command="echo integration_ok",
+            description="integration test",
+            workdir=str(tmp_path),
+        )
+        bash_ok = bs.error is None and "integration_ok" in bs.output
+        record(results, "bash_command", bash_ok, bs.error or "command output ok")
+
+
+async def check_runner(results: List[CheckResult]) -> None:
+    from agent.core.runner import AgentRunner
+    from agent.trace.store import FileSystemTraceStore
+    from agent.trace.models import Trace, Message
+
+    with TemporaryDirectory(prefix="agent-main-runner-") as tmp:
+        store = FileSystemTraceStore(base_path=tmp)
+
+        # call 模式
+        runner_call = AgentRunner(trace_store=store, llm_call=mock_llm_call)
+        call_result = await runner_call.call(messages=[{"role": "user", "content": "ping"}], trace=True)
+        call_ok = bool(call_result.trace_id) and isinstance(call_result.reply, str)
+        record(results, "runner_call", call_ok, f"trace_id={call_result.trace_id}, reply={call_result.reply}")
+
+        # run 模式(含工具调用)
+        state = {"call_no": 0}
+
+        async def llm_with_state(messages, model="gpt-4o", tools=None, **kwargs):
+            kwargs["_test_state"] = state
+            return await mock_llm_call(messages=messages, model=model, tools=tools, **kwargs)
+
+        runner_run = AgentRunner(trace_store=store, llm_call=llm_with_state)
+        events: List[Any] = []
+        async for item in runner_run.run(
+            task="请执行一次bash并给出结果",
+            system_prompt="你是测试助手",
+            model="gpt-4o-mini",
+        ):
+            events.append(item)
+
+        final_trace = None
+        assistant_texts = []
+        for item in events:
+            if isinstance(item, Trace):
+                final_trace = item
+            if isinstance(item, Message) and item.role == "assistant":
+                content = item.content
+                text = content.get("text", "") if isinstance(content, dict) else str(content)
+                if text:
+                    assistant_texts.append(text)
+
+        run_ok = bool(final_trace) and final_trace.status == "completed" and "run_fallback_ok" in assistant_texts
+        record(
+            results,
+            "runner_run",
+            run_ok,
+            f"status={getattr(final_trace, 'status', 'n/a')}, assistant_count={len(assistant_texts)}",
+        )
+
+
+async def check_subagent(results: List[CheckResult]) -> None:
+    from agent.core.runner import AgentRunner
+    from agent.trace.store import FileSystemTraceStore
+    from agent.trace.models import Trace
+    from agent.trace.goal_models import GoalTree
+    from agent.tools.builtin.subagent import subagent
+
+    with TemporaryDirectory(prefix="agent-main-subagent-") as tmp:
+        store = FileSystemTraceStore(base_path=tmp)
+        runner = AgentRunner(trace_store=store, llm_call=mock_llm_call)
+
+        main_trace = Trace(
+            trace_id="main-trace",
+            mode="agent",
+            task="主任务",
+            agent_type="default",
+            status="running",
+        )
+        await store.create_trace(main_trace)
+        goal_tree = GoalTree(mission="主任务")
+        goals = goal_tree.add_goals(["验证 subagent 功能"])
+        goal_tree.focus(goals[0].id)
+        await store.update_goal_tree(main_trace.trace_id, goal_tree)
+
+        ctx = {"store": store, "trace_id": main_trace.trace_id, "goal_id": goals[0].id, "runner": runner}
+
+        r1 = await subagent(mode="delegate", task="实现登录", context=ctx)
+        r2 = await subagent(mode="explore", branches=["方案A", "方案B"], background="请比较", context=ctx)
+        r3 = await subagent(
+            mode="evaluate",
+            target_goal_id=goals[0].id,
+            evaluation_input={"actual_result": "实现完成"},
+            requirements="给出是否通过",
+            context=ctx,
+        )
+        r4 = await subagent(mode="delegate", task="继续优化", continue_from=r1["sub_trace_id"], context=ctx)
+
+        s1 = str(r1.get("status", "")).strip()
+        s2 = str(r2.get("status", "")).strip()
+        s3 = str(r3.get("status", "")).strip()
+        s4 = str(r4.get("status", "")).strip()
+        same_trace = str(r4.get("sub_trace_id", "")).strip() == str(r1.get("sub_trace_id", "")).strip()
+        ok = (s1 == "completed" and s2 == "completed" and s3 == "completed" and s4 == "completed" and same_trace)
+        detail = (
+            f"delegate={s1}, explore={s2}, evaluate={s3}, continue={s4}, continue_same={same_trace}"
+        )
+        record(results, "subagent_unified", ok, detail)
+
+
+async def main() -> int:
+    results: List[CheckResult] = []
+
+    try:
+        check_tool_registry(results)
+        await check_file_tools(results)
+        await check_runner(results)
+        await check_subagent(results)
+    except Exception as exc:
+        record(results, "unexpected_exception", False, repr(exc))
+
+    total = len(results)
+    passed = sum(1 for r in results if r.ok)
+    failed = total - passed
+
+    print("\n=== Integration Summary ===")
+    print(f"Total: {total}")
+    print(f"Passed: {passed}")
+    print(f"Failed: {failed}")
+
+    return 0 if failed == 0 else 1
+
+
+if __name__ == "__main__":
+    raise SystemExit(asyncio.run(main()))

+ 75 - 26
examples/research/run.py

@@ -1,7 +1,7 @@
 """
 """
-浏览器调研示例
+创意写作调研示例
 
 
-使用 Agent 模式 + 浏览器工具进行网络调研
+使用 Agent 模式 + explore 工具进行创意内容探索
 """
 """
 
 
 import os
 import os
@@ -33,12 +33,8 @@ async def main():
     output_dir = base_dir / "output"
     output_dir = base_dir / "output"
     output_dir.mkdir(exist_ok=True)
     output_dir.mkdir(exist_ok=True)
 
 
-    # Skills 目录(可选:用户自定义 skills)
-    # 注意:内置 skills(agent/skills/core.md)会自动加载
-    skills_dir = None  # 或者指定自定义 skills 目录,如: project_root / "skills"
-
     print("=" * 60)
     print("=" * 60)
-    print("浏览器调研任务 (Agent 模式)")
+    print("创意写作调研 (Agent 模式)")
     print("=" * 60)
     print("=" * 60)
     print()
     print()
 
 
@@ -59,17 +55,20 @@ async def main():
     print("2. 构建任务消息...")
     print("2. 构建任务消息...")
     messages = prompt.build_messages()
     messages = prompt.build_messages()
 
 
-    # 3. 创建 Agent Runner(配置 skills 和浏览器工具)
+    # 3. 创建 Agent Runner
     print("3. 创建 Agent Runner...")
     print("3. 创建 Agent Runner...")
-    print(f"   - Skills 目录: {skills_dir}")
     print(f"   - 模型: {model_name} (via OpenRouter)")
     print(f"   - 模型: {model_name} (via OpenRouter)")
 
 
-    # 使用 OpenRouter 的 Gemini 模型
+    # Trace 输出到测试目录
+    trace_dir = base_dir / ".trace"
+    trace_dir.mkdir(exist_ok=True)
+    print(f"   - Trace 目录: {trace_dir}")
+
     runner = AgentRunner(
     runner = AgentRunner(
-        trace_store=FileSystemTraceStore(base_path=".trace"),
+        trace_store=FileSystemTraceStore(base_path=str(trace_dir)),
         llm_call=create_openrouter_llm_call(model=f"google/{model_name}"),
         llm_call=create_openrouter_llm_call(model=f"google/{model_name}"),
-        skills_dir=skills_dir,
-        debug=True  # 启用 debug,输出到 .trace/
+        skills_dir=None,
+        debug=True
     )
     )
 
 
     # 4. Agent 模式执行
     # 4. Agent 模式执行
@@ -78,6 +77,7 @@ async def main():
 
 
     final_response = ""
     final_response = ""
     current_trace_id = None
     current_trace_id = None
+    subagent_calls = []
 
 
     async for item in runner.run(
     async for item in runner.run(
         task=user_task,
         task=user_task,
@@ -85,9 +85,9 @@ async def main():
         system_prompt=system_prompt,
         system_prompt=system_prompt,
         model=f"google/{model_name}",
         model=f"google/{model_name}",
         temperature=temperature,
         temperature=temperature,
-        max_iterations=20,  # 调研任务可能需要更多迭代
+        max_iterations=30,  # 增加迭代次数以支持多个 subagent 调用
     ):
     ):
-        # 处理 Trace 对象(整体状态变化)
+        # 处理 Trace 对象
         if isinstance(item, Trace):
         if isinstance(item, Trace):
             current_trace_id = item.trace_id
             current_trace_id = item.trace_id
             if item.status == "running":
             if item.status == "running":
@@ -100,7 +100,7 @@ async def main():
             elif item.status == "failed":
             elif item.status == "failed":
                 print(f"[Trace] 失败: {item.error_message}")
                 print(f"[Trace] 失败: {item.error_message}")
 
 
-        # 处理 Message 对象(执行过程)
+        # 处理 Message 对象
         elif isinstance(item, Message):
         elif isinstance(item, Message):
             if item.role == "assistant":
             if item.role == "assistant":
                 content = item.content
                 content = item.content
@@ -109,7 +109,6 @@ async def main():
                     tool_calls = content.get("tool_calls")
                     tool_calls = content.get("tool_calls")
 
 
                     if text and not tool_calls:
                     if text and not tool_calls:
-                        # 纯文本回复(最终响应)
                         final_response = text
                         final_response = text
                         print(f"[Response] Agent 完成")
                         print(f"[Response] Agent 完成")
                     elif text:
                     elif text:
@@ -120,6 +119,23 @@ async def main():
                             tool_name = tc.get("function", {}).get("name", "unknown")
                             tool_name = tc.get("function", {}).get("name", "unknown")
                             print(f"[Tool Call] {tool_name}")
                             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":
             elif item.role == "tool":
                 content = item.content
                 content = item.content
                 if isinstance(content, dict):
                 if isinstance(content, dict):
@@ -138,25 +154,58 @@ async def main():
     print("=" * 60)
     print("=" * 60)
     print()
     print()
 
 
-    # 6. 保存结果
-    output_file = output_dir / "research_result.txt"
+    # 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:
     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(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(f"✓ 结果已保存到: {output_file}")
     print()
     print()
 
 
-    # 提示使用 API 可视化
+    # 8. 可视化提示
     print("=" * 60)
     print("=" * 60)
-    print("可视化 Step Tree:")
+    print("Trace 信息:")
     print("=" * 60)
     print("=" * 60)
-    print("1. 启动 API Server:")
-    print("   python3 api_server.py")
+    print(f"Trace ID: {current_trace_id}")
+    print(f"Trace 目录: {trace_dir}")
     print()
     print()
-    print("2. 浏览器访问:")
-    print("   http://localhost:8000/api/traces")
+    print("查看 trace 文件:")
+    print(f"   ls -la {trace_dir}")
     print()
     print()
-    print(f"3. Trace ID: {current_trace_id}")
+    print("或启动 API Server 可视化:")
+    print("   python3 api_server.py")
+    print("   访问: http://localhost:8000/api/traces")
     print("=" * 60)
     print("=" * 60)
 
 
 
 

+ 7 - 2
examples/research/test.prompt

@@ -7,5 +7,10 @@ $system$
 你是最顶尖的AI助手,可以拆分并调用工具逐步解决复杂问题。
 你是最顶尖的AI助手,可以拆分并调用工具逐步解决复杂问题。
 
 
 $user$
 $user$
-使用浏览器帮我做个调研:一张图片中的构图可以如何表示?我希望寻找一些构图特征的表示方法。
-注意使用explore工具,在合适的时候调用多个分支并行探索。
+请为"一个人在雨夜独自等车"这个场景,创作三种不同风格的短篇描写。
+使用 subagent 的 explore 模式,并行探索以下三个方向:
+1. 悬疑惊悚风格
+2. 温馨治愈风格
+3. 科幻未来风格
+
+每个方向写 100-150 字的场景描写即可。

+ 16 - 0
examples/subagent_unified/README.md

@@ -0,0 +1,16 @@
+# Unified Subagent 测试
+
+本目录用于验证 main 分支新增的统一 `subagent` 工具能力:
+
+1. `mode="delegate"`
+2. `mode="explore"`
+3. `mode="evaluate"`
+4. `continue_from`
+
+## 运行
+
+```bash
+python examples/subagent_unified/run.py
+```
+
+脚本使用 mock LLM,不依赖外部 API Key。

+ 126 - 0
examples/subagent_unified/run.py

@@ -0,0 +1,126 @@
+"""
+统一 subagent 工具集成测试(mock LLM)。
+"""
+
+import asyncio
+import os
+import sys
+from pathlib import Path
+from tempfile import TemporaryDirectory
+
+sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
+
+from agent.core.runner import AgentRunner
+from agent.trace.store import FileSystemTraceStore
+from agent.trace.models import Trace
+from agent.trace.goal_models import GoalTree
+from agent.tools.builtin.subagent import subagent
+
+
+async def mock_llm_call(messages, model="gpt-4o", tools=None, **kwargs):
+    last_user = ""
+    for msg in reversed(messages):
+        if msg.get("role") == "user":
+            last_user = str(msg.get("content", ""))
+            break
+
+    if "# 评估任务" in last_user:
+        content = "## 评估结论\n通过\n\n## 评估理由\n满足需求。"
+    elif "# 探索任务" in last_user:
+        content = "探索完成:建议优先采用方案 1。"
+    else:
+        content = "委托任务已完成。"
+
+    return {
+        "content": content,
+        "tool_calls": None,
+        "finish_reason": "stop",
+        "prompt_tokens": 10,
+        "completion_tokens": 10,
+        "cost": 0.0,
+    }
+
+
+async def run_case():
+    with TemporaryDirectory(prefix="subagent-unified-") as tmp_dir:
+        store = FileSystemTraceStore(base_path=tmp_dir)
+        runner = AgentRunner(trace_store=store, llm_call=mock_llm_call)
+
+        # 创建主 Trace 与 GoalTree(供 subagent 作为父上下文)
+        main_trace = Trace(
+            trace_id="main-trace",
+            mode="agent",
+            task="主任务",
+            agent_type="default",
+            status="running",
+        )
+        await store.create_trace(main_trace)
+        goal_tree = GoalTree(mission="主任务")
+        new_goals = goal_tree.add_goals(["实现主流程"])
+        goal_tree.focus(new_goals[0].id)
+        await store.update_goal_tree(main_trace.trace_id, goal_tree)
+
+        context = {
+            "store": store,
+            "trace_id": main_trace.trace_id,
+            "goal_id": new_goals[0].id,
+            "runner": runner,
+        }
+
+        # 1) delegate
+        delegate_result = await subagent(
+            mode="delegate",
+            task="实现用户登录功能",
+            context=context,
+        )
+        assert delegate_result["status"] == "completed", delegate_result
+        assert delegate_result["summary"], delegate_result
+        delegate_trace = await store.get_trace(delegate_result["sub_trace_id"])
+        assert delegate_trace is not None
+        assert delegate_trace.parent_trace_id == main_trace.trace_id
+        assert delegate_trace.parent_goal_id == new_goals[0].id
+
+        # 2) explore
+        explore_result = await subagent(
+            mode="explore",
+            branches=["JWT 方案", "Session 方案"],
+            background="请比较维护成本和安全性。",
+            context=context,
+        )
+        assert explore_result["status"] == "completed", explore_result
+        assert "探索" in explore_result["summary"], explore_result
+
+        # 3) evaluate
+        evaluate_result = await subagent(
+            mode="evaluate",
+            target_goal_id=new_goals[0].id,
+            evaluation_input={"actual_result": "已实现登录接口并通过单元测试"},
+            requirements="请评估是否满足安全和可维护性要求。",
+            context=context,
+        )
+        assert evaluate_result["status"] == "completed", evaluate_result
+        assert "评估结论" in evaluate_result["summary"], evaluate_result
+
+        # 4) continue_from
+        continue_result = await subagent(
+            mode="delegate",
+            task="继续补充边界条件处理",
+            continue_from=delegate_result["sub_trace_id"],
+            context=context,
+        )
+        assert continue_result["status"] == "completed", continue_result
+        assert continue_result["sub_trace_id"] == delegate_result["sub_trace_id"]
+        assert continue_result["continue_from"] is True
+
+        print("✅ unified subagent tests passed")
+        print(f"delegate: {delegate_result['sub_trace_id']}")
+        print(f"explore : {explore_result['sub_trace_id']}")
+        print(f"evaluate: {evaluate_result['sub_trace_id']}")
+
+
+def main():
+    asyncio.run(run_case())
+
+
+if __name__ == "__main__":
+    main()

+ 61 - 0
examples/test_subagent_real/README.md

@@ -0,0 +1,61 @@
+# 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

+ 216 - 0
examples/test_subagent_real/run.py

@@ -0,0 +1,216 @@
+"""
+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
+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(
+        task=user_task,
+        messages=messages,
+        system_prompt=system_prompt,
+        model=f"google/{model_name}",
+        temperature=temperature,
+        max_iterations=30,  # 增加迭代次数以支持多个 subagent 调用
+    ):
+        # 处理 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())

+ 28 - 0
examples/test_subagent_real/test.prompt

@@ -0,0 +1,28 @@
+---
+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 工具的各种模式来完成这个任务。

+ 28 - 0
examples/test_subagent_real/test_continue.prompt

@@ -0,0 +1,28 @@
+---
+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 上迭代改进任务。

+ 187 - 0
examples/test_subagent_real/visualize_trace.py

@@ -0,0 +1,187 @@
+"""
+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}")