فهرست منبع

Merge branch 'main' into tanjingyu/dev

tanjingyu 3 هفته پیش
والد
کامیت
e1ae910f80
37فایلهای تغییر یافته به همراه2369 افزوده شده و 2259 حذف شده
  1. 0 3
      .env.template
  2. 176 39
      agent/core/runner.py
  3. 26 28
      agent/memory/skills/core.md
  4. 3 4
      agent/tools/builtin/__init__.py
  5. 108 0
      agent/tools/builtin/glob_tool.py
  6. 615 0
      agent/tools/builtin/subagent.py
  7. 0 176
      agent/trace/delegate.py
  8. 0 248
      agent/trace/explore.py
  9. 1 1
      agent/trace/goal_models.py
  10. 13 7
      agent/trace/goal_tool.py
  11. 9 1
      api_server.py
  12. 75 52
      docs/README.md
  13. 0 1476
      docs/context-management.md
  14. 78 0
      docs/decisions.md
  15. 41 0
      docs/tools.md
  16. 75 26
      examples/research/run.py
  17. 7 2
      examples/research/test.prompt
  18. 61 0
      examples/test_subagent_real/README.md
  19. 216 0
      examples/test_subagent_real/run.py
  20. 28 0
      examples/test_subagent_real/test.prompt
  21. 28 0
      examples/test_subagent_real/test_continue.prompt
  22. 187 0
      examples/test_subagent_real/visualize_trace.py
  23. 17 5
      frontend/API.md
  24. 7 4
      frontend/htmlTemplate/templateData.py
  25. 0 13
      frontend/htmlTemplate/ws_data/event.jsonl
  26. 24 9
      frontend/react-template/src/App.tsx
  27. 91 0
      frontend/react-template/src/components/DetailPanel/DetailPanel.module.css
  28. 64 18
      frontend/react-template/src/components/DetailPanel/DetailPanel.tsx
  29. 264 102
      frontend/react-template/src/components/FlowChart/FlowChart.tsx
  30. 90 17
      frontend/react-template/src/components/FlowChart/components/Edge.tsx
  31. 13 7
      frontend/react-template/src/components/FlowChart/components/Node.tsx
  32. 29 6
      frontend/react-template/src/components/FlowChart/hooks/useFlowChartData.ts
  33. 2 11
      frontend/react-template/src/components/MainContent/MainContent.tsx
  34. 5 3
      frontend/react-template/src/components/TopBar/TopBar.tsx
  35. 3 0
      frontend/react-template/src/types/goal.ts
  36. 12 1
      frontend/react-template/src/types/message.ts
  37. 1 0
      frontend/react-template/src/types/trace.ts

+ 0 - 3
.env.template

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

+ 176 - 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.protocols import TraceStore
 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.protocols import MemoryStore, StateStore
 from agent.memory.skill_loader import load_skills_from_dir
@@ -62,6 +61,7 @@ BUILTIN_TOOLS = [
     "skill",
     "list_skills",
     "goal",
+    "subagent",
 
     # 搜索工具
     "search_posts",
@@ -186,7 +186,6 @@ class AgentRunner:
             for tool in tools:
                 if tool not in tool_names:
                     tool_names.append(tool)
-
         tool_schemas = self.tools.get_schemas(tool_names)
 
         # 创建 Trace
@@ -244,6 +243,78 @@ class AgentRunner:
 
     # ===== 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(
         self,
         task: str,
@@ -256,6 +327,7 @@ class AgentRunner:
         max_iterations: Optional[int] = None,
         enable_memory: Optional[bool] = None,
         auto_execute_tools: Optional[bool] = None,
+        trace_id: Optional[str] = None,
         **kwargs
     ) -> AsyncIterator[Union[Trace, Message]]:
         """
@@ -272,6 +344,7 @@ class AgentRunner:
             max_iterations: 最大迭代次数
             enable_memory: 是否启用记忆
             auto_execute_tools: 是否自动执行工具
+            trace_id: Trace ID(可选,传入时复用已有 Trace)
             **kwargs: 其他参数
 
         Yields:
@@ -294,26 +367,44 @@ class AgentRunner:
                     tool_names.append(tool)
         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(表示开始)
         yield trace_obj
@@ -339,13 +430,34 @@ class AgentRunner:
                     logger.info(f"加载 {len(skills)} 个内置 skills")
 
             # 构建初始消息
+            sequence = 1
             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
-            sequence = 1
 
-            if system_prompt:
+            if system_prompt and not any(m.get("role") == "system" for m in messages):
                 # 注入记忆和 skills 到 system prompt
                 full_system = system_prompt
                 if skills_text:
@@ -368,21 +480,22 @@ class AgentRunner:
                     yield system_msg
                     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
             goal_tree = None
@@ -418,6 +531,25 @@ class AgentRunner:
                 step_tokens = prompt_tokens + completion_tokens
                 step_cost = result.get("cost", 0)
 
+                # 按需自动创建 root goal:LLM 有 tool 调用但未主动创建目标时兜底
+                if goal_tree and not goal_tree.goals and tool_calls:
+                    has_goal_call = any(
+                        tc.get("function", {}).get("name") == "goal"
+                        for tc in tool_calls
+                    )
+                    if not has_goal_call:
+                        root_desc = goal_tree.mission[:200] if len(goal_tree.mission) > 200 else goal_tree.mission
+                        goal_tree.add_goals(
+                            descriptions=[root_desc],
+                            reasons=["系统自动创建:Agent 未显式创建目标"],
+                            parent_id=None
+                        )
+                        goal_tree.focus(goal_tree.goals[0].id)
+                        if self.trace_store:
+                            await self.trace_store.update_goal_tree(trace_id, goal_tree)
+                            await self.trace_store.add_goal(trace_id, goal_tree.goals[0])
+                        logger.info(f"自动创建 root goal: {goal_tree.goals[0].id}")
+
                 # 获取当前 goal_id
                 current_goal_id = goal_tree.current_id if (goal_tree and goal_tree.current_id) else None
 
@@ -451,6 +583,9 @@ class AgentRunner:
                     })
 
                     for tc in tool_calls:
+                        # 每次工具执行前重新获取最新的 goal_id(处理并行 tool_calls 的情况)
+                        current_goal_id = goal_tree.current_id if (goal_tree and goal_tree.current_id) else None
+
                         tool_name = tc["function"]["name"]
                         tool_args = tc["function"]["arguments"]
 
@@ -471,7 +606,9 @@ class AgentRunner:
                             uid=uid or "",
                             context={
                                 "store": self.trace_store,
-                                "trace_id": trace_id
+                                "trace_id": trace_id,
+                                "goal_id": current_goal_id,
+                                "runner": self,
                             }
                         )
 

+ 26 - 28
agent/memory/skills/core.md

@@ -12,65 +12,63 @@ description: 核心系统能力,自动加载到 System Prompt
 
 ## 计划与执行
 
-对于复杂任务,你要先分析需求,并使用 `goal` 工具管理执行计划和进度这一工具会形成一棵目标树。
+使用 `goal` 工具管理执行计划。目标树是你的工作记忆——系统会定期将当前计划注入给你,帮助你追踪进度和关键结论
 
-### 创建计划:拆分任务步骤,创建TODO
+### 核心原则
+
+- **先明确目标再行动**:开始执行前,用 `goal` 明确当前要做什么
+- **灵活运用,不受约束**:
+  - 可以先做全局规划再行动:`goal(add="调研方案, 实现方案, 测试验证")`
+  - 可以走一步看一步,每次只规划下一个目标
+  - 行动中可以动态放弃并调整:`goal(abandon="方案不可行")`
+  - 规划本身可以作为一个目标(如 "调研并确定技术方案")
+- **简单任务只需一个目标**:`goal(add="将CSV转换为JSON")` 即可,不需要强制拆分
+
+### 使用方式
+
+创建目标:
 
 ```
 goal(add="调研并确定方案, 执行方案, 评估结果")
 ```
 
-这将创建3个目标。你可以在执行过程中继续添加子目标。
-
-### 开始执行
-
-聚焦到某个目标开始执行(使用目标的 ID):
+聚焦并开始执行(使用计划视图中的 ID,如 "1", "2.1"):
 
 ```
 goal(focus="1")
 ```
 
-目标的 ID 会显示在计划视图中,格式如 "1", "2", "2.1", "2.2" 等。
-
-### 完成目标
-
-完成当前目标并提供总结:
+完成目标,记录**关键结论**(不是过程描述):
 
 ```
-goal(done="人物姿势的最佳提取工具是openpose")
+goal(done="最佳方案是openpose,精度高且支持多人检测")
 ```
 
-### 完成并切换
-
-先完成当前目标,再切换焦点到下一个:
+完成并切换到下一个:
 
 ```
-goal(done="人物姿势的最佳提取工具是openpose", focus="2")
+goal(done="openpose方案确认可行", focus="2")
 ```
 
-这会先完成当前正在执行的目标,然后切换焦点到目标 "2"。
-
-### 添加子目标
-
-为指定目标添加子目标:
+添加子目标或同级目标:
 
 ```
 goal(add="设计接口, 实现代码", under="2")
+goal(add="编写文档", after="2")
 ```
 
-在指定目标后面添加同级目标:
+放弃不可行的目标:
 
 ```
-goal(add="编写文档", after="2")
+goal(abandon="方案A需要Redis,环境没有")
 ```
 
 ### 使用规范
 
 1. **同时只有一个目标处于执行中**:完成当前目标后再切换
-2. **summary 应简洁**:记录关键结论和发现,不要冗长
-3. **计划可调整**:根据执行情况追加或跳过目标
-4. **简单任务不需要计划**:单步操作直接执行即可
-5. **使用 ID 进行定位**:focus、after、under 参数都使用目标的 ID(如 "1", "2.1")
+2. **summary 记录结论**:记录关键发现,而非 "已完成调研" 这样无信息量的描述
+3. **计划可调整**:根据执行情况随时追加、跳过或放弃目标
+4. **使用 ID 定位**:focus、after、under 参数使用目标的 ID(如 "1", "2.1")
 
 ## 信息调研
 

+ 3 - 4
agent/tools/builtin/__init__.py

@@ -7,16 +7,14 @@
 参考版本:opencode main branch (2025-01)
 """
 
