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

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 + 工具)
 1. 执行 Agent 任务(循环调用 LLM + 工具)
 2. 记录执行图(Trace + Steps)
 2. 记录执行图(Trace + Steps)
 3. 检索和注入记忆(Experience + Skill)
 3. 检索和注入记忆(Experience + Skill)
-4. 收集反馈,提取经验
+4. 管理执行计划(Goal Tree)
+5. 收集反馈,提取经验
 """
 """
 
 
 import logging
 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.core.config import AgentConfig, CallResult
 from agent.execution import Trace, Step, TraceStore
 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.models import Experience, Skill
 from agent.memory.protocols import MemoryStore, StateStore
 from agent.memory.protocols import MemoryStore, StateStore
 from agent.memory.skill_loader import load_skills_from_dir
 from agent.memory.skill_loader import load_skills_from_dir
@@ -33,6 +35,7 @@ BUILTIN_TOOLS = [
     "bash_command",
     "bash_command",
     "skill",
     "skill",
     "list_skills",
     "list_skills",
+    "goal",
 ]
 ]
 
 
 
 
@@ -54,6 +57,7 @@ class AgentRunner:
         llm_call: Optional[Callable] = None,
         llm_call: Optional[Callable] = None,
         config: Optional[AgentConfig] = None,
         config: Optional[AgentConfig] = None,
         skills_dir: Optional[str] = None,
         skills_dir: Optional[str] = None,
+        goal_tree: Optional[GoalTree] = None,
         debug: bool = False,
         debug: bool = False,
     ):
     ):
         """
         """
@@ -67,6 +71,7 @@ class AgentRunner:
             llm_call: LLM 调用函数(必须提供,用于实际调用 LLM)
             llm_call: LLM 调用函数(必须提供,用于实际调用 LLM)
             config: Agent 配置
             config: Agent 配置
             skills_dir: Skills 目录路径(可选,不提供则不加载 skills)
             skills_dir: Skills 目录路径(可选,不提供则不加载 skills)
+            goal_tree: 执行计划(可选,不提供则在运行时按需创建)
             debug: 保留参数(已废弃,请使用 API Server 可视化)
             debug: 保留参数(已废弃,请使用 API Server 可视化)
         """
         """
         self.trace_store = trace_store
         self.trace_store = trace_store
@@ -76,6 +81,7 @@ class AgentRunner:
         self.llm_call = llm_call
         self.llm_call = llm_call
         self.config = config or AgentConfig()
         self.config = config or AgentConfig()
         self.skills_dir = skills_dir
         self.skills_dir = skills_dir
+        self.goal_tree = goal_tree
         self.debug = debug
         self.debug = debug
 
 
     def _generate_id(self) -> str:
     def _generate_id(self) -> str:
@@ -306,6 +312,9 @@ class AgentRunner:
             # 添加任务描述
             # 添加任务描述
             messages.append({"role": "user", "content": task})
             messages.append({"role": "user", "content": task})
 
 
+            # 初始化 GoalTree
+            goal_tree = self.goal_tree or GoalTree(mission=task)
+
             # 准备工具 Schema
             # 准备工具 Schema
             # 合并内置工具 + 用户指定工具
             # 合并内置工具 + 用户指定工具
             tool_names = BUILTIN_TOOLS.copy()
             tool_names = BUILTIN_TOOLS.copy()
