Просмотр исходного кода

design: context management update

Talegorithm 1 месяц назад
Родитель
Сommit
f16410da5d
10 измененных файлов с 2098 добавлено и 876 удалено
  1. 45 10
      agent/core/runner.py
  2. 2 5
      agent/execution/fs_store.py
  3. 29 0
      agent/goal/__init__.py
  4. 145 0
      agent/goal/compaction.py
  5. 257 0
      agent/goal/models.py
  6. 123 0
      agent/goal/tool.py
  7. 82 86
      docs/README.md
  8. 596 127
      docs/context-management.md
  9. 133 273
      docs/trace-api.md
  10. 686 375
      frontend/API.md

+ 45 - 10
agent/core/runner.py

@@ -5,7 +5,8 @@ Agent Runner - Agent 执行引擎
 1. 执行 Agent 任务(循环调用 LLM + 工具)
 2. 记录执行图(Trace + Steps)
 3. 检索和注入记忆(Experience + Skill)
-4. 收集反馈,提取经验
+4. 管理执行计划(Goal Tree)
+5. 收集反馈,提取经验
 """
 
 import logging
@@ -15,6 +16,7 @@ from typing import AsyncIterator, Optional, Dict, Any, List, Callable, Literal,
 
 from agent.core.config import AgentConfig, CallResult
 from agent.execution import Trace, Step, TraceStore
+from agent.goal import GoalTree, goal_tool, compress_messages_for_goal
 from agent.memory.models import Experience, Skill
 from agent.memory.protocols import MemoryStore, StateStore
 from agent.memory.skill_loader import load_skills_from_dir
@@ -33,6 +35,7 @@ BUILTIN_TOOLS = [
     "bash_command",
     "skill",
     "list_skills",
+    "goal",
 ]
 
 
@@ -54,6 +57,7 @@ class AgentRunner:
         llm_call: Optional[Callable] = None,
         config: Optional[AgentConfig] = None,
         skills_dir: Optional[str] = None,
+        goal_tree: Optional[GoalTree] = None,
         debug: bool = False,
     ):
         """
@@ -67,6 +71,7 @@ class AgentRunner:
             llm_call: LLM 调用函数(必须提供,用于实际调用 LLM)
             config: Agent 配置
             skills_dir: Skills 目录路径(可选,不提供则不加载 skills)