-# 文件操作工具
 from agent.tools.builtin.file.read import read_file
 from agent.tools.builtin.file.edit import edit_file
 from agent.tools.builtin.file.write import write_file
-from agent.tools.builtin.file.glob import glob_files
+from agent.tools.builtin.glob_tool import glob_files
 from agent.tools.builtin.file.grep import grep_content
-
-# 系统工具
 from agent.tools.builtin.bash import bash_command
 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.sandbox import (sandbox_create_environment, sandbox_run_shell,
                                          sandbox_rebuild_with_ports,sandbox_destroy_environment)
@@ -37,6 +35,7 @@ __all__ = [
     "bash_command",
     "skill",
     "list_skills",
+    "subagent",
     "search_posts",
     "get_search_suggestions",
     "sandbox_create_environment",

+ 108 - 0
agent/tools/builtin/glob_tool.py

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

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

@@ -0,0 +1,615 @@
+"""
+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", "goal"]
+    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({
+            "trace_id": sub_trace_id,
+            "mission": branch
+        })
+
+        # 创建 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", "goal"]
+        )
+        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]["trace_id"],
+                "failed", str(result), {}
+            )
+        else:
+            processed_results.append(result)
+            await broadcast_sub_trace_completed(
+                current_trace_id, sub_trace_ids[i]["trace_id"],
+                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
+        # 获取 mission
+        goal_tree = await store.get_goal_tree(continue_from)
+        mission = goal_tree.mission if goal_tree else task
+        sub_trace_ids = [{"trace_id": sub_trace_id, "mission": mission}]
+    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 = [{"trace_id": sub_trace_id, "mission": task}]
+
+        # 广播 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
+        # 获取 mission
+        goal_tree = await store.get_goal_tree(continue_from)
+        mission = goal_tree.mission if goal_tree else task_prompt
+        sub_trace_ids = [{"trace_id": sub_trace_id, "mission": mission}]
+    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 = [{"trace_id": sub_trace_id, "mission": task_prompt}]
+
+        # 广播 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"]
-            }
-        }
-    }

+ 1 - 1
agent/trace/goal_models.py

@@ -62,7 +62,7 @@ class Goal:
     summary: Optional[str] = None            # 完成/放弃时的总结
 
     # agent_call 特有
-    sub_trace_ids: Optional[List[str]] = None      # 启动的 Sub-Trace IDs
+    sub_trace_ids: Optional[List[Dict[str, str]]] = None      # 启动的 Sub-Trace 信息 [{"trace_id": "...", "mission": "..."}]
     agent_call_mode: Optional[str] = None          # "explore" | "delegate" | "sequential"
     sub_trace_metadata: Optional[Dict[str, Dict[str, Any]]] = None  # Sub-Trace 元数据
 

+ 13 - 7
agent/trace/goal_tool.py

@@ -247,15 +247,21 @@ def create_goal_tool_schema() -> dict:
     """创建 goal 工具的 JSON Schema"""
     return {
         "name": "goal",
-        "description": """管理执行计划。
+        "description": """管理执行计划。目标工具是灵活的支持系统,帮助你组织和追踪工作进度。
 
+使用策略(按需选择):
+- 全局规划:先规划所有目标,再逐个执行
+- 渐进规划:走一步看一步,每次只创建下一个目标
+- 动态调整:行动中随时 abandon 不可行的目标,创建新目标
+
+参数:
 - add: 添加目标(逗号分隔多个)
-- reason: 创建理由(逗号分隔多个,与 add 一一对应)。说明为什么要做这些目标。
-- after: 在指定目标后面添加(同层级)。使用目标的 ID。
-- under: 为指定目标添加子目标。使用目标的 ID。如已有子目标,追加到最后。
-- done: 完成当前目标,值为 summary
-- abandon: 放弃当前目标,值为原因(会触发 context 压缩)
-- focus: 切换焦点到指定目标。使用目标 ID。
+- reason: 创建理由(逗号分隔,与 add 一一对应)
+- after: 在指定目标后面添加同级目标。使用目标 ID。
+- under: 为指定目标添加子目标。使用目标 ID。如已有子目标,追加到最后。
+- done: 完成当前目标,值为 summary(记录关键结论)
+- abandon: 放弃当前目标,值为原因
+- focus: 切换焦点到指定目标。使用目标 ID。
 
 位置控制(优先使用 after):
 - 不指定 after/under: 添加到当前 focus 下作为子目标(无 focus 时添加到顶层)

+ 9 - 1
api_server.py

@@ -5,7 +5,9 @@ API Server - FastAPI 应用入口
 """
 
 import logging
-from fastapi import FastAPI
+import json
+import os
+from fastapi import FastAPI, Request, WebSocket
 from fastapi.middleware.cors import CORSMiddleware
 import uvicorn
 
@@ -59,6 +61,12 @@ app.include_router(api_router)
 # Step 树 WebSocket
 app.include_router(ws_router)
 
+@app.websocket("/ws_ping")
+async def ws_ping(websocket: WebSocket):
+    await websocket.accept()
+    await websocket.send_text("pong")
+    await websocket.close()
+
 
 # ===== 健康检查 =====
 

+ 75 - 52
docs/README.md

@@ -1,6 +1,5 @@
 # Agent 功能需求与架构设计文档
 
-
 ## 文档维护规范
 
 0. **先改文档,再动代码** - 新功能或重大修改需先完成文档更新、并完成审阅后,再进行代码实现;除非改动较小、不被文档涵盖
@@ -16,7 +15,7 @@
 | 类型 | 创建方式 | 父子关系 | 状态 |
 |------|---------|---------|------|
 | 主 Agent | 直接调用 `runner.run()` | 无 parent | 正常执行 |
-| 子 Agent | 通过 `task` 工具 | `parent_trace_id` 指向父 | 正常执行 |
+| 子 Agent | 通过 `subagent` 工具 | `parent_trace_id` / `parent_goal_id` 指向父 | 正常执行 |
 | 人类协助 | 通过 `ask_human` 工具 | `parent_trace_id` 指向父 | 阻塞等待 |
 
 ---
@@ -37,8 +36,6 @@ agent/
 │   ├── protocols.py       # TraceStore 接口
 │   ├── store.py           # FileSystemTraceStore 实现
 │   ├── goal_tool.py       # goal 工具(计划管理)
-│   ├── explore.py         # 探索式子任务
-│   ├── delegate.py        # 委派式子任务
 │   ├── compaction.py      # Context 压缩
 │   ├── api.py             # REST API
 │   ├── websocket.py       # WebSocket API
@@ -56,7 +53,7 @@ agent/
 │       ├── search.py      # 网络搜索
 │       ├── webfetch.py    # 网页抓取
 │       ├── skill.py       # 技能加载
-│       └── ask_human.py   # 人类协助
+│       └── subagent.py    # 子 Agent 统一入口(evaluate/delegate/explore)
 ├── memory/                # 跨会话记忆
 │   ├── models.py          # Experience, Skill
@@ -77,7 +74,7 @@ agent/
 | 模块 | 职责 |
 |-----|------|
 | **core/** | Agent 执行引擎 + 预设配置 |
-| **trace/** | 执行追踪 + 计划管理 + 子任务(goal, explore, delegate) |
+| **trace/** | 执行追踪 + 计划管理 |
 | **tools/** | 与外部世界交互(文件、命令、网络、浏览器) |
 | **memory/** | 跨会话知识(Skills、Experiences) |
 | **llm/** | LLM Provider 适配 |
@@ -132,21 +129,29 @@ async def run(task: str, agent_type: str = "default") -> AsyncIterator[Union[Tra
 
     # 4. ReAct 循环
     for step in range(max_iterations):
-        # 注入当前计划
-        plan_text = goal_tree.to_prompt()
+        # 注入当前计划(如果有 goals)
+        if goal_tree.goals:
+            inject_plan(goal_tree.to_prompt())
 
         # 调用 LLM
         response = await llm.chat(
             messages=messages,
-            system=system_prompt + plan_text,
+            system=system_prompt,
             tools=tool_registry.to_schema()
         )
 
-        # 记录 assistant Message
+        # 按需自动创建 root goal:LLM 有 tool 调用但未主动创建目标时兜底
+        if not goal_tree.goals and response.tool_calls:
+            if "goal" not in [tc.name for tc in response.tool_calls]:
+                goal_tree.add_goals([mission[:200]])
+                goal_tree.focus(goal_tree.goals[0].id)
+
+        # 记录 assistant Message(goal_id = goal_tree.current_id)
         await store.add_message(Message.create(
             trace_id=trace.trace_id,
             role="assistant",
             sequence=next_seq,
+            goal_id=goal_tree.current_id,
             content=response
         ))
         yield assistant_msg
@@ -162,6 +167,7 @@ async def run(task: str, agent_type: str = "default") -> AsyncIterator[Union[Tra
                 trace_id=trace.trace_id,
                 role="tool",
                 sequence=next_seq,
+                goal_id=goal_tree.current_id,
                 content=result
             ))
             yield tool_msg
@@ -173,6 +179,13 @@ async def run(task: str, agent_type: str = "default") -> AsyncIterator[Union[Tra
 
 **实现**:`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 +264,7 @@ class Goal:
 
     # agent_call 特有(启动 Sub-Trace)
     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
 
     # 统计
@@ -261,6 +274,17 @@ class Goal:
     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 工具):
 - `add` - 添加顶层目标
 - `under` - 在指定目标下添加子目标
@@ -282,7 +306,7 @@ class Message:
     trace_id: str
     role: Literal["system", "user", "assistant", "tool"]
     sequence: int                            # 全局顺序
-    goal_id: Optional[str] = None            # 关联的 Goal ID
+    goal_id: Optional[str] = None            # 关联的 Goal ID(初始消息为 None,系统会按需自动创建 root goal 兜底)
     description: str = ""                    # 系统自动生成的摘要
     tool_call_id: Optional[str] = None
     content: Any = None
@@ -347,56 +371,43 @@ AGENT_PRESETS = {
 
 ## 子 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, goal)
+- 汇总所有分支结果返回
 
 ### 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 工具
 
 创建阻塞式 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 +436,7 @@ async def my_tool(arg: str, ctx: ToolContext) -> ToolResult:
 | 目录 | 工具 | 说明 |
 |-----|------|------|
 | `trace/` | goal | Agent 内部计划管理 |
-| `trace/` | explore, delegate | 子 Trace 创建 |
+| `builtin/` | subagent | 子 Trace 创建(explore/delegate/evaluate) |
 | `builtin/file/` | read, write, edit, glob, grep | 文件操作 |
 | `builtin/browser/` | browser actions | 浏览器自动化 |
 | `builtin/` | bash, sandbox, search, webfetch, skill, ask_human | 其他工具 |
@@ -559,14 +570,26 @@ class TraceStore(Protocol):
 ├── {trace_id}/
 │   ├── meta.json        # Trace 元数据(含 tools 定义)
 │   ├── goal.json        # GoalTree(mission + goals 列表)
+│   ├── events.jsonl     # 事件流(goal 变更、sub_trace 生命周期等)
 │   └── 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 示例**:
 ```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 管理
-- 结果汇总机制

+ 78 - 0
docs/decisions.md

@@ -634,3 +634,81 @@ execution trace v2.0 引入了 Blob 存储系统用于处理大输出和图片
 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`, `goal`)
+
+**权限隔离**:
+- Explore 模式:文件系统只读权限 + goal 工具,防止副作用
+- 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 关联确保完整的执行历史,支持可视化和调试
+
+---
+
+## Goal 按需自动创建
+
+**日期**: 2026-02-10
+
+### 问题
+
+Agent(含 sub-agent)有时不创建 goal 就直接执行工具调用,导致 message 的 goal_id 为 null。这造成:
+1. 统计信息丢失(`_update_goal_stats` 跳过 null goal_id)
+2. 可视化缺失结构(前端降级为合成 "START" 节点)
+3. LLM 缺少 context 锚点(goals 为空时不注入计划)
+
+### 方案对比
+
+| 方案 | 优点 | 缺点 |
+|------|------|------|
+| **预创建 root goal** | 保证非 null | 复杂任务多一层无意义嵌套,需要溶解逻辑 |
+| **全面接受 null** | 无改动 | 丢失统计、可视化、context 锚点 |
+| **按需自动创建** | 仅在需要时兜底,不干扰正常规划 | 首轮含 goal() 调用时该轮消息仍为 null(可接受) |
+
+### 决策
+
+**选择:按需自动创建 + prompt 引导**
+
+**触发条件**(三个 AND):
+1. `goal_tree.goals` 为空(尚无任何目标)
+2. LLM 返回了 `tool_calls`(正在执行操作)
+3. `tool_calls` 中不包含 `goal()` 调用(LLM 未自行创建目标)
+
+**触发时机**:LLM 返回后、记录消息前(`runner.py` agent loop 中)
+
+**行为**:从 `goal_tree.mission` 截取前 200 字符作为 root goal description,创建并 focus。
+
+**Prompt 配合**:`core.md` 引导 LLM "先明确目标再行动",但不强制。
+
+**实现**:`agent/core/runner.py:AgentRunner.run`
+
+### 理由
+
+1. **不干扰 LLM 自主规划**:LLM 创建目标时,树结构完全由 LLM 控制,无多余嵌套
+2. **兜底覆盖遗漏**:LLM 跳过目标直接行动时,系统自动补位
+3. **实现简单**:无需 `is_auto_root` 标记、溶解逻辑或 display ID 特殊处理
+4. **可接受的 gap**:首轮含 `goal()` 调用时该轮消息 goal_id 为 null,仅影响一轮,属于规划阶段的过渡消息
+
+---
+

+ 41 - 0
docs/tools.md

@@ -712,6 +712,47 @@ print(f"Success rate: {stats['success_rate']:.1%}")
 | `bash_command` | 执行 shell 命令 | opencode bash.ts |
 | `glob_files` | 文件模式匹配 | opencode glob.ts |
 | `grep_content` | 内容搜索(正则表达式) | opencode grep.ts |
+| `subagent` | 统一子 Agent 调用(evaluate/delegate/explore) | main 自研 |
+
+### Subagent 工具
+
+创建子 Agent 执行任务,支持三种模式:
+
+| 模式 | 用途 | 并行执行 | 工具权限 |
+|------|------|---------|---------|
+| **explore** | 并行探索多个方案 | ✅ | 只读(read_file, grep_content, glob_files, goal) |
+| **delegate** | 委托单个任务 | ❌ | 完整(除 subagent 外) |
+| **evaluate** | 评估任务结果 | ❌ | 完整(除 subagent 外) |
+
+**Explore 模式**:
+- 适合对比多个方案(如技术选型、架构设计)
+- 使用 `asyncio.gather()` 并行执行,显著提升效率
+- 每个分支创建独立的 Sub-Trace,互不干扰
+- 只读权限(文件系统层面),可使用 goal 工具管理计划
+
+**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`
 
 ### 快速使用
 

+ 75 - 26
examples/research/run.py

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

+ 7 - 2
examples/research/test.prompt

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

+ 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}")

+ 17 - 5
frontend/API.md

@@ -139,7 +139,10 @@ GET /api/traces/{trace_id}
         "reason": "评估不同技术选型",
         "status": "in_progress",
         "agent_call_mode": "explore",
-        "sub_trace_ids": ["abc123.A", "abc123.B"],
+        "sub_trace_ids": [
+          {"trace_id": "abc123.A", "mission": "JWT 方案"},
+          {"trace_id": "abc123.B", "mission": "Session 方案"}
+        ],
         "sub_trace_metadata": null,
         "self_stats": { "message_count": 0, "total_tokens": 0, "total_cost": 0.0, "preview": null },
         "cumulative_stats": { "message_count": 0, "total_tokens": 0, "total_cost": 0.0, "preview": null }
@@ -208,8 +211,14 @@ GET /api/traces/{trace_id}
         "status": "completed",
         "agent_call_mode": "explore",
         "sub_trace_ids": [
-          "abc123@explore-20260204220012-001",
-          "abc123@explore-20260204220012-002"
+          {
+            "trace_id": "abc123@explore-20260204220012-001",
+            "mission": "JWT 方案"
+          },
+          {
+            "trace_id": "abc123@explore-20260204220012-002",
+            "mission": "Session 方案"
+          }
         ],
         "sub_trace_metadata": {
           "abc123@explore-20260204220012-001": {
@@ -692,7 +701,7 @@ if (data.event === 'sub_trace_completed') {
 | `reason` | string | 创建理由(为什么做)|
 | `status` | string | `pending` / `in_progress` / `completed` / `abandoned` |
 | `summary` | string \| null | 完成/放弃时的总结 |
-| `sub_trace_ids` | string[] \| null | 启动的 Sub-Trace IDs(仅 agent_call)|
+| `sub_trace_ids` | Array<{trace_id: string, mission: string}> \| null | 启动的 Sub-Trace 信息(仅 agent_call)|
 | `agent_call_mode` | string \| null | "explore" / "delegate" / "sequential"(仅 agent_call)|
 | `sub_trace_metadata` | object \| null | Sub-Trace 元数据(仅 agent_call,包含最后消息等)|
 | `self_stats` | GoalStats | 自身统计 |
@@ -1078,7 +1087,10 @@ const mainTrace = {
   goal_tree: {
     goals: [
       { id: "1", type: "normal", description: "分析问题" },
-      { id: "2", type: "agent_call", agent_call_mode: "explore", sub_trace_ids: ["abc123.A", "abc123.B"] },
+      { id: "2", type: "agent_call", agent_call_mode: "explore", sub_trace_ids: [
+        {"trace_id": "abc123.A", "mission": "JWT 方案"},
+        {"trace_id": "abc123.B", "mission": "Session 方案"}
+      ] },
       { id: "3", type: "normal", description: "完善实现" }
     ]
   },

+ 7 - 4
frontend/htmlTemplate/templateData.py

@@ -249,8 +249,10 @@ async def _watch_ws_events(trace_id: str, since_event_id: int = 0, ws_url: Optio
                     event = data.get("event")
                     if event:
                         print(f"收到事件: {event}")
-        except Exception:
-            print("WebSocket 连接断开,1 秒后重连")
+        except Exception as e:
+            import traceback
+            traceback.print_exc()
+            print(f"WebSocket 连接断开: {e},1 秒后重连")
             await asyncio.sleep(1)
 
 
@@ -291,10 +293,11 @@ if __name__ == "__main__":
             # save_ws_data_to_file(args.trace_id, args.since_event_id, args.ws_url)
     else:
         trace_list_data = generate_trace_list()
-        print(f"🐒trace_list_data: {trace_list_data}")
+        # print(f"🐒trace_list_data: {trace_list_data}")
 
         traces = trace_list_data.get("traces") or []
-        trace_id = traces[0].get("trace_id") if traces else None
+        # trace_id = traces[0].get("trace_id") if traces else None
+        trace_id = "eb3aa9f6-37d4-4888-96ba-a9b9c5a4766b"
         if trace_id:
             if args.watch:
                 print(f"✅使用 trace_id 监听: {trace_id}")

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 13
frontend/htmlTemplate/ws_data/event.jsonl


+ 24 - 9
frontend/react-template/src/App.tsx

@@ -1,9 +1,11 @@
-import { useEffect, useRef, useState } from "react";
+import { useEffect, useMemo, useRef, useState } from "react";
 import { TopBar } from "./components/TopBar/TopBar";
 import { MainContent } from "./components/MainContent/MainContent";
 import { DetailPanel } from "./components/DetailPanel/DetailPanel";
 import type { Goal } from "./types/goal";
 import type { Edge } from "./types/message";
+import { useFlowChartData } from "./components/FlowChart/hooks/useFlowChartData";
+import { useTrace } from "./hooks/useTrace";
 import "./styles/global.css";
 
 function App() {
@@ -14,14 +16,14 @@ function App() {
   const [isDragging, setIsDragging] = useState(false);
   const bodyRef = useRef<HTMLDivElement | null>(null);
 
-  const handleNodeClick = (node: Goal) => {
-    setSelectedNode(node);
-    setSelectedEdge(null);
-  };
+  // 获取数据以传递给 DetailPanel
+  const { trace } = useTrace(selectedTraceId);
+  const initialGoals = useMemo(() => trace?.goal_tree?.goals ?? [], [trace]);
+  const { msgGroups } = useFlowChartData(selectedTraceId, initialGoals);
 
-  const handleEdgeClick = (edge: Edge) => {
-    setSelectedEdge(edge);
-    setSelectedNode(null);
+  const handleNodeClick = (node: Goal, edge?: Edge) => {
+    setSelectedNode(node);
+    setSelectedEdge(edge || null);
   };
 
   const handleCloseDetail = () => {
@@ -29,6 +31,19 @@ function App() {
     setSelectedEdge(null);
   };
 
+  // 根据选中的节点获取对应的消息
+  const selectedMessages = useMemo(() => {
+    if (selectedNode) {
+      return msgGroups[selectedNode.id] || [];
+    }
+    // 如果点击的是边,且该边是主链上的边,通常我们显示源节点的消息(如果需要)
+    // 但根据用户需求 "边里面的信息,显示对应msgGroup里面的所有的子集的描述"
+    // 如果 selectedEdge 存在且 selectedNode 为空(虽然目前逻辑是联动),这里可以做处理
+    // 目前 handleNodeClick 会设置 selectedNode 为 link.source
+    // 所以 selectedNode.id 就是边的源节点 ID,直接取 msgGroups 即可
+    return [];
+  }, [selectedNode, msgGroups]);
+
   useEffect(() => {
     if (!isDragging) return;
     const handleMove = (event: MouseEvent) => {
@@ -61,7 +76,6 @@ function App() {
           <MainContent
             traceId={selectedTraceId}
             onNodeClick={handleNodeClick}
-            onEdgeClick={handleEdgeClick}
           />
         </div>
         {(selectedNode || selectedEdge) && (
@@ -82,6 +96,7 @@ function App() {
               <DetailPanel
                 node={selectedNode}
                 edge={selectedEdge}
+                messages={selectedMessages}
                 onClose={handleCloseDetail}
               />
             </div>

+ 91 - 0
frontend/react-template/src/components/DetailPanel/DetailPanel.module.css

@@ -1,5 +1,6 @@
 .panel {
   width: 100%;
+  height: 100%;
   /* border-left: 1px solid var(--border-color, #e0e0e0); */
   background: #ffffff;
   display: flex;
@@ -33,6 +34,25 @@
   flex: 1;
 }
 