@@ -324,9 +333,16 @@ class AgentRunner:
             total_cost = 0.0
             total_cost = 0.0
 
 
             for iteration in range(max_iterations):
             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
                 # 调用 LLM
                 result = await self.llm_call(
                 result = await self.llm_call(
-                    messages=messages,
+                    messages=llm_messages,
                     model=model,
                     model=model,
                     tools=tool_schemas,
                     tools=tool_schemas,
                     **kwargs
                     **kwargs
@@ -402,7 +418,12 @@ class AgentRunner:
                         break
                         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:
                     for tc in tool_calls:
                         tool_name = tc["function"]["name"]
                         tool_name = tc["function"]["name"]
@@ -411,12 +432,25 @@ class AgentRunner:
                             import json
                             import json
                             tool_args = json.loads(tool_args)
                             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
                         action_step_id = self._generate_id()
                         action_step_id = self._generate_id()
@@ -471,7 +505,8 @@ class AgentRunner:
                             "role": "tool",
                             "role": "tool",
                             "tool_call_id": tc["id"],
                             "tool_call_id": tc["id"],
                             "name": tool_name,
                             "name": tool_name,
-                            "content": tool_result
+                            "content": tool_result,
+                            "goal_id": goal_tree.current_id,
                         })
                         })
 
 
                     continue  # 继续循环
                     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 = 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))
                 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", {
         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
         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 + 父子关系) |
 | 状态管理 | 无 | 有 (Trace) | 有 (独立 Trace + 父子关系) |
 | 记忆检索 | 无 | 有 (Experience/Skill) | 有 (继承主 Agent) |
 | 记忆检索 | 无 | 有 (Experience/Skill) | 有 (继承主 Agent) |
-| 执行图 | 1 个节点 | N 个节点的 DAG | 嵌套 DAG(多个 Trace) |
+| 执行图 | 1 条 Message | N 条 Messages 的 DAG | 嵌套 DAG(多个 Trace) |
 | 触发方式 | 直接调用 | 直接调用 | 通过 Task 工具 |
 | 触发方式 | 直接调用 | 直接调用 | 通过 Task 工具 |
 | 权限范围 | 完整 | 完整 | 受限(可配置) |
 | 权限范围 | 完整 | 完整 | 受限(可配置) |
 
 
@@ -55,7 +55,8 @@
 ┌─────────────────────────────────────────────────────────────┐
 ┌─────────────────────────────────────────────────────────────┐
 │ Layer 1: Task State(任务状态)                               │
 │ Layer 1: Task State(任务状态)                               │
 │ - 当前任务的工作记忆                                          │
 │ - 当前任务的工作记忆                                          │
-│ - Trace/Step 记录执行过程                                    │
+│ - Trace + Messages 记录执行过程                               │
+│ - GoalTree 管理执行计划                                       │
 └─────────────────────────────────────────────────────────────┘
 └─────────────────────────────────────────────────────────────┘
 ```
 ```
 
 
@@ -68,42 +69,45 @@
 ## 核心流程:Agent Loop
 ## 核心流程:Agent Loop
 
 
 ```python
 ```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
     # 1. 创建 Trace
     trace = Trace.create(mode="agent", task=task, status="in_progress")
     trace = Trace.create(mode="agent", task=task, status="in_progress")
     await trace_store.create_trace(trace)
     await trace_store.create_trace(trace)
     yield trace  # 返回 Trace(表示开始)
     yield trace  # 返回 Trace(表示开始)
 
 
     # 2. 加载 Skills(内置 + 自定义)
     # 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)
     skills_text = format_skills(skills)
 
 
     # 3. 检索 Experiences,构建 system prompt
     # 3. 检索 Experiences,构建 system prompt
     experiences = await search_experiences(task)
     experiences = await search_experiences(task)
     system_prompt = build_system_prompt(experiences, skills_text)
     system_prompt = build_system_prompt(experiences, skills_text)
 
 
-    # 4. 初始化消息
+    # 4. 初始化消息和 GoalTree
     messages = [{"role": "user", "content": task}]
     messages = [{"role": "user", "content": task}]
+    goal_tree = GoalTree(mission=task)
 
 
     # 5. ReAct 循环
     # 5. ReAct 循环
     for step in range(max_steps):
     for step in range(max_steps):