+            goal_tree: 执行计划(可选,不提供则在运行时按需创建)
             debug: 保留参数(已废弃,请使用 API Server 可视化)
         """
         self.trace_store = trace_store
@@ -76,6 +81,7 @@ class AgentRunner:
         self.llm_call = llm_call
         self.config = config or AgentConfig()
         self.skills_dir = skills_dir
+        self.goal_tree = goal_tree
         self.debug = debug
 
     def _generate_id(self) -> str:
@@ -306,6 +312,9 @@ class AgentRunner:
             # 添加任务描述
             messages.append({"role": "user", "content": task})
 
+            # 初始化 GoalTree
+            goal_tree = self.goal_tree or GoalTree(mission=task)
+
             # 准备工具 Schema
             # 合并内置工具 + 用户指定工具
             tool_names = BUILTIN_TOOLS.copy()
@@ -324,9 +333,16 @@ class AgentRunner:
             total_cost = 0.0
 
             for iteration in range(max_iterations):
+                # 注入当前计划到 messages(如果有 goals)
+                llm_messages = list(messages)
+                if goal_tree.goals:
+                    plan_text = f"\n## Current Plan\n\n{goal_tree.to_prompt()}"
+                    # 作为最后一条 system 消息注入
+                    llm_messages.append({"role": "system", "content": plan_text})
+
                 # 调用 LLM
                 result = await self.llm_call(
-                    messages=messages,
+                    messages=llm_messages,
                     model=model,
                     tools=tool_schemas,
                     **kwargs
@@ -402,7 +418,12 @@ class AgentRunner:
                         break
 
                     # 执行工具
-                    messages.append({"role": "assistant", "content": response_content, "tool_calls": tool_calls})
+                    messages.append({
+                        "role": "assistant",
+                        "content": response_content,
+                        "tool_calls": tool_calls,
+                        "goal_id": goal_tree.current_id,
+                    })
 
                     for tc in tool_calls:
                         tool_name = tc["function"]["name"]
@@ -411,12 +432,25 @@ class AgentRunner:
                             import json
                             tool_args = json.loads(tool_args)
 
-                        # 执行工具
-                        tool_result = await self.tools.execute(
-                            tool_name,
-                            tool_args,
-                            uid=uid or ""
-                        )
+                        # 拦截 goal 工具调用
+                        if tool_name == "goal":
+                            prev_goal_id = goal_tree.current_id
+                            prev_goal = goal_tree.get_current()
+                            tool_result = goal_tool(tree=goal_tree, **tool_args)
+
+                            # 如果 done/abandon 触发了压缩
+                            if prev_goal_id and prev_goal:
+                                if prev_goal.status in ("completed", "abandoned") and prev_goal.summary:
+                                    messages = compress_messages_for_goal(
+                                        messages, prev_goal_id, prev_goal.summary
+                                    )
+                        else:
+                            # 执行普通工具
+                            tool_result = await self.tools.execute(
+                                tool_name,
+                                tool_args,
+                                uid=uid or ""
+                            )
 
                         # 记录 action Step
                         action_step_id = self._generate_id()
@@ -471,7 +505,8 @@ class AgentRunner:
                             "role": "tool",
                             "tool_call_id": tc["id"],
                             "name": tool_name,
-                            "content": tool_result
+                            "content": tool_result,
+                            "goal_id": goal_tree.current_id,
                         })
 
                     continue  # 继续循环

+ 2 - 5
agent/execution/fs_store.py

@@ -196,12 +196,9 @@ class FileSystemTraceStore:
                 parent_file = self._get_step_file(trace_id, step.parent_id)
                 parent_file.write_text(json.dumps(parent.to_dict(view="full"), indent=2, ensure_ascii=False))
 
-        # 4. 追加 step_added 事件
+        # 4. 追加 step_added 事件(包含完整 compact 视图,用于断线续传)
         await self.append_event(trace_id, "step_added", {
-            "step_id": step.step_id,
-            "step_type": step.step_type,
-            "parent_id": step.parent_id,
-            "sequence": step.sequence,
+            "step": step.to_dict(view="compact")
         })
 
         return step.step_id

+ 29 - 0
agent/goal/__init__.py

@@ -0,0 +1,29 @@
+"""
+Goal 模块 - 执行计划管理
+
+提供 Goal 和 GoalTree 数据模型,以及 goal 工具。
+"""
+
+from agent.goal.models import Goal, GoalTree, GoalStatus
+from agent.goal.tool import goal_tool, create_goal_tool_schema
+from agent.goal.compaction import (
+    compress_messages_for_goal,
+    compress_all_completed,
+    get_messages_for_goal,
+    should_compress,
+)
+
+__all__ = [
+    # Models
+    "Goal",
+    "GoalTree",
+    "GoalStatus",
+    # Tool
+    "goal_tool",
+    "create_goal_tool_schema",
+    # Compaction
+    "compress_messages_for_goal",
+    "compress_all_completed",
+    "get_messages_for_goal",
+    "should_compress",
+]

+ 145 - 0
agent/goal/compaction.py

@@ -0,0 +1,145 @@
+"""
+Context 压缩
+
+基于 Goal 状态进行增量压缩:
+- 当 Goal 完成或放弃时,将相关的详细 messages 替换为 summary
+"""
+
+from typing import List, Dict, Any, Optional
+from agent.goal.models import GoalTree, Goal
+
+
+def compress_messages_for_goal(
+    messages: List[Dict[str, Any]],
+    goal_id: str,
+    summary: str,
+) -> List[Dict[str, Any]]:
+    """
+    压缩指定 goal 关联的 messages
+
+    将 goal_id 关联的所有详细 messages 替换为一条 summary message。
+
+    Args:
+        messages: 原始消息列表
+        goal_id: 要压缩的 goal ID
+        summary: 压缩后的摘要
+
+    Returns:
+        压缩后的消息列表
+    """
+    # 分离:关联的 messages vs 其他 messages
+    related = []
+    other = []
+
+    for msg in messages:
+        if msg.get("goal_id") == goal_id:
+            related.append(msg)
+        else:
+            other.append(msg)
+
+    # 如果没有关联的消息,直接返回
+    if not related:
+        return messages
+
+    # 找到第一条关联消息的位置(用于插入 summary)
+    first_related_index = None
+    for i, msg in enumerate(messages):
+        if msg.get("goal_id") == goal_id:
+            first_related_index = i
+            break
+
+    # 创建 summary message
+    summary_message = {
+        "role": "assistant",
+        "content": f"[Goal {goal_id} Summary] {summary}",
+        "goal_id": goal_id,
+        "is_summary": True,
+    }
+
+    # 构建新的消息列表
+    result = []
+    summary_inserted = False
+
+    for i, msg in enumerate(messages):
+        if msg.get("goal_id") == goal_id:
+            # 跳过关联的详细消息,在第一个位置插入 summary
+            if not summary_inserted:
+                result.append(summary_message)
+                summary_inserted = True
+        else:
+            result.append(msg)
+
+    return result
+
+
+def should_compress(goal: Goal) -> bool:
+    """判断 goal 是否需要压缩"""
+    return goal.status in ("completed", "abandoned") and goal.summary is not None
+
+
+def compress_all_completed(
+    messages: List[Dict[str, Any]],
+    tree: GoalTree,
+) -> List[Dict[str, Any]]:
+    """
+    压缩所有已完成/已放弃的 goals
+
+    遍历 GoalTree,对所有需要压缩的 goal 执行压缩。
+
+    Args:
+        messages: 原始消息列表
+        tree: GoalTree 实例
+
+    Returns:
+        压缩后的消息列表
+    """
+    result = messages
+
+    def process_goal(goal: Goal):
+        nonlocal result
+        if should_compress(goal):
+            # 检查是否已经压缩过(避免重复压缩)
+            already_compressed = any(
+                msg.get("goal_id") == goal.id and msg.get("is_summary")
+                for msg in result
+            )
+            if not already_compressed:
+                result = compress_messages_for_goal(result, goal.id, goal.summary)
+
+        # 递归处理子目标
+        for child in goal.children:
+            process_goal(child)
+
+    for goal in tree.goals:
+        process_goal(goal)
+
+    return result
+
+
+def get_messages_for_goal(
+    messages: List[Dict[str, Any]],
+    goal_id: str,
+) -> List[Dict[str, Any]]:
+    """获取指定 goal 关联的所有 messages"""
+    return [msg for msg in messages if msg.get("goal_id") == goal_id]
+
+
+def count_tokens_estimate(messages: List[Dict[str, Any]]) -> int:
+    """
+    估算消息的 token 数量(简单估算)
+
+    实际使用时应该用 tiktoken 或 API 返回的 token 数。
+    这里用简单的字符数 / 4 来估算。
+    """
+    total_chars = 0
+    for msg in messages:
+        content = msg.get("content", "")
+        if isinstance(content, str):
+            total_chars += len(content)
+        elif isinstance(content, list):
+            # 多模态消息
+            for part in content:
+                if isinstance(part, dict) and part.get("type") == "text":
+                    total_chars += len(part.get("text", ""))
+
+    return total_chars // 4

+ 257 - 0
agent/goal/models.py

@@ -0,0 +1,257 @@
+"""
+Goal 数据模型
+
+Goal: 执行计划中的目标节点
+GoalTree: 目标树,管理整个执行计划
+"""
+
+from dataclasses import dataclass, field
+from datetime import datetime
+from typing import Dict, Any, List, Optional, Literal
+import json
+
+
+# Goal 状态
+GoalStatus = Literal["pending", "in_progress", "completed", "abandoned"]
+
+
+@dataclass
+class Goal:
+    """
+    执行目标
+
+    通过 children 形成层级结构。
+    """
+    id: str                                  # 自动生成: "1", "1.1", "2"
+    description: str                         # 目标描述
+    status: GoalStatus = "pending"           # 状态
+    summary: Optional[str] = None            # 完成/放弃时的总结
+    children: List["Goal"] = field(default_factory=list)
+    created_at: datetime = field(default_factory=datetime.now)
+
+    def to_dict(self) -> Dict[str, Any]:
+        """转换为字典"""
+        return {
+            "id": self.id,
+            "description": self.description,
+            "status": self.status,
+            "summary": self.summary,
+            "children": [c.to_dict() for c in self.children],
+            "created_at": self.created_at.isoformat() if self.created_at else None,
+        }
+
+    @classmethod
+    def from_dict(cls, data: Dict[str, Any]) -> "Goal":
+        """从字典创建"""
+        children = [cls.from_dict(c) for c in data.get("children", [])]
+        created_at = data.get("created_at")
+        if isinstance(created_at, str):
+            created_at = datetime.fromisoformat(created_at)
+
+        return cls(
+            id=data["id"],
+            description=data["description"],
+            status=data.get("status", "pending"),
+            summary=data.get("summary"),
+            children=children,
+            created_at=created_at or datetime.now(),
+        )
+
+
+@dataclass
+class GoalTree:
+    """
+    目标树 - 管理整个执行计划
+    """
+    mission: str                             # 总任务描述
+    goals: List[Goal] = field(default_factory=list)
+    current_id: Optional[str] = None         # 当前焦点 goal ID
+    created_at: datetime = field(default_factory=datetime.now)
+
+    def find(self, goal_id: str) -> Optional[Goal]:
+        """按 ID 查找 Goal"""
+        def search(goals: List[Goal]) -> Optional[Goal]:
+            for goal in goals:
+                if goal.id == goal_id:
+                    return goal
+                found = search(goal.children)
+                if found:
+                    return found
+            return None
+        return search(self.goals)
+
+    def find_parent(self, goal_id: str) -> Optional[Goal]:
+        """查找指定 Goal 的父节点"""
+        def search(goals: List[Goal], parent: Optional[Goal] = None) -> Optional[Goal]:
+            for goal in goals:
+                if goal.id == goal_id:
+                    return parent
+                found = search(goal.children, goal)
+                if found is not None:
+                    return found
+            return None
+        return search(self.goals, None)
+
+    def get_current(self) -> Optional[Goal]:
+        """获取当前焦点 Goal"""
+        if self.current_id:
+            return self.find(self.current_id)
+        return None
+
+    def _generate_id(self, parent_id: Optional[str], sibling_count: int) -> str:
+        """生成新的 Goal ID"""
+        new_index = sibling_count + 1
+        if parent_id:
+            return f"{parent_id}.{new_index}"
+        return str(new_index)
+
+    def add_goals(self, descriptions: List[str], parent_id: Optional[str] = None) -> List[Goal]:
+        """
+        添加目标
+
+        如果 parent_id 为 None,添加到顶层
+        如果 parent_id 有值,添加为该 goal 的子目标
+        """
+        # 确定添加位置
+        if parent_id:
+            parent = self.find(parent_id)
+            if not parent:
+                raise ValueError(f"Parent goal not found: {parent_id}")
+            target_list = parent.children
+        else:
+            target_list = self.goals
+
+        # 创建新目标
+        new_goals = []
+        for desc in descriptions:
+            goal_id = self._generate_id(parent_id, len(target_list))
+            goal = Goal(id=goal_id, description=desc.strip())
+            target_list.append(goal)
+            new_goals.append(goal)
+
+        return new_goals
+
+    def focus(self, goal_id: str) -> Goal:
+        """切换焦点到指定 Goal,并将其状态设为 in_progress"""
+        goal = self.find(goal_id)
+        if not goal:
+            raise ValueError(f"Goal not found: {goal_id}")
+
+        # 更新状态
+        if goal.status == "pending":
+            goal.status = "in_progress"
+
+        self.current_id = goal_id
+        return goal
+
+    def complete(self, goal_id: str, summary: str) -> Goal:
+        """完成指定 Goal"""
+        goal = self.find(goal_id)
+        if not goal:
+            raise ValueError(f"Goal not found: {goal_id}")
+
+        goal.status = "completed"
+        goal.summary = summary
+
+        # 如果完成的是当前焦点,清除焦点
+        if self.current_id == goal_id:
+            self.current_id = None
+
+        return goal
+
+    def abandon(self, goal_id: str, reason: str) -> Goal:
+        """放弃指定 Goal"""
+        goal = self.find(goal_id)
+        if not goal:
+            raise ValueError(f"Goal not found: {goal_id}")
+
+        goal.status = "abandoned"
+        goal.summary = reason
+
+        # 如果放弃的是当前焦点,清除焦点
+        if self.current_id == goal_id:
+            self.current_id = None
+
+        return goal
+
+    def to_prompt(self) -> str:
+        """格式化为 Prompt 注入文本"""
+        lines = []
+        lines.append(f"**Mission**: {self.mission}")
+
+        if self.current_id:
+            current = self.find(self.current_id)
+            if current:
+                lines.append(f"**Current**: {self.current_id} {current.description}")
+
+        lines.append("")
+        lines.append("**Progress**:")
+
+        def format_goal(goal: Goal, indent: int = 0) -> List[str]:
+            prefix = "    " * indent
+
+            # 状态图标
+            if goal.status == "completed":
+                icon = "[✓]"
+            elif goal.status == "in_progress":
+                icon = "[→]"
+            elif goal.status == "abandoned":
+                icon = "[✗]"
+            else:
+                icon = "[ ]"
+
+            # 当前焦点标记
+            current_mark = " ← current" if goal.id == self.current_id else ""
+
+            result = [f"{prefix}{icon} {goal.id}. {goal.description}{current_mark}"]
+
+            # 显示 summary(如果有)
+            if goal.summary:
+                result.append(f"{prefix}    → {goal.summary}")
+
+            # 递归处理子目标
+            for child in goal.children:
+                result.extend(format_goal(child, indent + 1))
+
+            return result
+
+        for goal in self.goals:
+            lines.extend(format_goal(goal))
+
+        return "\n".join(lines)
+
+    def to_dict(self) -> Dict[str, Any]:
+        """转换为字典"""
+        return {
+            "mission": self.mission,
+            "goals": [g.to_dict() for g in self.goals],
+            "current_id": self.current_id,
+            "created_at": self.created_at.isoformat() if self.created_at else None,
+        }
+
+    @classmethod
+    def from_dict(cls, data: Dict[str, Any]) -> "GoalTree":
+        """从字典创建"""
+        goals = [Goal.from_dict(g) for g in data.get("goals", [])]
+        created_at = data.get("created_at")
+        if isinstance(created_at, str):
+            created_at = datetime.fromisoformat(created_at)
+
+        return cls(
+            mission=data["mission"],
+            goals=goals,
+            current_id=data.get("current_id"),
+            created_at=created_at or datetime.now(),
+        )
+
+    def save(self, path: str) -> None:
+        """保存到 JSON 文件"""
+        with open(path, "w", encoding="utf-8") as f:
+            json.dump(self.to_dict(), f, ensure_ascii=False, indent=2)
+
+    @classmethod
+    def load(cls, path: str) -> "GoalTree":
+        """从 JSON 文件加载"""
+        with open(path, "r", encoding="utf-8") as f:
+            data = json.load(f)
+        return cls.from_dict(data)

+ 123 - 0
agent/goal/tool.py

@@ -0,0 +1,123 @@
+"""
+Goal 工具 - 计划管理
+
+提供 goal 工具供 LLM 管理执行计划。
+"""
+
+from typing import Optional, List, TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from agent.goal.models import GoalTree
+
+
+def goal_tool(
+    tree: "GoalTree",
+    add: Optional[str] = None,
+    done: Optional[str] = None,
+    abandon: Optional[str] = None,
+    focus: Optional[str] = None,
+) -> str:
+    """
+    管理执行计划。
+
+    Args:
+        tree: GoalTree 实例
+        add: 添加目标(逗号分隔多个)。添加到当前 focus 的 goal 下作为子目标。
+        done: 完成当前目标,值为 summary
+        abandon: 放弃当前目标,值为原因
+        focus: 切换焦点到指定 id
+
+    Returns:
+        更新后的计划状态文本
+    """
+    changes = []
+
+    # 1. 处理 abandon(先处理,因为可能需要在 add 新目标前放弃旧的)
+    if abandon is not None:
+        if not tree.current_id:
+            return "错误:没有当前目标可以放弃"
+        goal = tree.abandon(tree.current_id, abandon)
+        changes.append(f"已放弃: {goal.id}. {goal.description}")
+
+    # 2. 处理 done
+    if done is not None:
+        if not tree.current_id:
+            return "错误:没有当前目标可以完成"
+        goal = tree.complete(tree.current_id, done)
+        changes.append(f"已完成: {goal.id}. {goal.description}")
+
+    # 3. 处理 focus(在 add 之前,这样 add 可以添加到新焦点下)
+    if focus is not None:
+        goal = tree.focus(focus)
+        changes.append(f"切换焦点: {goal.id}. {goal.description}")
+
+    # 4. 处理 add
+    if add is not None:
+        descriptions = [d.strip() for d in add.split(",") if d.strip()]
+        if descriptions:
+            # 添加到当前焦点下(如果有焦点),否则添加到顶层
+            parent_id = tree.current_id
+            new_goals = tree.add_goals(descriptions, parent_id)
+            if parent_id:
+                changes.append(f"在 {parent_id} 下添加 {len(new_goals)} 个子目标")
+            else:
+                changes.append(f"添加 {len(new_goals)} 个顶层目标")
+
+            # 如果没有焦点且添加了目标,自动 focus 到第一个新目标
+            if not tree.current_id and new_goals:
+                tree.focus(new_goals[0].id)
+                changes.append(f"自动切换焦点: {new_goals[0].id}")
+
+    # 返回当前状态
+    result = []
+    if changes:
+        result.append("## 更新")
+        result.extend(f"- {c}" for c in changes)
+        result.append("")
+
+    result.append("## Current Plan")
+    result.append(tree.to_prompt())
+
+    return "\n".join(result)
+
+
+def create_goal_tool_schema() -> dict:
+    """创建 goal 工具的 JSON Schema"""
+    return {
+        "name": "goal",
+        "description": """管理执行计划。
+
+- add: 添加目标(逗号分隔多个)。添加到当前 focus 的 goal 下作为子目标。
+- done: 完成当前目标,值为 summary
+- abandon: 放弃当前目标,值为原因(会触发 context 压缩)
+- focus: 切换焦点到指定 id
+
+示例:
+- goal(add="分析代码, 实现功能, 测试") - 添加顶层目标
+- goal(focus="2", add="设计接口, 实现代码") - 切换到目标2,并添加子目标
+- goal(done="发现用户模型在 models/user.py") - 完成当前目标
+- goal(abandon="方案A需要Redis,环境没有", add="实现方案B") - 放弃当前并添加新目标
+""",
+        "parameters": {
+            "type": "object",
+            "properties": {
+                "add": {
+                    "type": "string",
+                    "description": "添加目标(逗号分隔多个)。添加到当前 focus 的 goal 下作为子目标。"
+                },
+                "done": {
+                    "type": "string",
+                    "description": "完成当前目标,值为 summary"
+                },
+                "abandon": {
+                    "type": "string",
+                    "description": "放弃当前目标,值为原因"
+                },
+                "focus": {
+                    "type": "string",
+                    "description": "切换焦点到指定 goal id"
+                }
+            },
+            "required": []
+        }
+    }

+ 82 - 86
docs/README.md

@@ -27,7 +27,7 @@
 | 工具调用 | 可选 | 常用 | 受限工具集 |
 | 状态管理 | 无 | 有 (Trace) | 有 (独立 Trace + 父子关系) |
 | 记忆检索 | 无 | 有 (Experience/Skill) | 有 (继承主 Agent) |
-| 执行图 | 1 个节点 | N 个节点的 DAG | 嵌套 DAG(多个 Trace) |
+| 执行图 | 1 条 Message | N 条 Messages 的 DAG | 嵌套 DAG(多个 Trace) |
 | 触发方式 | 直接调用 | 直接调用 | 通过 Task 工具 |
 | 权限范围 | 完整 | 完整 | 受限(可配置) |
 
@@ -55,7 +55,8 @@
 ┌─────────────────────────────────────────────────────────────┐
 │ Layer 1: Task State(任务状态)                               │
 │ - 当前任务的工作记忆                                          │
-│ - Trace/Step 记录执行过程                                    │
+│ - Trace + Messages 记录执行过程                               │
+│ - GoalTree 管理执行计划                                       │
 └─────────────────────────────────────────────────────────────┘
 ```
 
@@ -68,42 +69,45 @@
 ## 核心流程:Agent Loop
 
 ```python
-async def run(task: str, max_steps: int = 50) -> AsyncIterator[Union[Trace, Step]]:
+async def run(task: str, max_steps: int = 50) -> AsyncIterator[Union[Trace, Message]]:
     # 1. 创建 Trace
     trace = Trace.create(mode="agent", task=task, status="in_progress")
     await trace_store.create_trace(trace)
     yield trace  # 返回 Trace(表示开始)
 
     # 2. 加载 Skills(内置 + 自定义)
-    # 内置 skills(agent/skills/core.md)自动加载
-    skills = load_skills_from_dir(skills_dir)  # skills_dir 可选
+    skills = load_skills_from_dir(skills_dir)
     skills_text = format_skills(skills)
 
     # 3. 检索 Experiences,构建 system prompt
     experiences = await search_experiences(task)
     system_prompt = build_system_prompt(experiences, skills_text)
 
-    # 4. 初始化消息
+    # 4. 初始化消息和 GoalTree
     messages = [{"role": "user", "content": task}]
+    goal_tree = GoalTree(mission=task)
 
     # 5. ReAct 循环
     for step in range(max_steps):
+        # 注入当前计划到 system prompt
+        plan_text = goal_tree.to_prompt()
+
         # 调用 LLM
         response = await llm.chat(
             messages=messages,
-            system=system_prompt,
-            tools=tool_registry.to_schema()  # 包括 skill、task 等工具
+            system=system_prompt + plan_text,
+            tools=tool_registry.to_schema()
         )
 
-        # 记录 LLM 调用 Step
-        llm_step = Step.create(
+        # 记录 assistant Message
+        assistant_msg = Message.create(
             trace_id=trace.trace_id,
-            step_type="thought",
-            status="completed",
-            data={"content": response.content, "tool_calls": response.tool_calls}
+            role="assistant",
+            goal_id=goal_tree.current_id,
+            content=response.content,  # text + tool_calls
         )
-        await trace_store.add_step(llm_step)
-        yield llm_step  # 返回 Step
+        await trace_store.add_message(assistant_msg)
+        yield assistant_msg
 
         # 没有工具调用,完成
         if not response.tool_calls:
@@ -111,53 +115,34 @@ async def run(task: str, max_steps: int = 50) -> AsyncIterator[Union[Trace, Step
 
         # 执行工具
         for tool_call in response.tool_calls:
-            # Doom loop 检测
-            if is_doom_loop(tool_call):
-                raise DoomLoopError()
-
-            # 执行工具(包括 skill、task 工具)
             result = await execute_tool(tool_call)
 
-            # 记录 action Step
-            action_step = Step.create(
+            # 记录 tool Message
+            tool_msg = Message.create(
                 trace_id=trace.trace_id,
-                step_type="action",
-                status="completed",
-                parent_id=llm_step.step_id,
-                data={"tool_name": tool_call.name, "arguments": tool_call.args}
+                role="tool",
+                goal_id=goal_tree.current_id,
+                tool_call_id=tool_call.id,
+                content=result,
             )
-            await trace_store.add_step(action_step)
-            yield action_step
-
-            # 记录 result Step
-            result_step = Step.create(
-                trace_id=trace.trace_id,
-                step_type="result",
-                status="completed",
-                parent_id=action_step.step_id,
-                data={"output": result}
-            )
-            await trace_store.add_step(result_step)
-            yield result_step
+            await trace_store.add_message(tool_msg)
+            yield tool_msg
 
             # 添加到消息历史
             messages.append({"role": "assistant", "tool_calls": [tool_call]})
-            messages.append({"role": "tool", "content": result})
+            messages.append({"role": "tool", "tool_call_id": tool_call.id, "content": result})
 
     # 6. 完成
     trace.status = "completed"
     await trace_store.update_trace(trace.trace_id, status="completed")
-    yield trace  # 返回更新后的 Trace
-
-    return trace
+    yield trace
 ```
 
 **关键机制**:
-- **统一返回类型**:`AsyncIterator[Union[Trace, Step]]` - 实时返回执行状态
+- **统一返回类型**:`AsyncIterator[Union[Trace, Message]]` - 实时返回执行状态
+- **GoalTree 注入**:每次 LLM 调用前注入当前计划(过滤废弃目标,连续编号)
+- **Message 关联 Goal**:每条 Message 通过 `goal_id` 关联所属 Goal
 - **Doom Loop 检测**:跟踪最近 3 次工具调用,如果都是同一个工具且参数相同,中断循环
-- **Skills 自动加载**:`agent/skills/core.md` 总是自动加载,`skills_dir` 可选额外加载
-- **动态工具加载**:Skill 通过 tool 动态加载,按需消耗 context
-- **Sub-Agent 支持**:通过 task 工具启动专门化的 Sub-Agent 处理子任务
 
 ### Sub-Agent 执行流程
 
@@ -195,36 +180,47 @@ class Trace:
     # Sub-Agent 支持
     parent_trace_id: Optional[str] = None      # 父 Trace ID
     agent_definition: Optional[str] = None     # Agent 类型名称
-    spawned_by_tool: Optional[str] = None      # 启动此 Sub-Agent 的 Step ID
+    spawned_by_tool: Optional[str] = None      # 启动此 Sub-Agent 的 Message ID
 
     # 统计
-    total_steps: int = 0
+    total_messages: int = 0
     total_tokens: int = 0
     total_cost: float = 0.0
 
     # 上下文
     uid: Optional[str] = None
     context: Dict[str, Any] = field(default_factory=dict)
+    current_goal_id: Optional[str] = None      # 当前焦点 goal
 ```
 
 **实现**:`agent/execution/models.py:Trace`
 
-### Step(执行步骤)
+### Message(执行消息)
+
+对应 LLM API 的消息,加上元数据。不再有 parent_id 树结构。
 
 ```python
 @dataclass
-class Step:
-    step_id: str
+class Message:
+    message_id: str
     trace_id: str
-    step_type: StepType    # "goal", "thought", "action", "result", "evaluation", "response"
-    status: Status         # "planned", "in_progress", "completed", "failed", "skipped"
-    parent_id: Optional[str] = None  # 树结构(单父节点)
-    description: str = ""            # 系统自动提取
-    data: Dict[str, Any] = field(default_factory=dict)
-    summary: Optional[str] = None    # 仅 evaluation 类型需要
+    role: Literal["assistant", "tool"]   # 和 LLM API 一致
+    sequence: int                        # 全局顺序
+    goal_id: str                         # 关联的 Goal 内部 ID
+    tool_call_id: Optional[str] = None   # tool 消息关联对应的 tool_call
+    content: Any = None                  # 消息内容(和 LLM API 格式一致)
+    description: str = ""                # 系统自动提取的摘要
+
+    # 元数据
+    tokens: Optional[int] = None
+    cost: Optional[float] = None
 ```
 
-**实现**:`agent/execution/models.py:Step`
+**实现**:`agent/execution/models.py:Message`
+
+**Message 类型**:
+- `role="assistant"`:模型的一次返回(可能同时包含文本和多个 tool_calls)
+- `role="tool"`:一个工具的执行结果(通过 `tool_call_id` 关联)
 
 ---
 
@@ -247,12 +243,14 @@ class Step:
 
 **使用示例**:`examples/subagent_example.py`
 
-### [Context 管理](./context-management.md)
-- OpenCode 方案参考:Message 管理、两阶段压缩、Sub-Agent
-- goal 工具:线性计划管理
+### [Context 管理与可视化](./context-management.md)
+- GoalTree:层级目标管理(嵌套 JSON,注入 LLM)
+- Goal ID 设计:内部 ID(稳定)vs 显示序号(连续,给 LLM)
+- goal 工具:计划管理(add, done, abandon, focus)
 - explore 工具:并行探索-合并
-- 回溯机制:abandon + context 压缩
-- 数据结构:Goal Tree + 线性 Message List
+- 回溯机制:未执行直接修改 / 已执行标记废弃+新分支
+- DAG 可视化:节点=结果,边=动作,边可展开/折叠
+- 数据结构:GoalTree + Messages(扁平列表,goal_id 关联)
 
 ### [工具系统](./tools.md)
 - 工具定义和注册
@@ -440,8 +438,9 @@ system_prompt = base_prompt + "\n\n# Learned Experiences\n" + "\n".join([
 class TraceStore(Protocol):
     async def save(self, trace: Trace) -> None: ...
     async def get(self, trace_id: str) -> Trace: ...
-    async def add_step(self, step: Step) -> None: ...
-    async def get_steps(self, trace_id: str) -> List[Step]: ...
+    async def add_message(self, message: Message) -> None: ...
+    async def get_messages(self, trace_id: str) -> List[Message]: ...
+    async def get_messages_by_goal(self, trace_id: str, goal_id: str) -> List[Message]: ...
 
 class ExperienceStore(Protocol):
     async def search(self, scope: str, query: str, limit: int) -> List[Dict]: ...
@@ -457,11 +456,11 @@ class SkillLoader(Protocol):
 ```
 
 **实现**:
-- Trace/Step 协议:`agent/execution/protocols.py`
+- Trace/Message 协议:`agent/execution/protocols.py`
 - Memory 协议:`agent/memory/protocols.py`
 
 **实现策略**:
-- Trace/Step: 文件系统(JSON)
+- Trace/Message: 文件系统(JSON)
   - `FileSystemTraceStore` - 文件持久化(支持跨进程)
 - Experience: PostgreSQL + pgvector
 - Skill: 文件系统(Markdown)
@@ -479,11 +478,10 @@ agent/
 │   └── config.py          # AgentConfig, CallResult
 ├── execution/             # 执行追踪
-│   ├── models.py          # Trace, Step
+│   ├── models.py          # Trace, Message
 │   ├── protocols.py       # TraceStore
 │   ├── fs_store.py        # FileSystemTraceStore
-│   ├── tree_dump.py       # 可视化
-│   ├── api.py             # RESTful API
+│   ├── api.py             # RESTful API(DAG 视图)
 │   └── websocket.py       # WebSocket
 ├── memory/                # 记忆系统
@@ -537,29 +535,26 @@ agent/
    - 需要统计分析
    - 数量大,动态更新
 
-4. **Context 管理:Goal + Explore 方案**
-   - 简单工具接口,系统管理复杂性
+4. **Context 管理:GoalTree + Message + DAG 可视化**
+   - GoalTree 嵌套 JSON 注入 LLM,Messages 扁平存储
+   - DAG 可视化从 GoalTree + Messages 派生
    - 详见 [`docs/context-management.md`](./context-management.md)
 
 ---
 
 ## Debug 工具
 
-开发调试时可实时查看 Step 树:
-
-```python
-from agent.debug import dump_tree
-
-# 每次 step 变化后调用
-dump_tree(trace, steps)
-```
+开发调试时可通过 API 查看 DAG 可视化:
 
 ```bash
-# 终端实时查看
-watch -n 0.5 cat .trace/tree.txt
+# 启动 API Server
+python api_server.py
+
+# 查看 DAG
+curl http://localhost:8000/api/traces/{trace_id}/dag
 ```
 
-**实现**:`agent/execution/tree_dump.py`
+**实现**:`agent/execution/api.py`
 
 ---
 
@@ -591,8 +586,9 @@ GEMINI_API_KEY=xxx pytest tests/e2e/ -v -m e2e
 | 概念 | 定义 | 存储 | 实现 |
 |------|------|------|------|
 | **Trace** | 一次任务执行 | 文件系统(JSON) | `execution/models.py` |
-| **Step** | 执行步骤 | 文件系统(JSON) | `execution/models.py` |
-| **Goal** | 计划目标 | goal.json | `goal/models.py`(待实现) |
+| **Message** | 执行消息(对应 LLM 消息) | 文件系统(JSON) | `execution/models.py` |
+| **GoalTree** | 层级执行计划 | goal.json | `goal/models.py` |
+| **Goal** | 计划中的目标节点 | 嵌套在 GoalTree 中 | `goal/models.py` |
 | **Sub-Agent** | 专门化的子代理 | 独立 Trace | `tools/builtin/task.py` |
 | **AgentDefinition** | Agent 类型定义 | 配置文件/代码 | `subagents/` |
 | **Skill** | 能力描述(Markdown) | 文件系统 | `memory/skill_loader.py` |

+ 596 - 127
docs/context-management.md

@@ -140,16 +140,17 @@ Todo.Info = {
 
 ```
 ┌─────────────────────────────────────────────┐
-│              Plan (goal.json)                │
-│  结构化的目标树,LLM 通过 goal 工具维护       │
+│              GoalTree (嵌套 JSON)             │
+│  层级目标,LLM 通过 goal 工具维护              │
+│  注入 LLM 时过滤废弃目标,重新生成连续显示序号   │
 └─────────────────────────────────────────────┘
          ┌────────────┴────────────┐
          ↓                         ↓
 ┌─────────────────┐      ┌─────────────────┐
-│   线性执行       │      │  并行探索        │
-│   (主 message   │      │  (explore 工具)  │
-│    list)        │      │  多个独立分支    │
+│   Messages      │      │  并行探索        │
+│   (扁平列表,    │      │  (explore 工具)  │
+│    goal_id 关联) │      │  多个独立分支    │
 └─────────────────┘      └─────────────────┘
          │                         │
          ↓                         ↓
@@ -159,11 +160,118 @@ Todo.Info = {
 │  触发 context   │      └─────────────────┘
 │  压缩           │
 └─────────────────┘
+         │
+         ↓
+┌─────────────────────────────────────────────┐
+│              DAG 可视化(派生视图)             │
+│  从 GoalTree + Messages 生成                  │
+│  节点 = 结果/里程碑,边 = 动作/执行过程         │
+│  边可展开/折叠,对应目标的层级展开              │
+└─────────────────────────────────────────────┘
+```
+
+### 数据结构
+
+#### 两层数据
+
+后端存储两类数据,可视化的 DAG 是派生视图:
+
+1. **GoalTree**(嵌套 JSON):层级目标,注入 LLM
+2. **Messages**(扁平列表):执行记录,通过 `goal_id` 关联 Goal
+
+不存在独立的"边"数据结构,边在可视化时从 Messages 聚合生成。
+
+#### 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", "explore_start", "explore_merge"]
+
+@dataclass
+class Goal:
+    id: str                              # 内部唯一 ID,纯自增("1", "2", "3"...)
+    parent_id: Optional[str] = None      # 父 Goal ID(层级关系)
+    branch_id: Optional[str] = None      # 所属分支 ID(分支关系,null=主线)
+    type: GoalType = "normal"            # Goal 类型
+
+    description: str                     # 目标描述(做什么)
+    reason: str                          # 创建理由(为什么做)
+    status: GoalStatus                   # pending | in_progress | completed | abandoned
+    summary: Optional[str] = None        # 完成/放弃时的总结
+
+    # explore_start 特有
+    branch_ids: Optional[List[str]] = None       # 关联的分支 ID 列表
+
+    # explore_merge 特有
+    explore_start_id: Optional[str] = None       # 关联的 explore_start Goal
+    merge_summary: Optional[str] = None          # 各分支汇总结果
+    selected_branch: Optional[str] = None        # 选中的分支(可选)
+
+    # 统计(后端维护,用于可视化边的数据)
+    self_stats: GoalStats                # 自身统计(仅直接关联的 messages)
+    cumulative_stats: GoalStats          # 累计统计(自身 + 所有后代)
 ```
 
+**实现**:`agent/goal/models.py:Goal`
+
+**ID 设计**:
+- **内部 ID**:纯自增数字("1", "2", "3", "4"...),不管层级、分支、废弃
+- **层级关系**:通过 `parent_id` 字段维护
+- **分支关系**:通过 `branch_id` 字段维护(null 表示主线,"A"/"B" 表示分支)
+- **显示序号**:`to_prompt()` 时动态生成连续有意义的编号("1", "2", "2.1", "2.2"...)
+
+**统计更新逻辑**:
+- 每次添加 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 构建层级)
+```
+
+**实现**:`agent/goal/models.py:GoalTree`
+
+#### Message
+
+Message 对应 LLM API 的消息,加上元数据。每条 Message 通过 `goal_id` 和 `branch_id` 关联所属 Goal。
+
+```python
+@dataclass
+class Message:
+    message_id: str
+    trace_id: str
+    branch_id: Optional[str] = None      # 所属分支(null=主线, "A"/"B"=分支)
+    role: Literal["assistant", "tool"]   # 和 LLM API 一致
+    sequence: int                        # 全局顺序
+    goal_id: str                         # 关联的 Goal 内部 ID
+    tool_call_id: Optional[str]          # tool 消息关联对应的 tool_call
+    content: Any                         # 消息内容(和 LLM API 格式一致)
+
+    # 元数据
+    tokens: Optional[int] = None
+    cost: Optional[float] = None
+    created_at: datetime
+```
+
+**实现**:`agent/execution/models.py:Message`
+
+**Message 类型说明**:
+- `role="assistant"`:模型的一次返回,可能同时包含文本和多个 tool_calls
+- `role="tool"`:一个工具的执行结果,通过 `tool_call_id` 关联对应的 tool_call
+
 ### 工具设计
 
-#### 1. goal 工具:计划管理
+#### goal 工具:计划管理
 
 ```python
 @tool
@@ -171,11 +279,13 @@ def goal(
     add: Optional[str] = None,       # 添加目标(逗号分隔多个)
     done: Optional[str] = None,      # 完成当前目标,值为 summary
     abandon: Optional[str] = None,   # 放弃当前目标,值为原因
-    focus: Optional[str] = None,     # 切换焦点到指定 id
+    focus: Optional[str] = None,     # 切换焦点到指定显示序号
 ) -> str:
     """管理执行计划。"""
 ```
 
+**实现**:`agent/goal/tool.py:goal_tool`
+
 **层级支持**:`add` 添加到当前 focus 的 goal 下作为子目标。
 
 ```python
@@ -202,6 +312,7 @@ goal(add="设计接口, 实现代码")
 pending ──focus──→ in_progress ──done──→ completed
                         │                    ↓
                         │              (压缩 context)
+                        │              (级联:若所有兄弟都 completed,父 goal 自动 completed)
                      abandon
@@ -210,7 +321,7 @@ pending ──focus──→ in_progress ──done──→ completed
                   (压缩 context)
 ```
 
-#### 2. explore 工具:并行探索
+#### explore 工具:并行探索
 
 基于 sub-agent 机制实现。
 
@@ -228,74 +339,17 @@ def explore(
     """
 ```
 
-**示例**:
-```python
-explore(
-    background="我们在实现用户认证。项目用 FastAPI,用户模型在 models/user.py。环境没有 Redis。",
-    branches=[
-        "调研 JWT 方案,考虑 token 刷新和撤销",
-        "调研 Session 方案,寻找 Redis 替代存储"
-    ]
-)
-```
-
-**执行流程**:
-```
-1. 为每个 branch 创建 sub-agent
-   - context = background(或继承主 msg list)
-   - prompt = branch 指令
-2. 串行执行各 sub-agent
-3. 收集结论,汇总返回主会话
-```
-
-**分支 context 初始化**:
-- 有 `background`:LLM 概括的背景信息作为初始 context
-- 无 `background`:继承全部主 message list(适用于 context 不长的情况)
-
-### 数据结构
-
-#### Goal
-
-```python
-@dataclass
-class Goal:
-    id: str                              # 自动生成: "1", "1.1", "2"
-    description: str                     # 目标描述
-    status: Status                       # pending | in_progress | completed | abandoned
-    summary: Optional[str] = None        # 完成/放弃时的总结
-    children: List["Goal"] = field(default_factory=list)
-
-Status = Literal["pending", "in_progress", "completed", "abandoned"]
-
-@dataclass
-class GoalTree:
-    mission: str                         # 总任务描述
-    current_id: Optional[str] = None     # 当前焦点
-    goals: List[Goal] = field(default_factory=list)
-```
-
-#### Message 关联
-
-```python
-# 每条 message 记录它属于哪个 goal
-message = {
-    "role": "assistant",
-    "content": "...",
-    "goal_id": "2.1"  # 关联到目标 2.1
-}
-```
-
 ### Context 管理
 
 #### 1. Plan 注入
 
-每次 LLM 调用时,在 system prompt 末尾注入当前计划状态:
+每次 LLM 调用时,在 system prompt 末尾注入当前计划状态。注入时过滤掉 abandoned 目标,使用连续的显示序号:
 
 ```markdown
 ## Current Plan
 
 **Mission**: 实现用户认证功能