+.sectionTitle {
+  font-size: 16px;
+  font-weight: 600;
+  color: #333;
+  margin-bottom: 12px;
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.sectionTitle::before {
+  content: "";
+  display: block;
+  width: 4px;
+  height: 16px;
+  background: #2d72d2;
+  border-radius: 2px;
+}
+
 .section {
   margin-bottom: 16px;
 }
@@ -48,3 +68,74 @@
   color: #333;
   word-break: break-all;
 }
+
+.toolCalls {
+  margin-top: 4px;
+}
+
+.toolCall {
+  background: #f5f5f5;
+  border-radius: 4px;
+  padding: 8px;
+  margin-bottom: 8px;
+}
+
+.toolCall:last-child {
+  margin-bottom: 0;
+}
+
+.toolName {
+  font-size: 12px;
+  font-weight: 600;
+  color: #555;
+  margin-bottom: 4px;
+}
+
+.toolArgs {
+  font-family: monospace;
+  font-size: 11px;
+  color: #666;
+  white-space: pre-wrap;
+  word-break: break-all;
+  margin: 0;
+  background: rgba(0, 0, 0, 0.03);
+  padding: 4px;
+  border-radius: 2px;
+}
+
+.messages {
+  margin-top: 24px;
+  border-top: 1px solid #eee;
+  padding-top: 16px;
+}
+
+.messageList {
+  /* max-height removed to allow full content scrolling */
+}
+
+/* Custom scrollbar for message list */
+.messageList::-webkit-scrollbar {
+  width: 4px;
+}
+
+.messageList::-webkit-scrollbar-thumb {
+  background: #ccc;
+  border-radius: 2px;
+}
+
+.messageList::-webkit-scrollbar-track {
+  background: transparent;
+}
+
+.messageItem {
+  background: #fff;
+  border: 1px solid #eee;
+  border-radius: 8px;
+  padding: 12px;
+  margin-bottom: 12px;
+  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
+}
+
+.messageItem:last-child {
+  margin-bottom: 0;
+}