+        # 注入当前计划到 system prompt
+        plan_text = goal_tree.to_prompt()
+
         # 调用 LLM
         # 调用 LLM
         response = await llm.chat(
         response = await llm.chat(
             messages=messages,
             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,
             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:
         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:
         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)
             result = await execute_tool(tool_call)
 
 
-            # 记录 action Step
-            action_step = Step.create(
+            # 记录 tool Message
+            tool_msg = Message.create(
                 trace_id=trace.trace_id,
                 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": "assistant", "tool_calls": [tool_call]})
-            messages.append({"role": "tool", "content": result})
+            messages.append({"role": "tool", "tool_call_id": tool_call.id, "content": result})
 
 
     # 6. 完成
     # 6. 完成
     trace.status = "completed"
     trace.status = "completed"
     await trace_store.update_trace(trace.trace_id, 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 次工具调用,如果都是同一个工具且参数相同,中断循环
 - **Doom Loop 检测**:跟踪最近 3 次工具调用,如果都是同一个工具且参数相同,中断循环
-- **Skills 自动加载**:`agent/skills/core.md` 总是自动加载,`skills_dir` 可选额外加载
-- **动态工具加载**:Skill 通过 tool 动态加载,按需消耗 context
-- **Sub-Agent 支持**:通过 task 工具启动专门化的 Sub-Agent 处理子任务
 
 
 ### Sub-Agent 执行流程
 ### Sub-Agent 执行流程
 
 
@@ -195,36 +180,47 @@ class Trace:
     # Sub-Agent 支持
     # Sub-Agent 支持
     parent_trace_id: Optional[str] = None      # 父 Trace ID
     parent_trace_id: Optional[str] = None      # 父 Trace ID
     agent_definition: Optional[str] = None     # Agent 类型名称
     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_tokens: int = 0
     total_cost: float = 0.0
     total_cost: float = 0.0
 
 
     # 上下文
     # 上下文
     uid: Optional[str] = None
     uid: Optional[str] = None
     context: Dict[str, Any] = field(default_factory=dict)
     context: Dict[str, Any] = field(default_factory=dict)
+    current_goal_id: Optional[str] = None      # 当前焦点 goal
 ```
 ```
 
 
 **实现**:`agent/execution/models.py:Trace`
 **实现**:`agent/execution/models.py:Trace`
 
 
-### Step(执行步骤)
+### Message(执行消息)
+
+对应 LLM API 的消息,加上元数据。不再有 parent_id 树结构。
 
 
 ```python
 ```python
 @dataclass
 @dataclass
-class Step:
-    step_id: str
+class Message:
+    message_id: str
     trace_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`
 **使用示例**:`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 工具:并行探索-合并
 - explore 工具:并行探索-合并
-- 回溯机制:abandon + context 压缩
-- 数据结构:Goal Tree + 线性 Message List
+- 回溯机制:未执行直接修改 / 已执行标记废弃+新分支
+- DAG 可视化:节点=结果,边=动作,边可展开/折叠
+- 数据结构:GoalTree + Messages(扁平列表,goal_id 关联)
 
 
 ### [工具系统](./tools.md)
 ### [工具系统](./tools.md)
 - 工具定义和注册
 - 工具定义和注册
@@ -440,8 +438,9 @@ system_prompt = base_prompt + "\n\n# Learned Experiences\n" + "\n".join([
 class TraceStore(Protocol):
 class TraceStore(Protocol):
     async def save(self, trace: Trace) -> None: ...
     async def save(self, trace: Trace) -> None: ...
     async def get(self, trace_id: str) -> Trace: ...
     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):
 class ExperienceStore(Protocol):
     async def search(self, scope: str, query: str, limit: int) -> List[Dict]: ...
     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`
 - Memory 协议:`agent/memory/protocols.py`
 
 
 **实现策略**:
 **实现策略**:
-- Trace/Step: 文件系统(JSON)
+- Trace/Message: 文件系统(JSON)
   - `FileSystemTraceStore` - 文件持久化(支持跨进程)
   - `FileSystemTraceStore` - 文件持久化(支持跨进程)
 - Experience: PostgreSQL + pgvector
 - Experience: PostgreSQL + pgvector
 - Skill: 文件系统(Markdown)
 - Skill: 文件系统(Markdown)
@@ -479,11 +478,10 @@ agent/
 │   └── config.py          # AgentConfig, CallResult
 │   └── config.py          # AgentConfig, CallResult
 ├── execution/             # 执行追踪
 ├── execution/             # 执行追踪
-│   ├── models.py          # Trace, Step
+│   ├── models.py          # Trace, Message
 │   ├── protocols.py       # TraceStore
 │   ├── protocols.py       # TraceStore
 │   ├── fs_store.py        # FileSystemTraceStore
 │   ├── fs_store.py        # FileSystemTraceStore
-│   ├── tree_dump.py       # 可视化
-│   ├── api.py             # RESTful API
+│   ├── api.py             # RESTful API(DAG 视图)
 │   └── websocket.py       # WebSocket
 │   └── websocket.py       # WebSocket
 ├── memory/                # 记忆系统
 ├── 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)
    - 详见 [`docs/context-management.md`](./context-management.md)
 
 
 ---
 ---
 
 
 ## Debug 工具
 ## Debug 工具
 
 
-开发调试时可实时查看 Step 树:
-
-```python
-from agent.debug import dump_tree
-
-# 每次 step 变化后调用
-dump_tree(trace, steps)
-```
+开发调试时可通过 API 查看 DAG 可视化:
 
 
 ```bash
 ```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` |
 | **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` |
 | **Sub-Agent** | 专门化的子代理 | 独立 Trace | `tools/builtin/task.py` |
 | **AgentDefinition** | Agent 类型定义 | 配置文件/代码 | `subagents/` |
 | **AgentDefinition** | Agent 类型定义 | 配置文件/代码 | `subagents/` |
 | **Skill** | 能力描述(Markdown) | 文件系统 | `memory/skill_loader.py` |
 | **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   │      └─────────────────┘
 │  触发 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
 ```python
 @tool
 @tool
@@ -171,11 +279,13 @@ def goal(
     add: Optional[str] = None,       # 添加目标(逗号分隔多个)
     add: Optional[str] = None,       # 添加目标(逗号分隔多个)
     done: Optional[str] = None,      # 完成当前目标,值为 summary
     done: Optional[str] = None,      # 完成当前目标,值为 summary
     abandon: Optional[str] = None,   # 放弃当前目标,值为原因
     abandon: Optional[str] = None,   # 放弃当前目标,值为原因
-    focus: Optional[str] = None,     # 切换焦点到指定 id
+    focus: Optional[str] = None,     # 切换焦点到指定显示序号
 ) -> str:
 ) -> str:
     """管理执行计划。"""
     """管理执行计划。"""
 ```
 ```
 
 
+**实现**:`agent/goal/tool.py:goal_tool`
+
 **层级支持**:`add` 添加到当前 focus 的 goal 下作为子目标。
 **层级支持**:`add` 添加到当前 focus 的 goal 下作为子目标。
 
 
 ```python
 ```python
@@ -202,6 +312,7 @@ goal(add="设计接口, 实现代码")
 pending ──focus──→ in_progress ──done──→ completed
 pending ──focus──→ in_progress ──done──→ completed
                         │                    ↓
                         │                    ↓
                         │              (压缩 context)
                         │              (压缩 context)
+                        │              (级联:若所有兄弟都 completed,父 goal 自动 completed)
                      abandon
                      abandon
@@ -210,7 +321,7 @@ pending ──focus──→ in_progress ──done──→ completed
                   (压缩 context)
                   (压缩 context)
 ```
 ```
 
 
-#### 2. explore 工具:并行探索
+#### explore 工具:并行探索
 
 
 基于 sub-agent 机制实现。
 基于 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 管理
 ### Context 管理
 
 
 #### 1. Plan 注入
 #### 1. Plan 注入
 
 
-每次 LLM 调用时,在 system prompt 末尾注入当前计划状态:
+每次 LLM 调用时,在 system prompt 末尾注入当前计划状态。注入时过滤掉 abandoned 目标,使用连续的显示序号:
 
 
 ```markdown
 ```markdown
 ## Current Plan
 ## Current Plan
 
 
 **Mission**: 实现用户认证功能
 **Mission**: 实现用户认证功能
-**Current**: 2.1 实现登录接口
+**Current**: 2.2 实现登录接口
 
 
 **Progress**:
 **Progress**:
 [✓] 1. 分析代码
 [✓] 1. 分析代码
@@ -307,98 +361,510 @@ message = {
 [ ] 3. 测试
 [ ] 3. 测试
 ```
 ```
 
 
+**实现**:`agent/goal/models.py:GoalTree.to_prompt`
+
 #### 2. 完成时压缩
 #### 2. 完成时压缩
 
 
 当调用 `goal(done="...")` 时:
 当调用 `goal(done="...")` 时:
-1. 找到该 goal 关联的所有 messages
+1. 找到该 goal 关联的所有 messages(通过 goal_id)
 2. 将详细 messages 替换为一条 summary message
 2. 将详细 messages 替换为一条 summary message
 3. 更新 goal 状态为 completed
 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 回溯**:
 **Before 回溯**:
 ```
 ```
+GoalTree 数据:
+  [✓] 1. 分析代码               (内部ID: 1)
+  [→] 2. 实现方案 A              (内部ID: 2)
+  [ ] 3. 测试                    (内部ID: 3)
+
 Messages:
 Messages:
   [分析代码的 20 条 message...]
   [分析代码的 20 条 message...]
-  [实现方案 A 的 30 条 message...]  ← 这些要压缩
+  [实现方案 A 的 30 条 message...]
   [测试失败的 message...]
   [测试失败的 message...]
-
-Plan:
-  [✓] 1. 分析代码
-  [✓] 2. 实现方案 A
-  [→] 3. 测试
 ```
 ```
 
 
 **After 回溯**:
 **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. 分析代码
   [✓] 1. 分析代码
-  [✗] 2. 实现方案 A (abandoned: 依赖问题)
-  [→] 2'. 实现方案 B
+  [→] 2. 实现方案 B  ← current
   [ ] 3. 测试
   [ ] 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}/
 .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 元数据
 └── 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 方案的对比
 
 
 | 方面 | OpenCode | 我们的方案 |
 | 方面 | OpenCode | 我们的方案 |
 |------|----------|-----------|
 |------|----------|-----------|
-| Plan 格式 | 纯文本 (plan.md) | 结构化 (goal.json) |
+| Plan 格式 | 纯文本 (plan.md) | 结构化 (GoalTree 嵌套 JSON) |
 | Plan 与执行关联 | 无 | 通过 goal_id 关联 |
 | Plan 与执行关联 | 无 | 通过 goal_id 关联 |
+| 执行记录 | Message List | Message List(加 goal_id 元数据) |
 | 压缩时机 | 事后(context 满时) | 增量(goal 完成/放弃时) |
 | 压缩时机 | 事后(context 满时) | 增量(goal 完成/放弃时) |
 | 并行探索 | Sub-agent(手动管理) | explore 工具(自动汇总) |
 | 并行探索 | 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` | 待实现 |
 | 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 工具
 ### Phase 1: 基础 goal 工具
-- Goal 数据结构
+- GoalTree 数据结构(含 ID 映射)
 - goal 工具(add, done, focus)
 - goal 工具(add, done, focus)
-- Plan 注入到 system prompt
-- 基础可视化
+- Plan 注入到 system prompt(含显示序号生成)
+- Message 模型(替代 Step)
 
 
 ### Phase 2: 回溯支持
 ### Phase 2: 回溯支持
-- abandon 操作
-- Message 关联 goal_id
+- abandon 操作(两种模式)
+- 废弃分支 summary 累积到新分支
 - 基于 goal 的 context 压缩
 - 基于 goal 的 context 压缩
 
 
-### Phase 3: 并行探索
+### Phase 3: 可视化
+- DAG 视图 API
+- WebSocket goal/message 事件
+- 展开/折叠逻辑
+
+### Phase 4: 并行探索
 - explore 工具
 - explore 工具
 - 独立 message list 管理
 - 独立 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/
 agent/execution/
-├── models.py          # Trace/Step 数据模型
+├── models.py          # Trace/Message 数据模型
 ├── protocols.py       # TraceStore 存储接口
 ├── protocols.py       # TraceStore 存储接口
-├── memory_store.py    # 内存存储实现
-├── api.py             # RESTful API(懒加载)
+├── fs_store.py        # 文件系统存储实现
+├── api.py             # RESTful API
 └── websocket.py       # WebSocket 实时推送
 └── websocket.py       # WebSocket 实时推送
 ```
 ```
 
 
 **设计原则**:
 **设计原则**:
-- **高内聚**:所有 Trace 相关代码在一个模块
-- **松耦合**:核心模型不依赖 FastAPI
-- **可扩展**:易于添加 PostgreSQL/Neo4j 实现
+- **高内聚**:所有 Trace 相关代码在一个模块
+- **松耦合**:核心模型不依赖 FastAPI
+- **可扩展**:易于添加 PostgreSQL 等存储实现
 
 
 ---
 ---
 
 
@@ -31,353 +31,213 @@ agent/execution/
 一次完整的 LLM 交互(单次调用或 Agent 任务)
 一次完整的 LLM 交互(单次调用或 Agent 任务)
 
 
 ```python
 ```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.trace_id        # UUID
 trace.mode            # "call" | "agent"
 trace.mode            # "call" | "agent"
 trace.task            # 任务描述
 trace.task            # 任务描述
 trace.status          # "running" | "completed" | "failed"
 trace.status          # "running" | "completed" | "failed"
-trace.total_steps     # Step 总数
+trace.total_messages  # Message 总数
 trace.total_tokens    # Token 总数
 trace.total_tokens    # Token 总数
 trace.total_cost      # 总成本
 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,
     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
 ### TraceStore Protocol
 
 
-定义所有存储实现必须遵守的接口
-
 ```python
 ```python
-from agent.trace import TraceStore
-
-class MyCustomStore:
-    """实现 TraceStore 接口的所有方法"""
-
+class TraceStore(Protocol):
+    # Trace 操作
     async def create_trace(self, trace: Trace) -> str: ...
     async def create_trace(self, trace: Trace) -> str: ...
     async def get_trace(self, trace_id: str) -> Optional[Trace]: ...
     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 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
 ```python
 from agent.execution import FileSystemTraceStore
 from agent.execution import FileSystemTraceStore
 
 
 store = FileSystemTraceStore(base_path=".trace")
 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
 ```http
 GET /api/traces?mode=agent&status=running&limit=20
 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
 ```http
-GET /api/traces/{trace_id}/tree
+GET /api/traces/{trace_id}
 ```
 ```
 
 
-**响应**:递归 Step 树(完整)
+返回 Trace 元数据、主线 GoalTree(扁平列表,含所有 Goal 及其 stats)、分支元数据(不含分支内部 GoalTree)。
 
 
-#### 3. 懒加载节点(大型 Trace)
+### 3. 获取 Messages
 
 
 ```http
 ```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
 ```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 - 前端对接文档
 # 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
 ### 1. 列出 Traces
 
 
-获取 Trace 列表(支持过滤)
-
 ```http
 ```http
 GET /api/traces?status=running&limit=20
 GET /api/traces?status=running&limit=20
 ```
 ```
@@ -50,12 +64,13 @@ GET /api/traces?status=running&limit=20
     {
     {
       "trace_id": "abc123",
       "trace_id": "abc123",
       "mode": "agent",
       "mode": "agent",
-      "task": "分析代码库结构",
+      "task": "实现用户认证功能",
       "status": "running",
       "status": "running",
-      "total_steps": 15,
+      "total_messages": 15,
       "total_tokens": 5000,
       "total_tokens": 5000,
       "total_cost": 0.05,
       "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
   "total": 1
@@ -64,7 +79,7 @@ GET /api/traces?status=running&limit=20
 
 
 ---
 ---
 
 
-### 2. 获取 Trace 元数据
+### 2. 获取 Trace + GoalTree
 
 
 ```http
 ```http
 GET /api/traces/{trace_id}
 GET /api/traces/{trace_id}
@@ -75,114 +90,212 @@ GET /api/traces/{trace_id}
 {
 {
   "trace_id": "abc123",
   "trace_id": "abc123",
   "mode": "agent",
   "mode": "agent",
-  "task": "分析代码库结构",
+  "task": "实现用户认证功能",
   "status": "running",
   "status": "running",
-  "total_steps": 15,
+  "total_messages": 15,
   "total_tokens": 5000,
   "total_tokens": 5000,
   "total_cost": 0.05,
   "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
 ```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
 ```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
 ```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(连接成功)
 #### 1. connected(连接成功)
 
 
+连接后推送完整 GoalTree,前端据此初始化 DAG。
+
 ```json
 ```json
 {
 {
   "event": "connected",
   "event": "connected",
   "trace_id": "abc123",
   "trace_id": "abc123",
-  "current_event_id": 15
+  "current_event_id": 15,
+  "goal_tree": {
+    "mission": "实现用户认证功能",
+    "current_id": "2.1",
+    "goals": [...]
+  }
 }
 }
 ```
 ```
 
 
-**说明**:连接建立后的第一条消息,包含当前最新的 event_id
-
 **前端处理**:
 **前端处理**:
 ```javascript
 ```javascript
 if (data.event === 'connected') {
 if (data.event === 'connected') {
-  // 保存 event_id 用于断线重连
+  initDAG(data.goal_tree)
   localStorage.setItem('last_event_id', data.current_event_id)
   localStorage.setItem('last_event_id', data.current_event_id)
 }
 }
 ```
 ```
 
 
 ---
 ---
 
 
-#### 2. step_added(新增 Step)⭐ 最常用
+#### 2. goal_added(新增 Goal)
 
 
 ```json
 ```json
 {
 {
-  "event": "step_added",
+  "event": "goal_added",
   "event_id": 16,
   "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
 ```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
 ```json
 {
 {
-  "event": "step_updated",
+  "event": "goal_updated",
   "event_id": 17,
   "event_id": 17,
-  "ts": "2026-02-03T15:30:15.123456",
-  "step_id": "step-016",
-  "patch": {
+  "goal_id": "3",
+  "updates": {
     "status": "completed",
     "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
 ```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
 ```json
 {
 {
-  "event": "trace_completed",
+  "event": "message_added",
   "event_id": 18,
   "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
 ```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
 ```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
 ```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
 ```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
 ### Trace
 
 
@@ -399,261 +600,371 @@ ws.onmessage = (event) => {
 |------|------|------|
 |------|------|------|
 | `trace_id` | string | 唯一 ID |
 | `trace_id` | string | 唯一 ID |
 | `mode` | string | `call` - 单次调用 / `agent` - Agent 模式 |
 | `mode` | string | `call` - 单次调用 / `agent` - Agent 模式 |
-| `task` | string | 任务描述(Agent 模式)|
+| `task` | string | 任务描述 |
 | `status` | string | `running` / `completed` / `failed` |
 | `status` | string | `running` / `completed` / `failed` |
-| `total_steps` | int | Step 总数 |
+| `total_messages` | int | Message 总数 |
 | `total_tokens` | int | Token 总消耗 |
 | `total_tokens` | int | Token 总消耗 |
 | `total_cost` | float | 成本总和 |
 | `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)|
 | `created_at` | string | 创建时间(ISO 8601)|
 | `completed_at` | string \| null | 完成时间 |
 | `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
 ```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
 ```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
 ```javascript
-function connectWebSocket(traceId) {
-  const lastEventId = localStorage.getItem(`trace_${traceId}_event_id`) || 0
+let lastEventId = 0
+
+function connect(traceId) {
   const ws = new WebSocket(
   const ws = new WebSocket(
     `ws://localhost:8000/api/traces/${traceId}/watch?since_event_id=${lastEventId}`
     `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) - 后端实现细节