-**Current**: 2.1 实现登录接口
+**Current**: 2.2 实现登录接口
 
 **Progress**:
 [✓] 1. 分析代码
@@ -307,98 +361,510 @@ message = {
 [ ] 3. 测试
 ```
 
+**实现**:`agent/goal/models.py:GoalTree.to_prompt`
+
 #### 2. 完成时压缩
 
 当调用 `goal(done="...")` 时:
-1. 找到该 goal 关联的所有 messages
+1. 找到该 goal 关联的所有 messages(通过 goal_id)
 2. 将详细 messages 替换为一条 summary message
 3. 更新 goal 状态为 completed
 
-#### 3. 回溯时压缩
+#### 3. 回溯(Abandon)
+
+两种模式:
 
-当调用 `goal(abandon="...")` 时:
-1. 找到该 goal 关联的所有 messages
-2. 生成 summary(包含失败原因,供后续参考)
-3. 将详细 messages 替换为 summary message
-4. 更新 goal 状态为 abandoned
+**模式 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...]  ← 这些要压缩
+  [实现方案 A 的 30 条 message...]
   [测试失败的 message...]
-
-Plan:
-  [✓] 1. 分析代码
-  [✓] 2. 实现方案 A
-  [→] 3. 测试
 ```
 
 **After 回溯**:
 ```
-Messages:
-  [分析代码的 20 条 message...]
-  [Summary: "尝试方案 A,因依赖问题失败"]  ← 压缩为 1 条
-  [开始方案 B 的 message...]
+GoalTree 数据(含废弃):
+  [✓] 1. 分析代码               (内部ID: 1)
+  [✗] 2. 实现方案 A              (内部ID: 2, abandoned)
+  [→] 3. 实现方案 B              (内部ID: 4, 新建)
+  [ ] 4. 测试                    (内部ID: 3)
 
-Plan:
+to_prompt() 输出(给 LLM,连续编号):
   [✓] 1. 分析代码
-  [✗] 2. 实现方案 A (abandoned: 依赖问题)
-  [→] 2'. 实现方案 B
+  [→] 2. 实现方案 B  ← current
   [ ] 3. 测试
+
+Messages:
+  [分析代码的 20 条 message...]
+  [Summary: "尝试方案 A,因依赖问题失败"]     ← 原 messages 压缩为 1 条
+  [方案 B 第一条消息,包含废弃分支的 summary]  ← 供 LLM 参考
+  [方案 B 的后续 message...]
 ```
 
+**实现**:`agent/goal/compaction.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/{trace_id}/
-├── goal.json          # Goal Tree(LLM 通过工具维护)
-├── messages.jsonl     # 消息记录(系统自动,含 goal_id)
+├── goal.json          # GoalTree(嵌套 JSON,含 abandoned 目标)
+├── messages/          # Messages(每条独立文件)
+│   ├── {message_id}.json
+│   └── ...
+├── events.jsonl       # 事件流(WebSocket 断线续传)
 └── meta.json          # Trace 元数据
 ```
 
-### 可视化
+---
 
-Goal Tree + Messages 合并展示:
+## 分支-合并设计(explore 工具)
 
+### 场景
+
+```
+主线 Agent:
+  [1] 分析问题
+  [2] explore_start: 启动并行探索 (type=explore_start)
+      │
+      ├── 分支A (sub-agent): [1]设计 → [2]实现 → 完成
+      └── 分支B (sub-agent): [1]设计 → [2]实现 → [3]测试 → 完成
+      │
+      ↓ (工具返回汇总结果)
+  [3] explore_merge: 评估选择JWT (type=explore_merge)
+  [4] 完善实现
 ```
-Mission: 实现用户认证功能
-══════════════════════════════════════════
 
-[✓] 1. 分析代码 (5 steps, 1.2s)
-    → 用户模型在 models/user.py
-    ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
-    ├─ glob_files("**/user*.py")
-    ├─ read_file("models/user.py")
-    └─ [详细步骤已折叠]
+**核心原则**:
+- 每个分支是独立的 sub-agent,有自己的 GoalTree 和 Message List
+- 模型在分支内看到的是简单的连续编号 "1", "2", "3"(独立于主线)
+- `explore_start` 和 `explore_merge` 是主线 GoalTree 中的特殊 Goal 类型
+- 分支数据独立存储,不直接嵌入主线 GoalTree
+- explore 工具返回时自动汇总各分支 summary
 
-[✗] 2. 实现方案 A (abandoned)
-    → 需要 Redis,环境没有
+### 数据结构
 
-[→] 2'. 实现方案 B  ← current
-    ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
-    ├─ read_file("requirements.txt")
-    └─ edit_file("app.py")
+**主线 GoalTree**(不含分支内部 Goals):
 
-[ ] 3. 测试
+```python
+# Goal 类型在前面已定义,这里展示主线 GoalTree 示例
+goals = [
+    Goal(id="1", type="normal", description="分析问题", ...),
+    Goal(id="2", type="explore_start", description="探索认证方案",
+         branch_ids=["A", "B"], ...),
+    Goal(id="3", type="explore_merge", description="选择JWT方案",
+         explore_start_id="2", merge_summary="...", selected_branch="A", ...),
+    Goal(id="4", type="normal", description="完善实现", ...),
+]
+```
+
+**分支上下文**(独立存储):
+
+```python
+@dataclass
+class BranchContext:
+    """分支执行上下文(独立的 sub-agent 环境)"""
+    id: str                          # 分支 ID,如 "A", "B"
+    explore_start_id: str            # 关联的 explore_start Goal ID
+    description: str                 # 探索方向描述
+    status: BranchStatus             # exploring | completed | abandoned
+
+    # 独立的执行环境
+    goal_tree: GoalTree              # 分支自己的 GoalTree(简单编号 1, 2, 3...)
+
+    summary: Optional[str]           # 完成时的总结
+    cumulative_stats: GoalStats      # 累计统计
+    last_message: Optional[Message]  # 最新消息(用于可视化预览)
+```
+
+### 存储结构
+
+```
+.trace/{trace_id}/
+├── meta.json              # Trace 元数据
+├── goal.json              # 主线 GoalTree
+├── messages/              # 主线 Messages
+│   └── ...
+├── branches/              # 分支数据(独立存储)
+│   ├── A/
+│   │   ├── meta.json      # BranchContext 元数据
+│   │   ├── goal.json      # 分支 A 的 GoalTree
+│   │   └── messages/      # 分支 A 的 Messages
+│   └── B/
+│       └── ...
+└── events.jsonl           # 事件流
+```
+
+### DAG 可视化
+
+**折叠视图**(explore 区域显示为 start → merge):
+```
+[1:分析] ──→ [2:explore_start] ──→ [3:explore_merge] ──→ [4:完善]
+                    │                      │
+               (启动2分支)            (汇总评估)
+```
+
+**展开分支视图**(显示并行路径):
+```
+                  ┌──→ [A:JWT方案] ────┐
+[1:分析] ──→ [2] ─┤                    ├──→ [3:合并] ──→ [4:完善]
+                  └──→ [B:Session方案] ┘
+```
+
+**继续展开分支 A 内部**:
+```
+                  ┌──→ [A.1:设计] → [A.2:实现] ──┐
+[1:分析] ──→ [2] ─┤                              ├──→ [3:合并] ──→ [4:完善]
+                  └──→ [B:Session方案] ──────────┘
+```
+
+注意:`[A.1]`, `[A.2]` 是**前端显示格式**,后端存储的是 `(branch_id="A", goal_id="1")`。
+
+### 前端 API
+
+**REST**:返回主线 GoalTree + 分支元数据(不含分支内部 Goals),按需加载分支详情。
+
+```http
+GET /api/traces/{trace_id}
+```
+
+响应:
+```json
+{
+  "goal_tree": {
+    "goals": [
+      {"id": "1", "type": "normal", "description": "分析问题", ...},
+      {"id": "2", "type": "explore_start", "branch_ids": ["A", "B"], ...},
+      {"id": "3", "type": "explore_merge", "explore_start_id": "2", ...},
+      {"id": "4", "type": "normal", ...}
+    ]
+  },
+  "branches": {
+    "A": {
+      "id": "A",
+      "explore_start_id": "2",
+      "description": "JWT方案",
+      "status": "completed",
+      "summary": "JWT方案实现完成,无状态但token较大",
+      "cumulative_stats": {"message_count": 8, "total_tokens": 4000, ...},
+      "goal_count": 2,
+      "last_message": {"role": "assistant", "content": "JWT实现完成...", ...}
+    },
+    "B": {...}
+  }
+}
+```
 
-──────────────────────────────────────────
-Progress: 1/3 goals | Current: 2'
+**按需加载分支详情**:
+
+```http
+GET /api/traces/{trace_id}/branches/{branch_id}
+```
+
+响应:
+```json
+{
+  "id": "A",
+  "description": "JWT方案",
+  "status": "completed",
+  "summary": "...",
+  "goal_tree": {
+    "goals": [
+      {"id": "1", "description": "JWT设计", ...},
+      {"id": "2", "description": "JWT实现", ...}
+    ]
+  },
+  "cumulative_stats": {...}
+}
+```
+
+**WebSocket 事件**:
+
+| 事件 | 触发时机 | payload |
+|------|---------|---------|
+| `branch_started` | 分支开始探索 | explore_start_id, branch 元数据 |
+| `branch_goal_added` | 分支内新增 Goal | branch_id, goal |
+| `branch_message_added` | 分支内新 Message | branch_id, message, affected_goals |
+| `branch_completed` | 分支完成 | branch_id, summary, cumulative_stats, last_message |
+| `explore_completed` | 所有分支完成 | explore_start_id, merge_summary |
+
+### explore 工具流程
+
+```python
+@tool
+def explore(branches: List[str]) -> str:
+    """并行探索多个方向"""
+
+    # 1. 创建 explore_start Goal
+    start_goal = Goal(
+        id=next_id(),
+        type="explore_start",
+        description=f"探索 {len(branches)} 个方案",
+        branch_ids=[chr(ord('A') + i) for i in range(len(branches))],
+    )
+
+    # 2. 为每个方向创建 sub-agent
+    for i, desc in enumerate(branches):
+        branch_id = chr(ord('A') + i)
+        create_branch_context(
+            branch_id=branch_id,
+            explore_start_id=start_goal.id,
+            description=desc,
+        )
+        spawn_sub_agent(branch_id)
+
+    # 3. 等待所有分支完成
+    results = await gather_branch_results()
+
+    # 4. 创建 explore_merge Goal
+    merge_summary = format_exploration_results(results)
+    merge_goal = Goal(
+        id=next_id(),
+        type="explore_merge",
+        description="评估探索结果",
+        explore_start_id=start_goal.id,
+        merge_summary=merge_summary,
+    )
+
+    # 5. 返回汇总给主线 Agent
+    return merge_summary
+```
+
+**汇总结果示例**(作为 explore 工具的返回值):
+```markdown
+## 探索结果
+
+### 分支 A: JWT 方案
+实现完成。优点:无状态,易扩展。缺点:token 较大,无法主动失效。
+
+### 分支 B: Session 方案
+实现完成。优点:token 小,可主动失效。缺点:需要 Redis 存储。
+
+---
+两种方案都已实现,请选择一种继续。
 ```
 
+### Context 压缩
+
+分支完成后的压缩策略:
+
+1. **分支完成时**:分支的详细 context 压缩为 summary,存储在 BranchContext.summary
+2. **explore 完成后**:所有分支的 summary 汇总为 merge_summary
+3. **主线 context**:explore 工具调用被压缩为一条包含 merge_summary 的消息
+
 ---
 
 ## 与 OpenCode 方案的对比
 
 | 方面 | OpenCode | 我们的方案 |
 |------|----------|-----------|
-| Plan 格式 | 纯文本 (plan.md) | 结构化 (goal.json) |
+| Plan 格式 | 纯文本 (plan.md) | 结构化 (GoalTree 嵌套 JSON) |
 | Plan 与执行关联 | 无 | 通过 goal_id 关联 |
+| 执行记录 | Message List | Message List(加 goal_id 元数据) |
 | 压缩时机 | 事后(context 满时) | 增量(goal 完成/放弃时) |
 | 并行探索 | Sub-agent(手动管理) | explore 工具(自动汇总) |
-| 回溯能力 | 有限 | 精确(基于 goal 压缩) |
-| 工具复杂度 | todoread/todowrite | goal/explore(更简单) |
+| 回溯能力 | 有限 | 精确(基于 goal 压缩 + 废弃分支 summary) |
+| 可视化 | 无 | DAG(边可展开/折叠) |
+| 工具复杂度 | todoread/todowrite | goal/explore |
 
 ---
 
@@ -406,34 +872,37 @@ Progress: 1/3 goals | Current: 2'
 
 | 功能 | 文件路径 | 状态 |
 |------|---------|------|
-| Goal 数据模型 | `agent/goal/models.py` | 待实现 |
-| goal 工具 | `agent/goal/tool.py` | 待实现 |
+| Goal 数据模型 | `agent/goal/models.py` | 待调整(ID 映射) |
+| goal 工具 | `agent/goal/tool.py` | 待调整 |
 | explore 工具 | `agent/goal/explore.py` | 待实现 |
-| Context 压缩 | `agent/goal/compaction.py` | 待实现 |
-| Plan 注入 | `agent/core/runner.py` | 待实现 |
-| 可视化 | `agent/goal/visualize.py` | 待实现 |
+| Context 压缩 | `agent/goal/compaction.py` | 待调整 |
+| Message 数据模型 | `agent/execution/models.py` | 待调整(Step→Message) |
+| TraceStore 协议 | `agent/execution/protocols.py` | 待调整 |
+| DAG 可视化 API | `agent/execution/api.py` | 待调整(tree→DAG) |
+| WebSocket 推送 | `agent/execution/websocket.py` | 待调整(新增 goal 事件) |
+| Plan 注入 | `agent/core/runner.py` | 待调整 |
 
 ---
 
 ## 渐进式实现计划
 
 ### Phase 1: 基础 goal 工具
-- Goal 数据结构
+- GoalTree 数据结构(含 ID 映射)
 - goal 工具(add, done, focus)
-- Plan 注入到 system prompt
-- 基础可视化
+- Plan 注入到 system prompt(含显示序号生成)
+- Message 模型(替代 Step)
 
 ### Phase 2: 回溯支持
-- abandon 操作
-- Message 关联 goal_id
+- abandon 操作(两种模式)
+- 废弃分支 summary 累积到新分支
 - 基于 goal 的 context 压缩
 
-### Phase 3: 并行探索
+### Phase 3: 可视化
+- DAG 视图 API
+- WebSocket goal/message 事件
+- 展开/折叠逻辑
+
+### Phase 4: 并行探索
 - explore 工具
 - 独立 message list 管理
 - 结果汇总机制
-
-### Phase 4: 优化
-- 更智能的压缩策略
-- 可视化增强
-- 性能优化

+ 133 - 273
docs/trace-api.md

@@ -1,26 +1,26 @@
-# Trace 模块 - Context 管理 + 可视化
+# Trace 模块 - 执行记录存储
 
-> 执行轨迹记录、存储和可视化 API
+> 执行轨迹记录和存储的后端实现
 
 ---
 
 ## 架构概览
 
-**职责定位**:`agent/execution` 模块负责所有 Trace/Step 相关功能
+**职责定位**:`agent/execution` 模块负责所有 Trace/Message 相关功能
 
 ```
 agent/execution/
-├── models.py          # Trace/Step 数据模型
+├── models.py          # Trace/Message 数据模型
 ├── protocols.py       # TraceStore 存储接口
-├── memory_store.py    # 内存存储实现
-├── api.py             # RESTful API(懒加载)
+├── fs_store.py        # 文件系统存储实现
+├── api.py             # RESTful API
 └── websocket.py       # WebSocket 实时推送
 ```
 
 **设计原则**:
-- **高内聚**:所有 Trace 相关代码在一个模块
-- **松耦合**:核心模型不依赖 FastAPI
-- **可扩展**:易于添加 PostgreSQL/Neo4j 实现
+- **高内聚**:所有 Trace 相关代码在一个模块
+- **松耦合**:核心模型不依赖 FastAPI
+- **可扩展**:易于添加 PostgreSQL 等存储实现
 
 ---
 
@@ -31,353 +31,213 @@ agent/execution/
 一次完整的 LLM 交互(单次调用或 Agent 任务)
 
 ```python
-from agent.trace import Trace
+trace = Trace.create(mode="agent", task="探索代码库")
 
-trace = Trace.create(
-    mode="agent",
-    task="探索代码库",
-    agent_type="researcher"
-)
-
-# 字段说明
 trace.trace_id        # UUID
 trace.mode            # "call" | "agent"
 trace.task            # 任务描述
 trace.status          # "running" | "completed" | "failed"
-trace.total_steps     # Step 总数
+trace.total_messages  # Message 总数
 trace.total_tokens    # Token 总数
 trace.total_cost      # 总成本
+trace.current_goal_id # 当前焦点 goal
 ```
 
-### Step - 执行步骤
+**实现**:`agent/execution/models.py:Trace`
 
-Trace 中的原子操作,形成树结构
+### Message - 执行消息
 
-```python
-from agent.trace import Step
+对应 LLM API 消息,加上元数据。通过 `goal_id` 和 `branch_id` 关联 GoalTree 中的目标。
 
-step = Step.create(
+```python
+# assistant 消息(模型返回,可能含 text + tool_calls)
+assistant_msg = Message.create(
     trace_id=trace.trace_id,
-    step_type="action",
-    sequence=1,
-    description="glob_files",
-    parent_id=parent_step_id,  # 树结构
-    data={
-        "tool_name": "glob_files",
-        "arguments": {"pattern": "**/*.py"}
-    }
+    role="assistant",
+    goal_id="3",                    # 内部 ID(纯自增)
+    branch_id=None,                 # 主线消息
+    content={"text": "...", "tool_calls": [...]},
 )
 
-# Step 类型
-# - goal: 目标/计划项
-# - thought: 思考/分析
-# - action: 工具调用
-# - result: 工具结果
-# - response: 最终回复
-# - memory_read/write: 记忆操作
-# - feedback: 人工反馈
+# 分支内的 tool 消息
+tool_msg = Message.create(
+    trace_id=trace.trace_id,
+    role="tool",
+    goal_id="1",                    # 分支内的 goal ID(分支内独立编号)
+    branch_id="A",                  # 分支 A
+    tool_call_id="call_abc123",
+    content="工具执行结果",
+)
 ```
 
+**实现**:`agent/execution/models.py:Message`
+
 ---
 
 ## 存储接口
 
 ### TraceStore Protocol
 
-定义所有存储实现必须遵守的接口
-
 ```python
-from agent.trace import TraceStore
-
-class MyCustomStore:
-    """实现 TraceStore 接口的所有方法"""
-
+class TraceStore(Protocol):
+    # Trace 操作
     async def create_trace(self, trace: Trace) -> str: ...
     async def get_trace(self, trace_id: str) -> Optional[Trace]: ...
+    async def update_trace(self, trace_id: str, **updates) -> None: ...
     async def list_traces(self, ...) -> List[Trace]: ...
 
-    async def add_step(self, step: Step) -> str: ...
-    async def get_step(self, step_id: str) -> Optional[Step]: ...
-    async def get_trace_steps(self, trace_id: str) -> List[Step]: ...
-    async def get_step_children(self, step_id: str) -> List[Step]: ...
+    # GoalTree 操作
+    async def get_goal_tree(self, trace_id: str) -> Optional[GoalTree]: ...
+    async def update_goal_tree(self, trace_id: str, tree: GoalTree) -> None: ...
+    async def add_goal(self, trace_id: str, goal: Goal) -> None: ...
+    async def update_goal(self, trace_id: str, goal_id: str, **updates) -> None: ...
+
+    # Branch 操作(分支独立存储)
+    async def create_branch(self, trace_id: str, branch: BranchContext) -> None: ...
+    async def get_branch(self, trace_id: str, branch_id: str) -> Optional[BranchContext]: ...
+    async def get_branch_detail(self, trace_id: str, branch_id: str) -> Optional[BranchDetail]: ...
+    async def update_branch(self, trace_id: str, branch_id: str, **updates) -> None: ...
+    async def list_branches(self, trace_id: str) -> Dict[str, BranchContext]: ...
+
+    # Message 操作
+    async def add_message(self, message: Message) -> str: ...
+    async def get_message(self, message_id: str) -> Optional[Message]: ...
+    async def get_trace_messages(self, trace_id: str) -> List[Message]: ...
+    async def get_messages_by_goal(self, trace_id: str, goal_id: str) -> List[Message]: ...
+    async def get_messages_by_branch(self, trace_id: str, branch_id: str) -> List[Message]: ...
+    async def update_message(self, message_id: str, **updates) -> None: ...
+
+    # 事件流(WebSocket 断线续传)
+    async def get_events(self, trace_id: str, since_event_id: int) -> List[Dict]: ...
+    async def append_event(self, trace_id: str, event_type: str, payload: Dict) -> int: ...
 ```
 
-### FileSystemTraceStore
+**实现**:`agent/execution/protocols.py`
 
-文件系统存储实现(支持跨进程和持久化)
+### FileSystemTraceStore
 
 ```python
 from agent.execution import FileSystemTraceStore
 
 store = FileSystemTraceStore(base_path=".trace")
-
-# 使用方法
-trace_id = await store.create_trace(trace)
-trace = await store.get_trace(trace_id)
-steps = await store.get_trace_steps(trace_id)
 ```
 
----
-
-## API 服务
-
-### 启动服务
-
-```bash
-# 1. 安装依赖
-pip install -r requirements.txt
+**目录结构**:
+```
+.trace/{trace_id}/
+├── meta.json           # Trace 元数据
+├── goal.json           # 主线 GoalTree(扁平 JSON,通过 parent_id 构建层级)
+├── messages/           # 主线 Messages(每条独立文件)
+│   ├── {message_id}.json
+│   └── ...
+├── branches/           # 分支数据(独立存储)
+│   ├── A/
+│   │   ├── meta.json   # BranchContext 元数据
+│   │   ├── goal.json   # 分支 A 的 GoalTree
+│   │   └── messages/   # 分支 A 的 Messages
+│   └── B/
+│       └── ...
+└── events.jsonl        # 事件流(WebSocket 续传)
+```
 
-# 2. 启动服务
-python api_server.py
+**实现**:`agent/execution/fs_store.py`
 
-# 3. 访问 API 文档
-open http://localhost:8000/docs
-```
+---
 
-### RESTful 端点
+## REST API 端点
 
-#### 1. 列出 Traces
+### 1. 列出 Traces
 
 ```http
 GET /api/traces?mode=agent&status=running&limit=20
 ```
 
-**响应**:
-```json
-{
-  "traces": [
-    {
-      "trace_id": "abc123",
-      "mode": "agent",
-      "task": "探索代码库",
-      "status": "running",
-      "total_steps": 15,
-      "total_tokens": 5000,
-      "total_cost": 0.05
-    }
-  ]
-}
-```
-
-#### 2. 获取完整树(小型 Trace)
+### 2. 获取 Trace + GoalTree + 分支元数据
 
 ```http
-GET /api/traces/{trace_id}/tree
+GET /api/traces/{trace_id}
 ```
 
-**响应**:递归 Step 树(完整)
+返回 Trace 元数据、主线 GoalTree(扁平列表,含所有 Goal 及其 stats)、分支元数据(不含分支内部 GoalTree)。
 
-#### 3. 懒加载节点(大型 Trace)
+### 3. 获取 Messages
 
 ```http
-GET /api/traces/{trace_id}/node/{step_id}?expand=true&max_depth=2
+GET /api/traces/{trace_id}/messages?goal_id=3
+GET /api/traces/{trace_id}/messages?branch_id=A
 ```
 
-**参数**:
-- `step_id`: Step ID(`null` 表示根节点)
-- `expand`: 是否加载子节点
-- `max_depth`: 递归深度(1-10)
+返回指定 Goal 或分支关联的所有 Messages(用于查看执行详情)。
 
-**核心算法**:简洁的层级懒加载(< 30 行)
-
-```python
-async def _build_tree(store, trace_id, step_id, expand, max_depth, current_depth):
-    # 1. 获取当前层节点
-    if step_id is None:
-        nodes = [s for s in steps if s.parent_id is None]
-    else:
-        nodes = await store.get_step_children(step_id)
-
-    # 2. 构建响应
-    result = []
-    for step in nodes:
-        node_dict = step.to_dict()
-        node_dict["children"] = []
-
-        # 3. 递归加载子节点(可选)
-        if expand and current_depth < max_depth:
-            node_dict["children"] = await _build_tree(...)
-
-        result.append(node_dict)
-
-    return result
-```
+### 4. 获取分支详情(按需加载)
 
-### WebSocket 推送
-
-实时监听进行中 Trace 的更新
-
-```javascript
-// 连接
-ws = new WebSocket(`/api/traces/${trace_id}/watch`)
-
-// 事件
-ws.onmessage = (e) => {
-  const event = JSON.parse(e.data)
-
-  switch (event.event) {
-    case "connected":
-      console.log("已连接")
-      break
-    case "step_added":
-      // 新增 Step
-      addStepToTree(event.step)
-      break
-    case "step_updated":
-      // Step 状态更新
-      updateStep(event.step_id, event.updates)
-      break
-    case "trace_completed":
-      // Trace 完成
-      console.log("完成")
-      ws.close()
-      break
-  }
-}
+```http
+GET /api/traces/{trace_id}/branches/{branch_id}
 ```
 
----
-
-## 使用场景
+返回分支完整详情,包括分支内的 GoalTree(用于展开分支时加载)。
 
-### 1. Agent 执行时记录 Trace
+**实现**:`agent/execution/api.py`
 
-```python
-from agent import AgentRunner
-from agent.execution import FileSystemTraceStore
+---
 
-# 初始化
-store = FileSystemTraceStore(base_path=".trace")
-runner = AgentRunner(trace_store=store, llm_call=my_llm_fn)
+## WebSocket 事件
 
-# 执行 Agent(自动记录 Trace)
-async for event in runner.run(task="探索代码库"):
-    print(event)
+### 连接
 
-# 查询 Trace
-traces = await store.list_traces(mode="agent", limit=10)
-steps = await store.get_trace_steps(traces[0].trace_id)
 ```
-
-### 2. 前端可视化(小型 Trace)
-
-```javascript
-// 一次性加载完整树
-const response = await fetch(`/api/traces/${traceId}/tree`)
-const { root_steps } = await response.json()
-
-// 渲染树
-renderTree(root_steps)
+ws://localhost:8000/api/traces/{trace_id}/watch?since_event_id=0
 ```
 
-### 3. 前端可视化(大型 Trace)
-
-```javascript
-// 懒加载:只加载根节点
-const response = await fetch(`/api/traces/${traceId}/node/null?expand=false`)
-const { children } = await response.json()
-
-// 用户点击展开时
-async function expandNode(stepId) {
-  const response = await fetch(
-    `/api/traces/${traceId}/node/${stepId}?expand=true&max_depth=1`
-  )
-  const { children } = await response.json()
-  return children
-}
-```
+### 事件类型
 
-### 4. 实时监控进行中的任务
-
-```javascript
-// WebSocket 监听
-ws = new WebSocket(`/api/traces/${traceId}/watch`)
-ws.onmessage = (e) => {
-  const event = JSON.parse(e.data)
-  if (event.event === "step_added") {
-    // 实时添加新 Step 到 UI
-    appendStep(event.step)
-  }
-}
-```
+| 事件 | 触发时机 | payload |
+|------|---------|---------|
+| `connected` | WebSocket 连接成功 | trace_id, current_event_id, goal_tree, branches(分支元数据) |
+| `goal_added` | 新增 Goal | goal 完整数据(含 stats, parent_id, branch_id, type) |
+| `goal_updated` | Goal 状态变化(含级联完成) | goal_id, updates, affected_goals(含 cumulative_stats) |
+| `message_added` | 新 Message | message 数据(含 goal_id, branch_id),affected_goals,affected_branches |
+| `branch_started` | 分支开始探索 | explore_start_id, branch 元数据 |
+| `branch_goal_added` | 分支内新增 Goal | branch_id, goal |
+| `branch_completed` | 分支完成 | explore_start_id, branch_id, summary, cumulative_stats, last_message |
+| `explore_completed` | 所有分支完成 | explore_start_id, merge_summary |
+| `trace_completed` | 执行完成 | 统计信息 |
 
----
+### Stats 更新逻辑
 
-## 扩展存储实现
+每次添加 Message 时,后端执行:
+1. 更新对应 Goal 的 `self_stats`
+2. 沿 `parent_id` 链向上更新所有祖先的 `cumulative_stats`
+3. 如果是分支内 Message(branch_id 非空),还需更新所属 Branch 的 `cumulative_stats`
+4. 在 `message_added` 事件的 `affected_goals` 和 `affected_branches` 中推送所有受影响的最新 stats
 
-### PostgreSQL 实现(未来)
+**分支统计**:
+- Branch 的 `cumulative_stats`:分支内所有 Goals 的累计统计
+- explore_start Goal 的 `cumulative_stats`:所有关联分支的 cumulative_stats 之和
 
-```python
-from agent.trace import TraceStore, Trace, Step
-
-class PostgreSQLTraceStore:
-    """PostgreSQL 存储实现"""
-
-    def __init__(self, connection_string: str):
-        self.pool = create_pool(connection_string)
-
-    async def create_trace(self, trace: Trace) -> str:
-        async with self.pool.acquire() as conn:
-            await conn.execute(
-                "INSERT INTO traces (...) VALUES (...)",
-                trace.to_dict()
-            )
-        return trace.trace_id
-
-    async def get_step_children(self, step_id: str) -> List[Step]:
-        # 使用递归 CTE 优化查询
-        query = """
-        WITH RECURSIVE subtree AS (
-          SELECT * FROM steps WHERE parent_id = $1
-        )
-        SELECT * FROM subtree ORDER BY sequence
-        """
-        # ...
-```
+**实现**:`agent/execution/websocket.py`
 
 ---
 
-## 导入路径(唯一正确方式)
+## 使用场景
+
+### Agent 执行时记录
 
 ```python
-# ✅ 推荐导入
-from agent.execution import Trace, Step, StepType, StepStatus
-from agent.execution import TraceStore, FileSystemTraceStore
+from agent import AgentRunner
+from agent.execution import FileSystemTraceStore
 
-# ✅ 顶层导入(等价)
-from agent import Trace, Step, TraceStore
+store = FileSystemTraceStore(base_path=".trace")
+runner = AgentRunner(trace_store=store, llm_call=my_llm_fn)
 
-# ❌ 旧导入(已删除,会报错)
-from agent.models.trace import Trace  # ModuleNotFoundError
-from agent.storage.protocols import TraceStore  # ImportError
+async for event in runner.run(task="探索代码库"):
+    print(event)  # Trace 或 Message
 ```
 
 ---
 
-## 性能优化
-
-### 小型 Trace(< 100 Steps)
-
-- **推荐**:使用 `/tree` 一次性加载
-- **优点**:最少请求数,前端体验最优
-- **缺点**:单次响应较大
-
-### 大型 Trace(> 100 Steps)
-
-- **推荐**:使用 `/node/{step_id}` 懒加载
-- **优点**:按需加载,内存占用小
-- **缺点**:需要多次请求
-
-### WebSocket vs 轮询
-
-- **进行中任务**:使用 WebSocket(实时推送)
-- **历史任务**:使用 RESTful(静态数据)
-
----
-
 ## 相关文档
 
-- [agent/execution/models.py](../agent/execution/models.py) - Trace/Step 模型定义
-- [agent/execution/api.py](../agent/execution/api.py) - RESTful API 实现
-- [api_server.py](../api_server.py) - FastAPI 应用入口
-- [requirements.txt](../requirements.txt) - FastAPI 依赖
+- [frontend/API.md](../frontend/API.md) - 前端对接 API 文档
+- [docs/context-management.md](./context-management.md) - Context 管理完整设计
+- [agent/goal/models.py](../agent/goal/models.py) - GoalTree 模型定义

+ 686 - 375
frontend/API.md

@@ -1,25 +1,41 @@
 # Agent Execution API - 前端对接文档
 
-> 版本:v2.0
-> 更新日期:2026-02-03
+> 版本:v3.0
+> 更新日期:2026-02-04
 
 ---
 
-## 📋 概览
+## 概览
 
-本 API 提供 Agent 执行过程的实时可视化能力,包括
-- **REST API** - 查询历史数据、获取完整 Step 树
-- **WebSocket** - 实时推送 Step 更新(支持断线续传)
+本 API 提供 Agent 执行过程的实时可视化能力:
+- **REST API** - 查询 Trace 和 GoalTree
+- **WebSocket** - 实时推送 Goal/Message 更新(支持断线续传)
 
 **核心概念**:
-- **Trace** - 一次完整的任务执行(如一次 Agent 运行)
-- **Step** - 执行过程中的一个原子操作,形成树结构
-- **Event** - Step 的变更事件(用于 WebSocket 推送和断线续传)
+- **Trace** - 一次完整的任务执行
+- **GoalTree** - 嵌套的目标树
+- **Goal** - 一个目标节点,包含 self_stats(自身统计)和 cumulative_stats(含后代统计)
+- **Message** - 执行消息,对应 LLM API 的 assistant/tool 消息,是详细执行数据
+
+**数据结构**:
+```
+后端存储两类数据:
+1. GoalTree(嵌套 JSON)- 目标树结构 + 聚合统计(stats)
+2. Messages(扁平列表)- 详细执行记录,通过 goal_id 关联 Goal
+
+关系:Goal.stats 是从关联的 Messages 聚合计算出来的
+```
+
+**DAG 可视化**(前端负责):
+- 从 GoalTree 生成 DAG 视图
+- 节点 = Goal 完成后的里程碑
+- 边 = 相邻节点之间的执行过程
+- 边的统计数据从 target Goal 的 stats 获取(折叠用 `cumulative_stats`,展开用 `self_stats`)
+- 边的详细内容需要查询该 Goal 关联的 Messages
 
-可以运行 python3 examples/feature_extract/run.py 来生成数据。
 ---
 
-## 🌐 REST API
+## REST API
 
 ### 基础信息
 
@@ -30,8 +46,6 @@
 
 ### 1. 列出 Traces
 
-获取 Trace 列表(支持过滤)
-
 ```http
 GET /api/traces?status=running&limit=20
 ```
@@ -50,12 +64,13 @@ GET /api/traces?status=running&limit=20
     {
       "trace_id": "abc123",
       "mode": "agent",
-      "task": "分析代码库结构",
+      "task": "实现用户认证功能",
       "status": "running",
-      "total_steps": 15,
+      "total_messages": 15,
       "total_tokens": 5000,
       "total_cost": 0.05,
-      "created_at": "2026-02-03T15:30:00"
+      "current_goal_id": "2.1",
+      "created_at": "2026-02-04T15:30:00"
     }
   ],
   "total": 1