+ 64 - 18
frontend/react-template/src/components/DetailPanel/DetailPanel.tsx

@@ -1,15 +1,44 @@
 import type { Goal } from "../../types/goal";
-import type { Edge } from "../../types/message";
+import type { Edge, Message } from "../../types/message";
 import styles from "./DetailPanel.module.css";
 
 interface DetailPanelProps {
   node: Goal | null;
   edge: Edge | null;
+  messages?: Message[];
   onClose: () => void;
 }
 
-export const DetailPanel = ({ node, edge, onClose }: DetailPanelProps) => {
+export const DetailPanel = ({ node, edge, messages = [], onClose }: DetailPanelProps) => {
   const title = node ? "节点详情" : edge ? "连线详情" : "详情";
+
+  const renderMessageContent = (content: Message["content"]) => {
+    if (!content) return "";
+    if (typeof content === "string") return content;
+
+    // 如果有 text,优先显示 text
+    if (content.text) return content.text;
+
+    // 如果有 tool_calls,展示 tool_calls 信息
+    if (content.tool_calls && content.tool_calls.length > 0) {
+      return (
+        <div className={styles.toolCalls}>
+          {content.tool_calls.map((call) => (
+            <div
+              key={call.id}
+              className={styles.toolCall}
+            >
+              <div className={styles.toolName}>工具调用: {call.name}</div>
+              <pre className={styles.toolArgs}>{JSON.stringify(call.arguments, null, 2)}</pre>
+            </div>
+          ))}
+        </div>
+      );
+    }
+
+    return JSON.stringify(content);
+  };
+
   return (
     <aside className={styles.panel}>
       <div className={styles.header}>
@@ -25,35 +54,52 @@ export const DetailPanel = ({ node, edge, onClose }: DetailPanelProps) => {
       <div className={styles.content}>
         {node && (
           <>
+            <div className={styles.sectionTitle}>节点</div>
             <div className={styles.section}>
               <div className={styles.label}>ID</div>
               <div className={styles.value}>{node.id}</div>
             </div>
             <div className={styles.section}>
-              <div className={styles.label}>描述</div>
+              <div className={styles.label}>目标描述</div>
               <div className={styles.value}>{node.description}</div>
             </div>
+            {node.reason && (
+              <div className={styles.section}>
+                <div className={styles.label}>创建理由</div>
+                <div className={styles.value}>{node.reason}</div>
+              </div>
+            )}
+            {node.summary && (
+              <div className={styles.section}>
+                <div className={styles.label}>总结</div>
+                <div className={styles.value}>{node.summary}</div>
+              </div>
+            )}
             <div className={styles.section}>
               <div className={styles.label}>状态</div>
               <div className={styles.value}>{node.status}</div>
             </div>
           </>
         )}
-        {edge && (
-          <>
-            <div className={styles.section}>
-              <div className={styles.label}>ID</div>
-              <div className={styles.value}>{edge.id}</div>
-            </div>
-            <div className={styles.section}>
-              <div className={styles.label}>起点</div>
-              <div className={styles.value}>{edge.source}</div>
-            </div>
-            <div className={styles.section}>
-              <div className={styles.label}>终点</div>
-              <div className={styles.value}>{edge.target}</div>
-            </div>
-          </>
+        {messages && messages.length > 0 && (
+          <div className={styles.messages}>
+            <div className={styles.sectionTitle}>边</div>
+            {messages.map((msg, idx) => (
+              <div
+                key={msg.id || idx}
+                className={styles.messageItem}
+              >
+                <div className={styles.section}>
+                  <div className={styles.label}>描述</div>
+                  <div className={styles.value}>{msg.description || "-"}</div>
+                </div>
+                <div className={styles.section}>
+                  <div className={styles.label}>内容</div>
+                  <div className={styles.value}>{renderMessageContent(msg.content)}</div>
+                </div>
+              </div>
+            ))}
+          </div>
         )}
       </div>
     </aside>

+ 264 - 102
frontend/react-template/src/components/FlowChart/FlowChart.tsx

@@ -1,4 +1,4 @@
-import { useEffect, useMemo, useRef, useState } from "react";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
 import type { FC } from "react";
 import * as d3 from "d3";
 import type { Goal } from "../../types/goal";
@@ -14,7 +14,7 @@ interface FlowChartProps {
   goals: Goal[];
   msgGroups?: Record<string, Message[]>;
   onNodeClick?: (node: Goal, edge?: EdgeType) => void;
-  onEdgeClick?: (edge: EdgeType) => void;
+  onSubTraceClick?: (parentGoal: Goal, entry: SubTraceEntry) => void;
 }
 
 interface LayoutNode extends d3.HierarchyPointNode<Goal> {
@@ -24,9 +24,25 @@ interface LayoutNode extends d3.HierarchyPointNode<Goal> {
 
 type TreeGoal = Goal & { children?: TreeGoal[] };
 const VIRTUAL_ROOT_ID = "__VIRTUAL_ROOT__";
+export type SubTraceEntry = { id: string; mission?: string };
 
-export const FlowChart: FC<FlowChartProps> = ({ goals, msgGroups = {}, onNodeClick, onEdgeClick }) => {
-  console.log("%c [ msgGroups ]-29", "font-size:13px; background:pink; color:#bf2c9f;", msgGroups);
+export const FlowChart: FC<FlowChartProps> = ({ goals, msgGroups = {}, onNodeClick, onSubTraceClick }) => {
+  // 确保 goals 中包含 END 节点
+  const displayGoals = useMemo(() => {
+    if (!goals) return [];
+    const hasEnd = goals.some((g) => g.id === "END");
+    if (hasEnd) return goals;
+    const endGoal: Goal = {
+      id: "END",
+      description: "终止",
+      status: "completed",
+      created_at: new Date().toISOString(),
+      reason: "",
+    };
+    return [...goals, endGoal];
+  }, [goals]);
+
+  console.log("%c [ goals ]-28", "font-size:13px; background:pink; color:#bf2c9f;", displayGoals);
   const svgRef = useRef<SVGSVGElement>(null);
   const containerRef = useRef<HTMLDivElement>(null);
   const [dimensions, setDimensions] = useState({ width: 1200, height: 800 });
@@ -82,8 +98,8 @@ export const FlowChart: FC<FlowChartProps> = ({ goals, msgGroups = {}, onNodeCli
 
   // 计算树布局坐标(横向)
   const layoutData = useMemo(() => {
-    if (!goals || goals.length === 0) return null;
-    const root = buildHierarchy(goals);
+    if (!displayGoals || displayGoals.length === 0) return null;
+    const root = buildHierarchy(displayGoals);
     const margin = { top: 60, right: 140, bottom: 60, left: 140 };
     const treeLayout = d3
       .tree<Goal>()
@@ -95,7 +111,7 @@ export const FlowChart: FC<FlowChartProps> = ({ goals, msgGroups = {}, onNodeCli
       links: treeData.links(),
       margin,
     };
-  }, [goals, dimensions]);
+  }, [displayGoals, dimensions]);
 
   // 选中节点到根的路径,用于高亮与弱化
   const pathNodeIds = useMemo(() => {
@@ -115,44 +131,142 @@ export const FlowChart: FC<FlowChartProps> = ({ goals, msgGroups = {}, onNodeCli
     const nonVirtualLinks = layoutData.links.filter(
       (link) => link.source.data.id !== VIRTUAL_ROOT_ID && link.target.data.id !== VIRTUAL_ROOT_ID,
     );
-    if (nonVirtualLinks.length > 0) return nonVirtualLinks;
+    // Remove the early return so we can merge nonVirtualLinks with manual sibling links
+    // if (nonVirtualLinks.length > 0) return nonVirtualLinks;
+
     const nodeMap = new Map(layoutData.nodes.map((node) => [node.data.id, node]));
-    const orderedNodes = goals.map((goal) => nodeMap.get(goal.id)).filter((node): node is LayoutNode => Boolean(node));
+    const orderedNodes = displayGoals
+      .map((goal) => nodeMap.get(goal.id))
+      .filter((node): node is LayoutNode => Boolean(node));
     const links: d3.HierarchyPointLink<Goal>[] = [];
+
+    // 处理主链(Sibling 顺序连接)
     for (let i = 0; i < orderedNodes.length - 1; i += 1) {
-      links.push({
-        source: orderedNodes[i],
-        target: orderedNodes[i + 1],
-      });
+      const current = orderedNodes[i];
+      const next = orderedNodes[i + 1];
+
+      // 如果是 END 节点前的节点,且该节点有子节点(非 END 的子节点),则不直接连接到 END
+      // 而是将子节点连接到 END
+      if (next.data.id === "END" && current.children && current.children.length > 0) {
+        // 过滤掉已经在 mainLinks 中的连接(理论上 current -> children 已经在 nonVirtualLinks 中)
+        // 这里添加 children -> END 的连接
+        current.children.forEach((child) => {
+          // 确保 child 不是 END(虽然 END 此时是 sibling)
+          if (child.data.id !== "END") {
+            links.push({
+              source: child as LayoutNode,
+              target: next,
+            });
+          }
+        });
+        // 不添加 current -> END
+        continue;
+      }
+
+      // 避免重复:如果 nonVirtualLinks 中已经存在 current -> next(即父子关系),则跳过
+      const exists = nonVirtualLinks.some(
+        (l) => l.source.data.id === current.data.id && l.target.data.id === next.data.id,
+      );
+      if (!exists) {
+        links.push({
+          source: current,
+          target: next,
+        });
+      }
     }
-    return links;
-  }, [layoutData, goals]);
 
-  // 节点点击:记录选中状态并回传对应边
-  const handleNodeClick = (node: LayoutNode) => {
-    setSelectedNodeId(node.data.id);
-    const nearestLink = mainLinks.find((link) => link.target.data.id === node.data.id);
-    const edge: EdgeType | undefined = nearestLink
-      ? {
-          id: `${nearestLink.source.data.id}-${nearestLink.target.data.id}`,
-          source: nearestLink.source.data.id,
-          target: nearestLink.target.data.id,
-          label: "",
+    return [...nonVirtualLinks, ...links];
+  }, [layoutData, displayGoals]);
+
+  const normalizeSubTraceEntries = (goal: Goal): SubTraceEntry[] => {
+    const raw = goal.sub_trace_ids || [];
+    return raw
+      .map((item) => {
+        if (typeof item === "string") return { id: item };
+        if (item && typeof item === "object" && "trace_id" in item) {
+          const meta = item as { trace_id?: unknown; mission?: unknown };
+          const id = typeof meta.trace_id === "string" ? meta.trace_id : "";
+          const mission = typeof meta.mission === "string" ? meta.mission : undefined;
+          return id ? { id, mission } : null;
         }
-      : undefined;
-    onNodeClick?.(node.data, edge);
+        return null;
+      })
+      .filter((entry): entry is SubTraceEntry => !!entry && entry.id.length > 0);
   };
 
-  // 边点击:构造 Edge 类型回传
-  const handleEdgeClick = (link: d3.HierarchyPointLink<Goal>) => {
-    const edge: EdgeType = {
-      id: `${link.source.data.id}-${link.target.data.id}`,
-      source: link.source.data.id,
-      target: link.target.data.id,
-      label: "",
-    };
-    onEdgeClick?.(edge);
-  };
+  // 节点点击:记录选中状态并回传对应边
+  const handleNodeClick = useCallback(
+    (node: LayoutNode) => {
+      setSelectedNodeId(node.data.id);
+      const nearestLink = mainLinks.find((link) => link.target.data.id === node.data.id);
+      const edge: EdgeType | undefined = nearestLink
+        ? {
+            id: `${nearestLink.source.data.id}-${nearestLink.target.data.id}`,
+            source: nearestLink.source.data.id,
+            target: nearestLink.target.data.id,
+            label: "",
+          }
+        : undefined;
+      onNodeClick?.(node.data, edge);
+    },
+    [mainLinks, onNodeClick],
+  );
+
+  const subTraceLinks = useMemo(() => {
+    if (!layoutData)
+      return [] as Array<{
+        link: d3.HierarchyPointLink<Goal>;
+        label?: string;
+        key: string;
+        onClick: () => void;
+        curveOffset?: number;
+      }>;
+    const nodeMap = new Map(layoutData.nodes.map((node) => [node.data.id, node]));
+    const orderedIds = displayGoals.map((goal) => goal.id);
+    const nextIdMap = new Map<string, string>();
+    for (let i = 0; i < orderedIds.length - 1; i += 1) {
+      nextIdMap.set(orderedIds[i], orderedIds[i + 1]);
+    }
+
+    const links: Array<{
+      link: d3.HierarchyPointLink<Goal>;
+      label?: string;
+      key: string;
+      onClick: () => void;
+      curveOffset?: number;
+    }> = [];
+
+    displayGoals.forEach((goal) => {
+      const entries = normalizeSubTraceEntries(goal);
+      if (entries.length === 0) return;
+      const sourceBase = nodeMap.get(goal.id);
+      const nextId = nextIdMap.get(goal.id);
+      const targetBase = nextId ? nodeMap.get(nextId) : undefined;
+      if (!sourceBase || !targetBase) return;
+
+      const centerIndex = (entries.length - 1) / 2;
+      entries.forEach((entry, index) => {
+        const offset = (index - centerIndex) * 60;
+        const onClick = () => {
+          if (onSubTraceClick) {
+            onSubTraceClick(goal, entry);
+          } else {
+            handleNodeClick(sourceBase);
+          }
+        };
+
+        links.push({
+          link: { source: sourceBase, target: targetBase },
+          label: entry.mission,
+          key: `${goal.id}-${entry.id}-explore-${index}`,
+          onClick,
+          curveOffset: offset,
+        });
+      });
+    });
+
+    return links;
+  }, [displayGoals, handleNodeClick, layoutData, onSubTraceClick]);
 
   // 当前选中节点的消息链
   const selectedMessages = useMemo(() => {
@@ -165,60 +279,54 @@ export const FlowChart: FC<FlowChartProps> = ({ goals, msgGroups = {}, onNodeCli
     if (!layoutData || !selectedNodeId || selectedMessages.length === 0) return null;
     const anchorNode = layoutData.nodes.find((n) => n.data.id === selectedNodeId);
     if (!anchorNode) return null;
-
-    const startPt = { x: anchorNode.x, y: anchorNode.y };
+    const anchorX = anchorNode.x;
+    const anchorY = anchorNode.y;
     const count = selectedMessages.length;
+    const sides = count + 2;
+    const radius = Math.max(90, 44 + count * 16 + Math.max(0, count - 4) * 6);
+    const centerX = anchorX;
+    const centerY = anchorY + radius;
+    const angleStep = (Math.PI * 2) / sides;
 
-    const nextGoalLink = mainLinks.find((l) => l.source.data.id === selectedNodeId);
-    // 如果没有下一个节点,则虚拟一个终点在右侧,形成自然的抛物线
-    const endPt = nextGoalLink
-      ? { x: nextGoalLink.target.x, y: nextGoalLink.target.y }
-      : { x: startPt.x + 400 + Math.max(0, count - 4) * 80, y: startPt.y };
-
-    // 计算悬链线控制点 (Quadratic Bezier)
-    // 深度随节点数增加,确保不拥挤
-    // 当节点数 > 4 时,额外增加深度以拉开间距
-    const depth = 120 + count * 35 + Math.max(0, count - 4) * 40;
-    const midX = (startPt.x + endPt.x) / 2;
-    const bottomY = Math.max(startPt.y, endPt.y);
-    const controlPt = { x: midX, y: bottomY + depth };
-
-    const getQuadraticBezierPoint = (
-      t: number,
-      p0: { x: number; y: number },
-      p1: { x: number; y: number },
-      p2: { x: number; y: number },
-    ) => {
-      const oneMinusT = 1 - t;
+    const msgNodes = selectedMessages.map((message, index) => {
+      const angle = -Math.PI / 2 - angleStep * (index + 1);
       return {
-        x: oneMinusT * oneMinusT * p0.x + 2 * oneMinusT * t * p1.x + t * t * p2.x,
-        y: oneMinusT * oneMinusT * p0.y + 2 * oneMinusT * t * p1.y + t * t * p2.y,
+        x: centerX + Math.cos(angle) * radius,
+        y: centerY + Math.sin(angle) * radius,
+        message,
       };
+    });
+
+    const nextGoalLink = mainLinks.find((l) => l.source.data.id === selectedNodeId);
+    const nextGoalTarget = nextGoalLink?.target;
+
+    // 节点半高度,用于计算下边界连接点
+    const nodeHalfH = 5;
+
+    const paths: { d: string; dashed?: boolean; terminal?: boolean }[] = [];
+    const buildSegment = (sx: number, sy: number, tx: number, ty: number) => {
+      const controlX = (sx + tx) / 2 - 40;
+      const controlY = Math.max(sy, ty) - 30;
+      return `M${sx},${sy} Q${controlX},${controlY} ${tx},${ty}`;
     };
 
-    // 采样点:起点 -> 消息点... -> 终点
-    const totalSegments = count + 1;
-    const points = [startPt];
-    const msgNodes = [];
-
-    for (let i = 1; i <= count; i += 1) {
-      const t = i / totalSegments;
-      const pos = getQuadraticBezierPoint(t, startPt, controlPt, endPt);
-      msgNodes.push({
-        x: pos.x,
-        y: pos.y,
-        message: selectedMessages[i - 1],
-      });
-      points.push(pos);
+    for (let i = -1; i < msgNodes.length - 1; i += 1) {
+      // 起始点:如果是第一个点(i < 0),则从 anchorNode 的下边界开始 (anchorY + nodeHalfH)
+      // 否则从上一个 msgNode 开始
+      const from = i < 0 ? { x: anchorX, y: anchorY + nodeHalfH } : msgNodes[i];
+      const to = msgNodes[i + 1];
+      const d = buildSegment(from.x, from.y, to.x, to.y);
+      paths.push({ d, dashed: true, terminal: true });
     }
-    points.push(endPt);
 
-    const paths: { d: string; dashed?: boolean; terminal?: boolean }[] = [];
-    for (let i = 0; i < points.length - 1; i += 1) {
-      const p1 = points[i];
-      const p2 = points[i + 1];
-      // 使用直线段连接采样点,形成带箭头的折线链,整体逼近曲线
-      const d = `M${p1.x},${p1.y} L${p2.x},${p2.y}`;
+    if (nextGoalTarget && msgNodes.length > 0) {
+      const last = msgNodes[msgNodes.length - 1];
+      const tx = nextGoalTarget.x;
+      // 结束点:连接到 nextGoalTarget 的下边界 (ty + nodeHalfH)
+      const ty = nextGoalTarget.y + nodeHalfH;
+      const controlX = (last.x + tx) / 2;
+      const controlY = Math.max(last.y, ty) - 2;
+      const d = `M${last.x},${last.y} Q${controlX},${controlY} ${tx},${ty}`;
       paths.push({ d, dashed: true, terminal: true });
     }
 
@@ -303,15 +411,33 @@ export const FlowChart: FC<FlowChartProps> = ({ goals, msgGroups = {}, onNodeCli
                         const dimmed = false;
                         return (
                           <Edge
-                            key={index}
+                            key={`edge-line-${index}`}
                             link={link}
+                            label={link.target.data.reason}
                             highlighted={isInPath}
                             dimmed={dimmed}
-                            onClick={() => handleEdgeClick(link)}
+                            onClick={() => handleNodeClick(link.source)}
+                            mode="line"
                           />
                         );
                       })}
                   </g>
+                  {subTraceLinks.length > 0 && (
+                    <g className={styles.links}>
+                      {subTraceLinks.map((item) => (
+                        <Edge
+                          key={`subtrace-line-${item.key}`}
+                          link={item.link}
+                          label={item.label}
+                          highlighted={false}
+                          dimmed={false}
+                          onClick={item.onClick}
+                          mode="line"
+                          curveOffset={item.curveOffset}
+                        />
+                      ))}
+                    </g>
+                  )}
                   <g className={styles.nodes}>
                     {layoutData.nodes
                       .filter((node) => node.data.id !== VIRTUAL_ROOT_ID)
@@ -330,6 +456,46 @@ export const FlowChart: FC<FlowChartProps> = ({ goals, msgGroups = {}, onNodeCli
                         );
                       })}
                   </g>
+                  <g className={styles.links}>
+                    {mainLinks
+                      .filter(
+                        (link) => link.source.data.id !== VIRTUAL_ROOT_ID && link.target.data.id !== VIRTUAL_ROOT_ID,
+                      )
+                      .map((link, index) => {
+                        const isInPath =
+                          pathNodeIds.size > 0 &&
+                          pathNodeIds.has(link.source.data.id) &&
+                          pathNodeIds.has(link.target.data.id);
+                        const dimmed = false;
+                        return (
+                          <Edge
+                            key={`edge-label-${index}`}
+                            link={link}
+                            label={link.target.data.reason}
+                            highlighted={isInPath}
+                            dimmed={dimmed}
+                            onClick={() => handleNodeClick(link.source)}
+                            mode="label"
+                          />
+                        );
+                      })}
+                  </g>
+                  {subTraceLinks.length > 0 && (
+                    <g className={styles.links}>
+                      {subTraceLinks.map((item) => (
+                        <Edge
+                          key={`subtrace-label-${item.key}`}
+                          link={item.link}
+                          label={item.label}
+                          highlighted={false}
+                          dimmed={false}
+                          onClick={item.onClick}
+                          mode="label"
+                          curveOffset={item.curveOffset}
+                        />
+                      ))}
+                    </g>
+                  )}
                   {messageOverlay && (
                     <g>
                       {messageOverlay.paths.map((p, idx) => (
@@ -347,28 +513,24 @@ export const FlowChart: FC<FlowChartProps> = ({ goals, msgGroups = {}, onNodeCli
                       ))}
                       {messageOverlay.msgNodes.map((mn, idx) =>
                         (() => {
-                          const fullText = mn.message.description || mn.message.content || "";
-                          const shortText = fullText.length > 6 ? `${fullText.slice(0, 6)}...` : fullText;
-                          const isLast = idx === messageOverlay.msgNodes.length - 1;
+                          const content = mn.message.content;
+                          let contentStr = "";
+                          if (typeof content === "string") {
+                            contentStr = content;
+                          } else if (content) {
+                            contentStr = content.text || JSON.stringify(content);
+                          }
+                          const fullText = mn.message.description || contentStr || "";
+                          const truncateMiddle = (text: string, limit: number) => {
+                            if (text.length <= limit) return text;
+                            return `${text.slice(0, 2)}...${text.slice(-2)}`;
+                          };
+                          const shortText = truncateMiddle(fullText, 6);
                           return (
                             <g
                               key={`msg-node-${idx}`}
                               transform={`translate(${mn.x},${mn.y})`}
                             >
-                              {isLast ? (
-                                <path
-                                  d="M-2,-4 L6,0 L-2,4 Z"
-                                  fill="#7aa0d6"
-                                />
-                              ) : (
-                                <circle
-                                  r={4.5}
-                                  fill="#7aa0d6"
-                                  stroke="#ffffff"
-                                  strokeWidth={2}
-                                />
-                              )}
-
                               <Tooltip content={fullText}>
                                 <text
                                   x={0}

+ 90 - 17
frontend/react-template/src/components/FlowChart/components/Edge.tsx

@@ -2,17 +2,25 @@ import type { FC } from "react";
 import * as d3 from "d3";
 import type { Goal } from "../../../types/goal";
 import styles from "../styles/Edge.module.css";
+import { Tooltip } from "@douyinfe/semi-ui";
 
 // 主链连接线:根据目标状态决定颜色与箭头,支持选中高亮
 interface EdgeProps {
   link: d3.HierarchyPointLink<Goal>;
+  label?: string;
   highlighted: boolean;
   dimmed: boolean;
   onClick: () => void;
+  mode?: "line" | "label";
+  curveOffset?: number;
 }
 
-export const Edge: FC<EdgeProps> = ({ link, highlighted, dimmed, onClick }) => {
+export const Edge: FC<EdgeProps> = ({ link, label, highlighted, dimmed, onClick, mode = "line", curveOffset = 0 }) => {
   const { source, target } = link;
+  const truncateMiddle = (text: string, limit: number) => {
+    if (text.length <= limit) return text;
+    return `${text.slice(0, 2)}...${text.slice(-2)}`;
+  };
 
   const createPath = () => {
     const sourceX = source.x;
@@ -24,6 +32,11 @@ export const Edge: FC<EdgeProps> = ({ link, highlighted, dimmed, onClick }) => {
     const startX = sourceX + direction * nodeHalfW;
     const endX = targetX - direction * nodeHalfW;
     const midX = (startX + endX) / 2;
+
+    if (curveOffset !== 0) {
+      return `M${startX},${sourceY} C${midX},${sourceY + curveOffset} ${midX},${targetY + curveOffset} ${endX},${targetY}`;
+    }
+
     return `M${startX},${sourceY} C${midX},${sourceY} ${midX},${targetY} ${endX},${targetY}`;
   };
 
@@ -57,23 +70,83 @@ export const Edge: FC<EdgeProps> = ({ link, highlighted, dimmed, onClick }) => {
     }
   };
 
-  return (
-    <g className={`${styles.edge} ${highlighted ? styles.selected : ""} ${dimmed ? styles.dimmed : ""}`}>
-      <path
-        d={createPath()}
-        fill="none"
-        stroke="transparent"
-        strokeWidth={24}
-        onClick={onClick}
+  const renderLabel = () => {
+    if (!label) return null;
+    const sourceX = source.x;
+    const sourceY = source.y;
+    const targetX = target.x;
+    const targetY = target.y;
+    const nodeHalfW = 80;
+    const direction = targetX >= sourceX ? 1 : -1;
+    const startX = sourceX + direction * nodeHalfW;
+    const endX = targetX - direction * nodeHalfW;
+    const midX = (startX + endX) / 2;
+    const midY = (sourceY + targetY) / 2 + curveOffset * 0.75;
+
+    const truncated = truncateMiddle(label, 6);
+
+    return (
+      <g
+        transform={`translate(${midX},${midY})`}
         style={{ cursor: "pointer" }}
-      />
-      <path
-        d={createPath()}
-        fill="none"
-        stroke={getStrokeColor()}
-        strokeWidth={3}
-        markerEnd={getMarkerUrl()}
-      />
+      >
+        <title>{label}</title>
+        <rect
+          x={-30}
+          y={-10}
+          width={60}
+          height={20}
+          rx={4}
+          fill="#fff"
+          fillOpacity={1}
+          stroke="#fff"
+          strokeWidth={1}
+        />
+        <Tooltip
+          content={label}
+          position="top"
+        >
+          <text
+            textAnchor="middle"
+            dy="0.35em"
+            fontSize={11}
+            fill="#1f2937"
+            style={{ userSelect: "none" }}
+          >
+            {truncated}
+          </text>
+        </Tooltip>
+      </g>
+    );
+  };
+
+  return (
+    <g className={styles.edge}>
+      {mode === "line" && (
+        <>
+          <path
+            d={createPath()}
+            fill="none"
+            stroke="transparent"
+            strokeWidth={15}
+            style={{ cursor: "pointer" }}
+            onClick={(e) => {
+              e.stopPropagation();
+              onClick();
+            }}
+          />
+          <path
+            d={createPath()}
+            fill="none"
+            stroke={getStrokeColor()}
+            strokeWidth={highlighted ? 3 : 2}
+            markerEnd={getMarkerUrl()}
+            opacity={dimmed ? 0.2 : 1}
+            style={{ pointerEvents: "none" }}
+          />
+        </>
+      )}
+      {mode === "label" && renderLabel()}
     </g>
   );
 };

+ 13 - 7
frontend/react-template/src/components/FlowChart/components/Node.tsx

@@ -15,25 +15,31 @@ interface NodeProps {
 export const Node: FC<NodeProps> = ({ node, selected, highlighted, dimmed, onClick }) => {
   const { x, y, data } = node;
 
-  const truncateText = (text: string, maxLength: number): string => {
-    return text.length > maxLength ? `${text.slice(0, maxLength)}...` : text;
+  const truncateMiddle = (text: string, limit: number) => {
+    if (text.length <= limit) return text;
+    const half = Math.floor((limit - 3) / 2);
+    return `${text.slice(0, half)}...${text.slice(-half)}`;
   };
 
+  const isEndNode = data.id === "END";
+
   return (
     <g
-      className={`${styles.node} ${selected ? styles.selected : ""} ${dimmed ? styles.dimmed : ""}`}
+      className={`${styles.node} ${selected ? styles.selected : ""} ${dimmed ? styles.dimmed : ""} ${isEndNode ? styles.endNode : ""}`}
       transform={`translate(${x},${y})`}
       onClick={onClick}
       style={{ cursor: "pointer" }}
     >
+      <title>{data.description}</title>
       <rect
         x={-70}
         y={-26}
         width={140}
         height={52}
         rx={8}
-        fill="transparent"
-        stroke="none"
+        fill={isEndNode ? "#fff" : "transparent"}
+        stroke={isEndNode ? "#ff4d4f" : "none"}
+        strokeDasharray={isEndNode ? "4 4" : "none"}
       />
       <text
         textAnchor="middle"
@@ -41,9 +47,9 @@ export const Node: FC<NodeProps> = ({ node, selected, highlighted, dimmed, onCli
         fontSize={16}
         fill={dimmed ? "#999" : "#333"}
         fontWeight={selected || highlighted ? 600 : 400}
-        style={{ opacity: dimmed ? 0.35 : 1 }}
+        style={{ opacity: dimmed ? 0.35 : 1, pointerEvents: "none" }}
       >
-        {truncateText(data.description || data.id, 16)}
+        {truncateMiddle(data.description || data.id, 10)}
       </text>
       {data.status === "running" && (
         <circle

+ 29 - 6
frontend/react-template/src/components/FlowChart/hooks/useFlowChartData.ts

@@ -81,8 +81,19 @@ export const useFlowChartData = (traceId: string | null, initialGoals: Goal[]) =
           (rawTrace && isRecord(rawTrace.goal_tree) ? rawTrace.goal_tree : undefined) ||
           {};
         const goalList = isRecord(goalTree) ? goalTree.goals : undefined;
+        console.log("%c [ goalList ]-90", "font-size:13px; background:pink; color:#bf2c9f;", goalList);
         const nextGoals = Array.isArray(goalList) ? (goalList as Goal[]) : [];
-        setGoals(nextGoals);
+        console.log("%c [ nextGoals ]-91", "font-size:13px; background:pink; color:#bf2c9f;", nextGoals);
+        setGoals((prev) => {
+          return nextGoals.map((ng) => {
+            const existing = prev.find((p) => p.id === ng.id);
+            // 保留 sub_trace_ids,如果 WebSocket 数据中缺失但本地已有
+            if (existing && existing.sub_trace_ids && !ng.sub_trace_ids) {
+              return { ...ng, sub_trace_ids: existing.sub_trace_ids };
+            }
+            return ng;
+          });
+        });
         return;
       }
 
@@ -93,7 +104,13 @@ export const useFlowChartData = (traceId: string | null, initialGoals: Goal[]) =
           const next = [...prev];
           const idx = next.findIndex((g) => g.id === goal.id);
           if (idx >= 0) {
-            next[idx] = { ...next[idx], ...goal };
+            const existing = next[idx];
+            const merged = { ...existing, ...goal };
+            // 保留 sub_trace_ids,如果 WebSocket 数据中缺失但本地已有
+            if (existing.sub_trace_ids && !merged.sub_trace_ids) {
+              merged.sub_trace_ids = existing.sub_trace_ids;
+            }
+            next[idx] = merged;
             return next;
           }
           next.push(goal as Goal);
@@ -133,14 +150,20 @@ export const useFlowChartData = (traceId: string | null, initialGoals: Goal[]) =
 
       if (event === "message_added") {
         const message = isMessage(data.message) ? data.message : isMessage(raw.message) ? raw.message : null;
-        if (!message) return;
-        setMessages((prev: Message[]) => [...prev, message]);
-        updateMessageGroups(message);
+        if (message) {
+          setMessages((prev) => {
+            const next = [...prev, message];
+            next.sort((a, b) => messageSortKey(a) - messageSortKey(b));
+            return next;
+          });
+          updateMessageGroups(message);
+        }
       }
     },
-    [updateMessageGroups],
+    [messageSortKey, updateMessageGroups],
   );
 
+  // 主 Trace 连接
   const wsOptions = useMemo(() => ({ onMessage: handleWebSocketMessage }), [handleWebSocketMessage]);
   const { connected } = useWebSocket(traceId, wsOptions);
 

+ 2 - 11
frontend/react-template/src/components/MainContent/MainContent.tsx

@@ -10,10 +10,9 @@ import styles from "./MainContent.module.css";
 interface MainContentProps {
   traceId: string | null;
   onNodeClick?: (node: Goal, edge?: Edge) => void;
-  onEdgeClick?: (edge: Edge) => void;
 }
 
-export const MainContent: FC<MainContentProps> = ({ traceId, onNodeClick, onEdgeClick }) => {
+export const MainContent: FC<MainContentProps> = ({ traceId, onNodeClick }) => {
   const { trace, loading } = useTrace(traceId);
   const initialGoals = useMemo(() => trace?.goal_tree?.goals ?? [], [trace]);
   const { goals, connected, msgGroups } = useFlowChartData(traceId, initialGoals);
@@ -35,17 +34,10 @@ export const MainContent: FC<MainContentProps> = ({ traceId, onNodeClick, onEdge
   return (
     <div className={styles.main}>
       <div className={styles.header}>
-        <div className={styles.title}>{trace?.task || "流程图"}</div>
+        <div className={styles.title}></div>
         <div className={styles.headerRight}>
           <div className={styles.status}>{connected ? "WebSocket 已连接" : "WebSocket 未连接"}</div>
           <div className={styles.legend}>
-            <div className={styles.legendItem}>
-              <span
-                className={styles.legendDot}
-                style={{ background: "#ff6b6b" }}
-              />
-              选中
-            </div>
             <div className={styles.legendItem}>
               <span
                 className={styles.legendDot}
@@ -87,7 +79,6 @@ export const MainContent: FC<MainContentProps> = ({ traceId, onNodeClick, onEdge
             goals={goals}
             msgGroups={msgGroups}
             onNodeClick={onNodeClick}
-            onEdgeClick={onEdgeClick}
           />
         )}
       </div>

+ 5 - 3
frontend/react-template/src/components/TopBar/TopBar.tsx

@@ -21,9 +21,11 @@ export const TopBar: FC<TopBarProps> = ({ onTraceSelect }) => {
           limit: 20,
         });
         const firstTrace = data.traces[0];
+        const traceId = firstTrace?.parent_trace_id || firstTrace.trace_id;
+        console.log("%c [ firstTrace ]-24", "font-size:13px; background:pink; color:#bf2c9f;", firstTrace);
         if (firstTrace) {
           setTitle(firstTrace.task);
-          onTraceSelect(firstTrace.trace_id);
+          onTraceSelect(traceId);
         } else {
           setTitle("流程图可视化系统");
           onTraceSelect("");
@@ -64,13 +66,13 @@ export const TopBar: FC<TopBarProps> = ({ onTraceSelect }) => {
           <option value="completed">已完成</option>
           <option value="failed">失败</option>
         </select>
-        <button
+        {/* <button
           onClick={() => loadTraces(statusFilter)}
           className={styles.button}
           disabled={loading}
         >
           {loading ? "加载中..." : "刷新"}
-        </button>
+        </button> */}
       </div>
     </header>
   );

+ 3 - 0
frontend/react-template/src/types/goal.ts

@@ -1,12 +1,15 @@
 export interface Goal {
   id: string;
   description: string;
+  reason?: string;
   status: "pending" | "running" | "completed" | "failed";
   summary?: string;
   parent_id?: string;
   created_at: string;
   completed_at?: string;
   metadata?: Record<string, unknown>;
+  agent_call_mode?: string;
+  sub_trace_ids?: Array<string | { trace_id: string; mission?: string }>;
 }
 
 export interface BranchContext {

+ 12 - 1
frontend/react-template/src/types/message.ts

@@ -1,9 +1,20 @@
+export interface ToolCall {
+  id: string;
+  name: string;
+  arguments: Record<string, unknown>;
+}
+
+export interface MessageContent {
+  text?: string;
+  tool_calls?: ToolCall[];
+}
+
 export interface Message {
   id?: string;
   message_id?: string;
   goal_id?: string;
   role?: string;
-  content?: string;
+  content?: string | MessageContent;
   description?: string;
   tokens?: number | null;
 }

+ 1 - 0
frontend/react-template/src/types/trace.ts

@@ -10,6 +10,7 @@ export interface TraceListItem {
   total_cost: number;
   current_goal_id: string;
   created_at: string;
+  parent_trace_id: string | null;
 }
 
 export interface TraceListResponse {

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است