@@ -64,7 +79,7 @@ GET /api/traces?status=running&limit=20
 
 ---
 
-### 2. 获取 Trace 元数据
+### 2. 获取 Trace + GoalTree
 
 ```http
 GET /api/traces/{trace_id}
@@ -75,114 +90,212 @@ GET /api/traces/{trace_id}
 {
   "trace_id": "abc123",
   "mode": "agent",
-  "task": "分析代码库结构",
+  "task": "实现用户认证功能",
   "status": "running",
-  "total_steps": 15,
+  "total_messages": 15,
   "total_tokens": 5000,
   "total_cost": 0.05,
-  "total_duration_ms": 12345,
-  "last_sequence": 15,
-  "last_event_id": 15,
-  "created_at": "2026-02-03T15:30:00",
-  "completed_at": null
+  "created_at": "2026-02-04T15:30:00",
+  "completed_at": null,
+  "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",
+        "summary": null,
+        "self_stats": { "message_count": 0, "total_tokens": 0, "total_cost": 0.0, "preview": null },
+        "cumulative_stats": { "message_count": 8, "total_tokens": 4200, "total_cost": 0.05, "preview": "read → edit × 3 → bash" }
+      },
+      {
+        "id": "3",
+        "parent_id": "2",
+        "branch_id": null,
+        "type": "normal",
+        "description": "设计接口",
+        "reason": "先定义 API 契约",
+        "status": "completed",
+        "summary": "RESTful API 设计完成",
+        "self_stats": { "message_count": 3, "total_tokens": 1500, "total_cost": 0.02, "preview": "read → edit" },
+        "cumulative_stats": { "message_count": 3, "total_tokens": 1500, "total_cost": 0.02, "preview": "read → edit" }
+      },
+      {
+        "id": "4",
+        "parent_id": "2",
+        "branch_id": null,
+        "type": "normal",
+        "description": "实现代码",
+        "reason": "按设计实现",
+        "status": "in_progress",
+        "summary": null,
+        "self_stats": { "message_count": 5, "total_tokens": 2700, "total_cost": 0.03, "preview": "edit × 3 → bash" },
+        "cumulative_stats": { "message_count": 5, "total_tokens": 2700, "total_cost": 0.03, "preview": "edit × 3 → bash" }
+      },
+      {
+        "id": "5",
+        "parent_id": null,
+        "branch_id": null,
+        "type": "normal",
+        "description": "测试",
+        "reason": "验证功能正确性",
+        "status": "pending",
+        "summary": 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 }
+      }
+    ]
+  },
+  "branches": {}
 }
 ```
 
 ---
 
-### 3. 获取完整 Step 树 ⭐ 重要
+### 3. 获取 Messages(边详情)
 
-获取 Trace 的完整 Step 树(适合小型 Trace,<100 个 Step)
+查询 Goal 关联的所有 Messages(用于查看边的详细执行内容)。
 
 ```http
-GET /api/traces/{trace_id}/tree?view=compact&max_depth=10
+GET /api/traces/{trace_id}/messages?goal_id=3
+GET /api/traces/{trace_id}/messages?branch_id=A
 ```
 
 **查询参数**:
-| 参数 | 类型 | 默认值 | 说明 |
-|------|------|--------|------|
-| `view` | string | `compact` | `compact` - 精简视图 / `full` - 完整视图 |
-| `max_depth` | int | 无限 | 最大树深度 |
+| 参数 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `goal_id` | string | 否 | 过滤指定 Goal 的 Messages(内部 ID)|
+| `branch_id` | string | 否 | 过滤指定分支的所有 Messages |
 
 **响应示例**:
 ```json
 {
-  "trace_id": "abc123",
-  "root_steps": [
+  "messages": [
     {
-      "step_id": "step-001",
-      "step_type": "thought",
-      "status": "completed",
-      "sequence": 1,
-      "parent_id": null,
-      "description": "分析项目结构...",
-      "has_children": true,
-      "children_count": 2,
-      "duration_ms": 1234,
-      "tokens": 500,
-      "cost": 0.005,
-      "created_at": "2026-02-03T15:30:01",
-      "data": {
-        "content": "让我先看看项目的目录结构...",
-        "model": "claude-sonnet-4.5"
+      "message_id": "msg-001",
+      "role": "assistant",
+      "sequence": 6,
+      "goal_id": "3",
+      "branch_id": null,
+      "content": {
+        "text": "让我先读取现有的 API 设计...",
+        "tool_calls": [
+          {
+            "id": "call_abc",
+            "name": "read_file",
+            "arguments": { "path": "api/routes.py" }
+          }
+        ]
       },
-      "children": [
-        {
-          "step_id": "step-002",
-          "step_type": "action",
-          "status": "completed",
-          "parent_id": "step-001",
-          "description": "glob_files(**/*.py)",
-          "data": {
-            "tool_name": "glob_files",
-            "arguments": {"pattern": "**/*.py"}
-          },
-          "children": [
-            {
-              "step_id": "step-003",
-              "step_type": "result",
-              "status": "completed",
-              "parent_id": "step-002",
-              "data": {
-                "tool_name": "glob_files",
-                "output": ["src/main.py", "src/utils.py"]
-              }
-            }
-          ]
-        }
-      ]
+      "tokens": 150,
+      "cost": 0.002,
+      "created_at": "2026-02-04T15:31:00"
+    },
+    {
+      "message_id": "msg-002",
+      "role": "tool",
+      "sequence": 7,
+      "goal_id": "3",
+      "branch_id": null,
+      "tool_call_id": "call_abc",
+      "content": "# API Routes\n...",
+      "tokens": null,
+      "cost": null,
+      "created_at": "2026-02-04T15:31:01"
+    },
+    {
+      "message_id": "msg-003",
+      "role": "assistant",
+      "sequence": 8,
+      "goal_id": "3",
+      "branch_id": null,
+      "content": {
+        "text": "现有 API 使用 REST 风格,我来设计新的认证接口...",
+        "tool_calls": [
+          {
+            "id": "call_def",
+            "name": "edit_file",
+            "arguments": { "path": "api/auth.py", "content": "..." }
+          }
+        ]
+      },
+      "tokens": 300,
+      "cost": 0.004,
+      "created_at": "2026-02-04T15:31:30"
     }
-  ]
+  ],
+  "total": 3
 }
 ```
 
-**注意**:
-- `children` 字段包含嵌套的子节点(递归结构)
-- `compact` 视图:`data` 中的大字段(如 `messages`)会被省略
-- `full` 视图:返回所有字段(数据量可能很大)
-
 ---
 
-### 4. 懒加载单个节点
-
-适用于大型 Trace(>100 Step),按需加载子树
+### 4. 获取分支详情(按需加载)
 
 ```http
-GET /api/traces/{trace_id}/node/{step_id}?expand=true&max_depth=2
+GET /api/traces/{trace_id}/branches/{branch_id}
 ```
 
-**查询参数**:
-| 参数 | 类型 | 默认值 | 说明 |
-|------|------|--------|------|
-| `expand` | bool | `false` | 是否展开子节点 |
-| `max_depth` | int | 1 | 展开深度 |
-| `view` | string | `compact` | 视图类型 |
-
-**响应**:与 `/tree` 格式相同,但只返回指定节点及其子树。
+**响应示例**:
+```json
+{
+  "id": "A",
+  "explore_start_id": "6",
+  "description": "JWT 方案",
+  "status": "completed",
+  "summary": "JWT 方案实现完成,无状态但 token 较大",
+  "goal_tree": {
+    "mission": "探索 JWT 方案",
+    "current_id": null,
+    "goals": [
+      {
+        "id": "1",
+        "parent_id": null,
+        "branch_id": "A",
+        "type": "normal",
+        "description": "JWT 设计",
+        "status": "completed",
+        "summary": "设计完成",
+        "self_stats": { "message_count": 3, "total_tokens": 1500, "total_cost": 0.02, "preview": "read → edit" },
+        "cumulative_stats": { "message_count": 3, "total_tokens": 1500, "total_cost": 0.02, "preview": "read → edit" }
+      },
+      {
+        "id": "2",
+        "parent_id": null,
+        "branch_id": "A",
+        "type": "normal",
+        "description": "JWT 实现",
+        "status": "completed",
+        "summary": "实现完成",
+        "self_stats": { "message_count": 5, "total_tokens": 2500, "total_cost": 0.03, "preview": "edit × 3 → bash" },
+        "cumulative_stats": { "message_count": 5, "total_tokens": 2500, "total_cost": 0.03, "preview": "edit × 3 → bash" }
+      }
+    ]
+  },
+  "cumulative_stats": { "message_count": 8, "total_tokens": 4000, "total_cost": 0.05, "preview": "read → edit × 4 → bash" }
+}
+```
 
 ---
 
-## ⚡ WebSocket API
+## WebSocket API
 
 ### 连接
 
@@ -195,7 +308,7 @@ const ws = new WebSocket(
 **查询参数**:
 | 参数 | 类型 | 默认值 | 说明 |
 |------|------|--------|------|
-| `since_event_id` | int | `0` | 从哪个事件 ID 开始<br>`0` = 补发所有历史<br>`>0` = 只补发指定 ID 之后的 |
+| `since_event_id` | int | `0` | 从哪个事件 ID 开始。`0` = 补发所有历史 |
 
 ---
 
@@ -203,195 +316,283 @@ const ws = new WebSocket(
 
 #### 1. connected(连接成功)
 
+连接后推送完整 GoalTree,前端据此初始化 DAG。
+
 ```json
 {
   "event": "connected",
   "trace_id": "abc123",
-  "current_event_id": 15
+  "current_event_id": 15,
+  "goal_tree": {
+    "mission": "实现用户认证功能",
+    "current_id": "2.1",
+    "goals": [...]
+  }
 }
 ```
 
-**说明**:连接建立后的第一条消息,包含当前最新的 event_id
-
 **前端处理**:
 ```javascript
 if (data.event === 'connected') {
-  // 保存 event_id 用于断线重连
+  initDAG(data.goal_tree)
   localStorage.setItem('last_event_id', data.current_event_id)
 }
 ```
 
 ---
 
-#### 2. step_added(新增 Step)⭐ 最常用
+#### 2. goal_added(新增 Goal)
 
 ```json
 {
-  "event": "step_added",
+  "event": "goal_added",
   "event_id": 16,
-  "ts": "2026-02-03T15:30:10.123456",
-  "step": {
-    "step_id": "step-016",
-    "step_type": "action",
-    "status": "completed",
-    "sequence": 16,
-    "parent_id": "step-001",
-    "description": "read_file(config.yaml)",
-    "has_children": false,
-    "children_count": 0,
-    "duration_ms": 50,
-    "data": {
-      "tool_name": "read_file",
-      "arguments": {"file_path": "config.yaml"}
-    }
-  }
+  "goal": {
+    "id": "6",
+    "parent_id": "2",
+    "branch_id": null,
+    "type": "normal",
+    "description": "编写测试",
+    "reason": "确保代码质量",
+    "status": "pending",
+    "summary": 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 }
+  },
+  "parent_id": "2"
 }
 ```
 
 **前端处理**:
 ```javascript
-if (data.event === 'step_added') {
-  // 添加到树结构
-  addStepToTree(data.step)
-
-  // 更新 event_id
-  localStorage.setItem('last_event_id', data.event_id)
+if (data.event === 'goal_added') {
+  insertGoal(data.goal, data.parent_id)
+  regenerateDAG()
 }
 ```
 
 ---
 
-#### 3. step_updated(Step 更新)
+#### 3. goal_updated(Goal 状态变化)
+
+包含级联完成场景:当所有子 Goal 完成时,父 Goal 自动 completed。
 
 ```json
 {
-  "event": "step_updated",
+  "event": "goal_updated",
   "event_id": 17,
-  "ts": "2026-02-03T15:30:15.123456",
-  "step_id": "step-016",
-  "patch": {
+  "goal_id": "3",
+  "updates": {
     "status": "completed",
-    "duration_ms": 1234
-  }
+    "summary": "接口设计完成"
+  },
+  "affected_goals": [
+    {
+      "goal_id": "3",
+      "cumulative_stats": { "message_count": 3, "total_tokens": 1500, "total_cost": 0.02, "preview": "read → edit" }
+    },
+    {
+      "goal_id": "2",
+      "status": "completed",
+      "summary": "功能实现完成",
+      "cumulative_stats": { "message_count": 8, "total_tokens": 4200, "total_cost": 0.05, "preview": "..." }
+    }
+  ]
 }
 ```
 
-**说明**:`patch` 是增量更新(只包含变化的字段)
-
 **前端处理**:
 ```javascript
-if (data.event === 'step_updated') {
-  const step = findStepById(data.step_id)
-  Object.assign(step, data.patch)
-  updateUI()
+if (data.event === 'goal_updated') {
+  updateGoal(data.goal_id, data.updates)
+  for (const g of data.affected_goals) {
+    updateGoalStats(g.goal_id, g)
+  }
+  regenerateDAG()
 }
 ```
 
 ---
 
-#### 4. trace_completed(任务完成)
+#### 4. message_added(新 Message)
+
+后端更新统计后推送,包含受影响的所有 Goals。
 
 ```json
 {
-  "event": "trace_completed",
+  "event": "message_added",
   "event_id": 18,
-  "ts": "2026-02-03T15:35:00.123456",
-  "trace_id": "abc123",
-  "total_steps": 18
+  "message": {
+    "message_id": "msg-018",
+    "role": "assistant",
+    "goal_id": "4",
+    "branch_id": null,
+    "content": { "text": "...", "tool_calls": [...] },
+    "tokens": 500,
+    "cost": 0.005
+  },
+  "affected_goals": [
+    {
+      "goal_id": "4",
+      "self_stats": { "message_count": 6, "total_tokens": 3200, "total_cost": 0.035, "preview": "edit × 3 → bash × 2" },
+      "cumulative_stats": { "message_count": 6, "total_tokens": 3200, "total_cost": 0.035, "preview": "edit × 3 → bash × 2" }
+    },
+    {
+      "goal_id": "2",
+      "cumulative_stats": { "message_count": 9, "total_tokens": 4700, "total_cost": 0.055, "preview": "read → edit × 4 → bash × 2" }
+    }
+  ]
 }
 ```
 
+**说明**:
+- `affected_goals[0]` 是直接关联的 Goal,更新 `self_stats` + `cumulative_stats`
+- 后续是所有祖先 Goal,仅更新 `cumulative_stats`
+
 **前端处理**:
 ```javascript
-if (data.event === 'trace_completed') {
-  console.log('Task completed!')
-  ws.close()
+if (data.event === 'message_added') {
+  for (const g of data.affected_goals) {
+    updateGoalStats(g.goal_id, g)
+  }
+  // 根据当前展开状态更新对应边
+  rerenderEdge(data.message.goal_id)
 }
 ```
 
 ---
 
-#### 5. error(错误)
+#### 5. trace_completed(任务完成
 
 ```json
 {
-  "event": "error",
-  "message": "Too many missed events (150), please reload full tree via REST API"
+  "event": "trace_completed",
+  "event_id": 50,
+  "trace_id": "abc123",
+  "total_messages": 50,
+  "total_tokens": 25000,
+  "total_cost": 0.25
 }
 ```
 
-**说明**:
-- 如果断线期间产生超过 100 条事件,会收到此错误
-- 此时应该用 REST API 重新加载完整树
+**前端处理**:
+```javascript
+if (data.event === 'trace_completed') {
+  markTraceCompleted()
+  ws.close()
+}
+```
 
 ---
 
-### 断线续传
+#### 6. branch_started(分支开始探索)
 
-**场景**:网络断开后重新连接,不丢失中间的更新
+explore 工具触发并行探索时,为每个分支发送此事件。
 
-**实现方式**:
+```json
+{
+  "event": "branch_started",
+  "event_id": 20,
+  "explore_start_id": "6",
+  "branch": {
+    "id": "A",
+    "explore_start_id": "6",
+    "description": "JWT 方案",
+    "status": "exploring",
+    "summary": null,
+    "cumulative_stats": { "message_count": 0, "total_tokens": 0, "total_cost": 0.0, "preview": null },
+    "goal_count": 0,
+    "last_message": null
+  }
+}
+```
 
+**前端处理**:
 ```javascript
-let lastEventId = 0
+if (data.event === 'branch_started') {
+  insertBranch(data.explore_start_id, data.branch)
+  regenerateDAG()
+}
+```
 
-// 初次连接
-const ws = new WebSocket(
-  `ws://localhost:8000/api/traces/abc123/watch?since_event_id=0`
-)
+---
 
-ws.onmessage = (event) => {
-  const data = JSON.parse(event.data)
+#### 7. branch_completed(分支完成)
 
-  // 保存最新 event_id
-  if (data.event_id) {
-    lastEventId = data.event_id
-    localStorage.setItem('trace_abc123_event_id', lastEventId)
-  }
-}
+分支内所有 Goals 完成后触发。
 
-ws.onclose = () => {
-  // 3 秒后重连
-  setTimeout(() => {
-    // 从上次的 event_id 继续
-    const ws2 = new WebSocket(
-      `ws://localhost:8000/api/traces/abc123/watch?since_event_id=${lastEventId}`
-    )
-    // 服务器会补发 lastEventId 之后的所有事件
-  }, 3000)
+```json
+{
+  "event": "branch_completed",
+  "event_id": 35,
+  "explore_start_id": "6",
+  "branch_id": "A",
+  "summary": "JWT 方案实现完成,无状态但 token 较大",
+  "cumulative_stats": { "message_count": 8, "total_tokens": 4000, "total_cost": 0.05, "preview": "read → edit × 3" },
+  "last_message": { "role": "assistant", "content": "JWT 实现完成...", "created_at": "..." }
 }
 ```
 
-**注意**:
-- 每条消息都有 `event_id` 和 `ts` 字段
-- 重连时传入 `since_event_id`,服务器自动补发缺失的事件(最多 100 条)
-- 超过 100 条会返回错误,需要用 REST API 重新加载
+**前端处理**:
+```javascript
+if (data.event === 'branch_completed') {
+  updateBranch(data.explore_start_id, data.branch_id, {
+    status: 'completed',
+    summary: data.summary,
+    cumulative_stats: data.cumulative_stats,
+    last_message: data.last_message
+  })
+  regenerateDAG()
+}
+```
 
 ---
 
-### 心跳检测
+#### 8. message_added 扩展(分支内消息)
 
-保持连接活跃,检测僵尸连接
+分支内的 Message 会包含 `branch_id` 和 `affected_branches` 字段:
 
+```json
+{
+  "event": "message_added",
+  "event_id": 25,
+  "message": {
+    "message_id": "msg-025",
+    "role": "assistant",
+    "goal_id": "1",
+    "branch_id": "A",
+    "content": { "text": "...", "tool_calls": [...] },
+    "tokens": 300,
+    "cost": 0.004
+  },
+  "affected_goals": [
+    { "goal_id": "1", "self_stats": {...}, "cumulative_stats": {...} }
+  ],
+  "affected_branches": [
+    { "branch_id": "A", "explore_start_id": "6", "cumulative_stats": {...} }
+  ]
+}
+```
+
+**前端处理**:
 ```javascript
-// 每 30 秒发送心跳
-const heartbeat = setInterval(() => {
-  if (ws.readyState === WebSocket.OPEN) {
-    ws.send('ping')
+if (data.event === 'message_added') {
+  for (const g of data.affected_goals) {
+    updateGoalStats(g.goal_id, g)
   }
-}, 30000)
-
-ws.onmessage = (event) => {
-  const data = JSON.parse(event.data)
-  if (data.event === 'pong') {
-    console.log('Connection alive')
+  // 分支内消息还需更新分支统计
+  if (data.affected_branches) {
+    for (const b of data.affected_branches) {
+      updateBranchStats(b.explore_start_id, b.branch_id, b.cumulative_stats)
+    }
   }
+  rerenderEdge(data.message.goal_id, data.message.branch_id)
 }
 ```
 
 ---
 
-## 📊 数据模型
+## 数据模型
 
 ### Trace
 
@@ -399,261 +600,371 @@ ws.onmessage = (event) => {
 |------|------|------|
 | `trace_id` | string | 唯一 ID |
 | `mode` | string | `call` - 单次调用 / `agent` - Agent 模式 |
-| `task` | string | 任务描述(Agent 模式)|
+| `task` | string | 任务描述 |
 | `status` | string | `running` / `completed` / `failed` |
-| `total_steps` | int | Step 总数 |
+| `total_messages` | int | Message 总数 |
 | `total_tokens` | int | Token 总消耗 |
 | `total_cost` | float | 成本总和 |
-| `total_duration_ms` | int | 总耗时(毫秒)|
-| `last_sequence` | int | 最新 Step 的 sequence |
-| `last_event_id` | int | 最新事件 ID |
+| `current_goal_id` | string | 当前焦点 Goal 的内部 ID |
 | `created_at` | string | 创建时间(ISO 8601)|
 | `completed_at` | string \| null | 完成时间 |
 
 ---
 
-### Step
-
-#### 顶层字段(所有 Step 共有)
+### GoalTree
 
 | 字段 | 类型 | 说明 |
 |------|------|------|
-| `step_id` | string | 唯一 ID |
-| `trace_id` | string | 所属 Trace ID |
-| `step_type` | string | 步骤类型(见下表)|
-| `status` | string | 状态(见下表)|
-| `sequence` | int | 在 Trace 中的顺序(递增)|
-| `parent_id` | string \| null | 父节点 ID |
-| `description` | string | 简短描述 |
-| `summary` | string \| null | 总结(仅 `evaluation` 类型)|
-| `has_children` | bool | 是否有子节点 |
-| `children_count` | int | 子节点数量 |
-| `duration_ms` | int \| null | 耗时(毫秒)|
-| `tokens` | int \| null | Token 消耗 |
-| `cost` | float \| null | 成本 |
-| `created_at` | string | 创建时间 |
-| `data` | object | 类型相关的详细数据 |
+| `mission` | string | 总任务描述(来自 Trace.task)|
+| `current_id` | string \| null | 当前焦点 Goal 的内部 ID |
+| `goals` | Goal[] | 顶层目标列表 |
+
+---
 
-#### step_type(步骤类型)
+### Goal
 
-| 类型 | 说明 | 来源 |
+| 字段 | 类型 | 说明 |
 |------|------|------|
-| `goal` | 目标/计划 | LLM |
-| `thought` | 思考/分析 | LLM |
-| `evaluation` | 评估总结 | LLM |
-| `response` | 最终回复 | LLM |
-| `action` | 工具调用 | System |
-| `result` | 工具结果 | System |
-| `memory_read` | 读取记忆 | System |
-| `memory_write` | 写入记忆 | System |
-
-#### status(步骤状态)
-
-| 状态 | 说明 |
-|------|------|
-| `planned` | 计划中(未执行)|
-| `in_progress` | 执行中 |
-| `awaiting_approval` | 等待用户确认 |
-| `completed` | 已完成 |
-| `failed` | 失败 |
-| `skipped` | 跳过 |
-
-#### data 字段(按 step_type)
-
-**thought / response**:
-```json
-{
-  "model": "claude-sonnet-4.5",
-  "content": "让我先分析...",
-  "messages": [...],  // full 视图才有
-  "tool_calls": [...]  // 如果有工具调用
-}
-```
+| `id` | string | 内部唯一 ID,纯自增("1", "2", "3"...)|
+| `parent_id` | string \| null | 父 Goal ID(层级关系)|
+| `branch_id` | string \| null | 所属分支 ID(null=主线)|
+| `type` | string | `normal` / `explore_start` / `explore_merge` |
+| `description` | string | 目标描述(做什么)|
+| `reason` | string | 创建理由(为什么做)|
+| `status` | string | `pending` / `in_progress` / `completed` / `abandoned` |
+| `summary` | string \| null | 完成/放弃时的总结 |
+| `branch_ids` | string[] \| null | 关联的分支 ID(仅 explore_start)|
+| `explore_start_id` | string \| null | 关联的 explore_start Goal(仅 explore_merge)|
+| `merge_summary` | string \| null | 各分支汇总(仅 explore_merge)|
+| `selected_branch` | string \| null | 选中的分支(仅 explore_merge)|
+| `self_stats` | GoalStats | 自身统计 |
+| `cumulative_stats` | GoalStats | 累计统计 |
+
+**ID 设计**:
+- 内部 ID 纯自增,不管层级、分支、废弃
+- 层级关系通过 `parent_id` 维护
+- 分支关系通过 `branch_id` 维护
+- 显示序号由前端/后端动态生成
 
-**action**:
-```json
-{
-  "tool_name": "read_file",
-  "arguments": {
-    "file_path": "config.yaml"
-  }
-}
-```
+---
 
-**result**:
-```json
-{
-  "tool_name": "read_file",
-  "output": "file content...",
-  "error": null
-}
-```
+### BranchContext
 
-**memory_read**:
-```json
-{
-  "experiences_count": 5,
-  "skills_count": 3
-}
-```
+分支元数据(主请求返回,不含内部 GoalTree)
 
----
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `id` | string | 分支 ID(如 `"A"`, `"B"`)|
+| `explore_start_id` | string | 关联的 explore_start Goal ID |
+| `description` | string | 探索方向描述 |
+| `status` | string | `exploring` / `completed` / `abandoned` |
+| `summary` | string \| null | 完成时的总结 |
+| `cumulative_stats` | GoalStats | 累计统计 |
+| `goal_count` | int | 分支内 Goal 数量 |
+| `last_message` | Message \| null | 最新消息(用于预览)|
 
-## 🎯 推荐的实现方案
+---
 
-### 方案 1:纯 WebSocket(简单场景)
+### BranchDetail
 
-适用于:实时监控进行中的任务,Step 数量 < 100
+分支详情(按需加载)
 
-```javascript
-// 只用 WebSocket,自动获取历史
-const ws = new WebSocket(
-  'ws://localhost:8000/api/traces/abc123/watch?since_event_id=0'
-)
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `id` | string | 分支 ID |
+| `description` | string | 探索方向描述 |
+| `status` | string | 状态 |
+| `summary` | string \| null | 总结 |
+| `goal_tree` | GoalTree | 分支内的 GoalTree |
+| `cumulative_stats` | GoalStats | 累计统计 |
 
-ws.onmessage = (event) => {
-  const data = JSON.parse(event.data)
+---
 
-  if (data.event === 'step_added') {
-    // 历史 + 新增的 Step 都会收到
-    addStepToTree(data.step)
-  }
-}
-```
+### GoalStats
 
-**优点**:代码简单
-**缺点**:超过 100 个 Step 会失败
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `message_count` | int | 消息数量 |
+| `total_tokens` | int | Token 总数 |
+| `total_cost` | float | 总成本 |
+| `preview` | string \| null | 工具调用摘要(如 `"read → edit × 2"`)|
 
 ---
 
-### 方案 2:REST + WebSocket(生产推荐)
+### Message
 
-适用于:查看历史任务,或 Step 数量 > 100
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `message_id` | string | 唯一 ID |
+| `trace_id` | string | 所属 Trace ID |
+| `branch_id` | string \| null | 所属分支(null=主线)|
+| `role` | string | `assistant` / `tool` |
+| `sequence` | int | 全局顺序 |
+| `goal_id` | string | 关联的 Goal 内部 ID |
+| `tool_call_id` | string \| null | tool 消息关联的 tool_call ID |
+| `content` | any | 消息内容(和 LLM API 格式一致)|
+| `tokens` | int \| null | Token 消耗 |
+| `cost` | float \| null | 成本 |
+| `created_at` | string | 创建时间 |
 
-```javascript
-// 1. 先用 REST API 获取完整树
-const response = await fetch(
-  `/api/traces/${traceId}/tree?view=compact`
-)
-const treeData = await response.json()
-renderTree(treeData.root_steps)
+---
 
-// 2. 连接 WebSocket 监听增量更新
-const ws = new WebSocket(
-  `ws://localhost:8000/api/traces/${traceId}/watch?since_event_id=0`
-)
+## DAG 可视化(前端实现)
 
-ws.onmessage = (event) => {
-  const data = JSON.parse(event.data)
-  if (data.event === 'step_added') {
-    addStepToTree(data.step)  // 只处理新增的
-  }
-}
-```
+后端只提供 GoalTree(扁平列表 + parent_id),前端负责生成 DAG 视图。
 
-**优点**:可靠,支持大型 Trace
-**缺点**:略复杂
+### 核心逻辑
 
----
+**普通 Goal(子目标)**:
+1. 未展开:显示父 Goal
+2. 已展开:父 Goal 被子 Goal **替代**(顺序)
 
-## 🐛 错误处理
+**explore Goal(分支)**:
+1. 未展开:显示单个 explore_start → explore_merge 节点
+2. 已展开:显示**并行分支路径**(分叉-汇合)
 
-### HTTP 错误码
+### 边数据
 
-| 状态码 | 说明 |
-|--------|------|
-| 200 | 成功 |
-| 404 | Trace/Step 不存在 |
-| 400 | 参数错误 |
-| 500 | 服务器错误 |
+从 target 节点的 stats 获取:
+- 折叠边 → `target.cumulative_stats`
+- 展开边 → `target.self_stats`
+- 分支边 → `branch.cumulative_stats`
 
-### WebSocket 错误
+### 示例:普通展开(子目标)
 
 ```javascript
-ws.onerror = (error) => {
-  console.error('WebSocket error:', error)
-  // 重连
+// GoalTree(扁平列表,通过 parent_id 构建层级)
+const goalTree = {
+  goals: [
+    { id: "1", parent_id: null, description: "分析代码" },
+    { id: "2", parent_id: null, description: "实现功能" },
+    { id: "3", parent_id: "2", description: "设计接口" },  // 2 的子目标
+    { id: "4", parent_id: "2", description: "实现代码" },  // 2 的子目标
+    { id: "5", parent_id: null, description: "测试" }
+  ]
 }
 
-ws.onclose = (event) => {
-  console.log('Connection closed:', event.code, event.reason)
-  // 自动重连
+// 构建层级视图
+function buildHierarchy(goals) {
+  const map = new Map(goals.map(g => [g.id, { ...g, children: [] }]))
+  const roots = []
+  for (const goal of goals) {
+    const node = map.get(goal.id)
+    if (goal.parent_id) {
+      map.get(goal.parent_id)?.children.push(node)
+    } else {
+      roots.push(node)
+    }
+  }
+  return roots
 }
-```
 
----
+// 展开状态
+const expanded = new Set()  // 空 = 全部折叠
+
+// 生成可见节点序列
+function getVisibleGoals(nodes, expanded) {
+  const result = []
+  for (const node of nodes) {
+    if (expanded.has(node.id) && node.children.length > 0) {
+      // 展开:递归处理子节点
+      result.push(...getVisibleGoals(node.children, expanded))
+    } else {
+      // 折叠:显示自己
+      result.push(node)
+    }
+  }
+  return result
+}
 
-## 💡 最佳实践
+// 折叠视图: [1] → [2] → [5]
+// 展开 "2" 后: [1] → [3] → [4] → [5]
+
+// 生成边
+function generateEdges(visibleGoals) {
+  const edges = []
+  for (let i = 0; i < visibleGoals.length; i++) {
+    const source = i === 0 ? null : visibleGoals[i - 1]
+    const target = visibleGoals[i]
+    edges.push({
+      source: source?.id ?? null,
+      target: target.id,
+      // 边数据来自 target 的 stats
+      // 如果 target 有子节点且未展开,用 cumulative_stats
+      // 否则用 self_stats
+      stats: hasUnexpandedChildren(target, expanded)
+        ? target.cumulative_stats
+        : target.self_stats
+    })
+  }
+  return edges
+}
+```
 
-### 1. 保存 event_id 用于断线重连
+### 示例:分支展开(explore)
 
 ```javascript
-ws.onmessage = (event) => {
-  const data = JSON.parse(event.data)
-  if (data.event_id) {
-    localStorage.setItem(
-      `trace_${traceId}_event_id`,
-      data.event_id
-    )
+// GoalTree 含 explore Goals + 分支元数据
+const goalTree = {
+  goals: [
+    { id: "1", parent_id: null, type: "normal", description: "分析问题" },
+    { id: "2", parent_id: null, type: "explore_start", branch_ids: ["A", "B"] },
+    { id: "3", parent_id: null, type: "explore_merge", explore_start_id: "2" },
+    { id: "4", parent_id: null, type: "normal", description: "完善实现" }
+  ]
+}
+
+const branches = {
+  "A": { id: "A", description: "JWT 方案", cumulative_stats: {...}, goal_count: 2 },
+  "B": { id: "B", description: "Session 方案", cumulative_stats: {...}, goal_count: 3 }
+}
+
+// 折叠视图: [1] → [2:explore_start] → [3:explore_merge] → [4]
+
+// 展开分支后的视图:
+//           ┌→ [A:JWT] ────┐
+// [1] ──────┼              ├──→ [3] ──→ [4]
+//           └→ [B:Session] ┘
+
+// 分支展开时,需要按需加载分支详情
+async function loadBranchDetail(traceId, branchId) {
+  const resp = await fetch(`/api/traces/${traceId}/branches/${branchId}`)
+  return await resp.json()  // 返回 BranchDetail,含 goal_tree
+}
+
+// 分支展开时,生成分叉结构
+function generateBranchDAG(prevGoal, exploreStartGoal, exploreEndGoal, nextGoal, branches, expandedBranches) {
+  const nodes = []
+  const edges = []
+
+  for (const branchId of exploreStartGoal.branch_ids) {
+    const branch = branches[branchId]
+
+    if (expandedBranches.has(branchId) && branch.goalTree) {
+      // 分支已加载且展开:显示分支内的 Goals
+      const branchNodes = getVisibleGoals(buildHierarchy(branch.goalTree.goals), expandedBranches)
+      nodes.push(...branchNodes)
+
+      // 分叉边:前一个节点 → 分支第一个节点
+      if (branchNodes.length > 0) {
+        edges.push({
+          source: prevGoal?.id ?? null,
+          target: branchNodes[0].id,
+          stats: branchNodes[0].self_stats,
+          isBranchEdge: true,
+          branchId: branchId
+        })
+
+        // 分支内部边
+        for (let i = 1; i < branchNodes.length; i++) {
+          edges.push({
+            source: branchNodes[i - 1].id,
+            target: branchNodes[i].id,
+            stats: branchNodes[i].self_stats,
+            branchId: branchId
+          })
+        }
+
+        // 汇合边:分支最后一个节点 → explore_merge
+        edges.push({
+          source: branchNodes[branchNodes.length - 1].id,
+          target: exploreEndGoal.id,
+          stats: null,
+          isMergeEdge: true,
+          branchId: branchId
+        })
+      }
+    } else {
+      // 分支折叠:显示为单个分支节点
+      const branchNode = {
+        id: `branch_${branchId}`,
+        description: branch.description,
+        isBranchNode: true,
+        branchId: branchId
+      }
+      nodes.push(branchNode)
+
+      edges.push({
+        source: prevGoal?.id ?? null,
+        target: branchNode.id,
+        stats: branch.cumulative_stats,
+        isBranchEdge: true
+      })
+
+      edges.push({
+        source: branchNode.id,
+        target: exploreEndGoal.id,
+        stats: null,
+        isMergeEdge: true
+      })
+    }
   }
+
+  return { nodes, edges }
 }
 ```
 
-### 2. 实现自动重连
+### 视觉区分
+
+| 边类型 | 说明 | 样式建议 |
+|--------|------|---------|
+| 普通边 | 顺序执行 | 实线 |
+| 分叉边 | explore 分支开始 | 虚线或带标记 |
+| 汇合边 | 分支合并到 merge 节点 | 虚线或带标记 |
+| 废弃边 | abandoned 分支 | 灰色 |
+
+---
+
+## 断线续传
 
 ```javascript
-function connectWebSocket(traceId) {
-  const lastEventId = localStorage.getItem(`trace_${traceId}_event_id`) || 0
+let lastEventId = 0
+
+function connect(traceId) {
   const ws = new WebSocket(
     `ws://localhost:8000/api/traces/${traceId}/watch?since_event_id=${lastEventId}`
   )
 
-  ws.onclose = () => {
-    setTimeout(() => connectWebSocket(traceId), 3000)
+  ws.onmessage = (event) => {
+    const data = JSON.parse(event.data)
+    if (data.event_id) {
+      lastEventId = data.event_id
+      localStorage.setItem(`trace_${traceId}_event_id`, lastEventId)
+    }
+    // 处理事件...
   }
 
-  return ws
+  ws.onclose = () => {
+    setTimeout(() => connect(traceId), 3000)
+  }
 }
 ```
 
-### 3. 使用 compact 视图减少流量
+---
 
-```javascript
-// ✅ 推荐
-const response = await fetch(`/api/traces/${id}/tree?view=compact`)
+## 错误处理
 
-// ❌ 避免(数据量可能很大)
-const response = await fetch(`/api/traces/${id}/tree?view=full`)
-```
+### HTTP 错误码
 
-### 4. 按需懒加载(大型 Trace)
+| 状态码 | 说明 |
+|--------|------|
+| 200 | 成功 |
+| 404 | Trace 不存在 |
+| 400 | 参数错误 |
+| 500 | 服务器错误 |
 
-```javascript
-// 初次只加载第一层
-const tree = await fetch(
-  `/api/traces/${id}/tree?max_depth=1`
-).then(r => r.json())
-
-// 用户点击展开时,懒加载子树
-async function onExpand(stepId) {
-  const node = await fetch(
-    `/api/traces/${id}/node/${stepId}?expand=true&max_depth=1`
-  ).then(r => r.json())
-
-  appendChildren(stepId, node.children)
+### WebSocket 错误
+
+```json
+{
+  "event": "error",
+  "message": "Too many missed events, please reload via REST API"
 }
 ```
 
 ---
 
-## 🔗 相关文档
-
-- [Step 树结构详解](./step-tree.md)
-- [API 接口规范](./trace-api.md)
-- [架构设计文档](./README.md)
-
----
-
-## 📞 问题反馈
+## 相关文档
 
-如有问题请提 Issue:https://github.com/anthropics/agent/issues
+- [Context 管理设计](../docs/context-management.md) - Goal 机制完整设计
+- [Trace 模块说明](../docs/trace-api.md) - 后端实现细节