Jelajahi Sumber

Merge branch 'main' of https://git.yishihui.com/howard/Agent

max_liu 1 bulan lalu
induk
melakukan
9beb9e2556

+ 2 - 2
agent/core/runner.py

@@ -153,7 +153,7 @@ class AgentRunner:
                 trace_id=trace_id,
                 trace_id=trace_id,
                 role="assistant",
                 role="assistant",
                 sequence=1,
                 sequence=1,
-                goal_id="0",  # 单次调用没有 goal,使用占位符
+                goal_id=None,  # 单次调用没有 goal
                 content={"text": result.get("content", ""), "tool_calls": result.get("tool_calls")},
                 content={"text": result.get("content", ""), "tool_calls": result.get("tool_calls")},
                 tokens=result.get("prompt_tokens", 0) + result.get("completion_tokens", 0),
                 tokens=result.get("prompt_tokens", 0) + result.get("completion_tokens", 0),
                 cost=result.get("cost", 0),
                 cost=result.get("cost", 0),
@@ -325,7 +325,7 @@ class AgentRunner:
                 step_cost = result.get("cost", 0)
                 step_cost = result.get("cost", 0)
 
 
                 # 获取当前 goal_id
                 # 获取当前 goal_id
-                current_goal_id = goal_tree.current_id if goal_tree else "0"
+                current_goal_id = goal_tree.current_id if (goal_tree and goal_tree.current_id) else None
 
 
                 # 记录 assistant Message
                 # 记录 assistant Message
                 assistant_msg = Message.create(
                 assistant_msg = Message.create(

+ 13 - 4
agent/execution/api.py

@@ -124,7 +124,7 @@ async def get_trace(trace_id: str):
 @router.get("/{trace_id}/messages", response_model=MessagesResponse)
 @router.get("/{trace_id}/messages", response_model=MessagesResponse)
 async def get_messages(
 async def get_messages(
     trace_id: str,
     trace_id: str,
-    goal_id: Optional[str] = Query(None, description="过滤指定 Goal 的消息")
+    goal_id: Optional[str] = Query(None, description="过滤指定 Goal 的消息。使用 '_init' 查询初始阶段(goal_id=None)的消息")
 ):
 ):
     """
     """
     获取 Messages
     获取 Messages
@@ -132,6 +132,9 @@ async def get_messages(
     Args:
     Args:
         trace_id: Trace ID
         trace_id: Trace ID
         goal_id: 可选,过滤指定 Goal 的消息
         goal_id: 可选,过滤指定 Goal 的消息
+                - 不指定: 返回所有消息
+                - "_init" 或 "null": 返回初始阶段(goal_id=None)的消息
+                - 其他值: 返回指定 Goal 的消息
     """
     """
     store = get_trace_store()
     store = get_trace_store()
 
 
@@ -141,10 +144,16 @@ async def get_messages(
         raise HTTPException(status_code=404, detail="Trace not found")
         raise HTTPException(status_code=404, detail="Trace not found")
 
 
     # 获取 Messages
     # 获取 Messages
-    if goal_id:
-        messages = await store.get_messages_by_goal(trace_id, goal_id)
-    else:
+    if goal_id is None:
+        # 没有指定 goal_id,返回所有消息
         messages = await store.get_trace_messages(trace_id)
         messages = await store.get_trace_messages(trace_id)
+    elif goal_id in ("_init", "null"):
+        # 特殊值:查询初始阶段的消息(goal_id=None)
+        all_messages = await store.get_trace_messages(trace_id)
+        messages = [m for m in all_messages if m.goal_id is None]
+    else:
+        # 查询指定 Goal 的消息
+        messages = await store.get_messages_by_goal(trace_id, goal_id)
 
 
     return MessagesResponse(
     return MessagesResponse(
         messages=[m.to_dict() for m in messages]
         messages=[m.to_dict() for m in messages]

+ 3 - 3
agent/execution/models.py

@@ -113,7 +113,7 @@ class Message:
     trace_id: str
     trace_id: str
     role: Literal["assistant", "tool"]   # 和 LLM API 一致
     role: Literal["assistant", "tool"]   # 和 LLM API 一致
     sequence: int                        # 全局顺序
     sequence: int                        # 全局顺序
-    goal_id: str                         # 关联的 Goal 内部 ID
+    goal_id: Optional[str] = None        # 关联的 Goal 内部 ID(None = 还没有创建 Goal)
     description: str = ""                # 消息描述(系统自动生成)
     description: str = ""                # 消息描述(系统自动生成)
     tool_call_id: Optional[str] = None   # tool 消息关联对应的 tool_call
     tool_call_id: Optional[str] = None   # tool 消息关联对应的 tool_call
     content: Any = None                  # 消息内容(和 LLM API 格式一致)
     content: Any = None                  # 消息内容(和 LLM API 格式一致)
@@ -130,7 +130,7 @@ class Message:
         trace_id: str,
         trace_id: str,
         role: Literal["assistant", "tool"],
         role: Literal["assistant", "tool"],
         sequence: int,
         sequence: int,
-        goal_id: str,
+        goal_id: Optional[str] = None,
         content: Any = None,
         content: Any = None,
         tool_call_id: Optional[str] = None,
         tool_call_id: Optional[str] = None,
         tokens: Optional[int] = None,
         tokens: Optional[int] = None,
@@ -141,7 +141,7 @@ class Message:
         description = cls._generate_description(role, content)
         description = cls._generate_description(role, content)
 
 
         return cls(
         return cls(
-            message_id=str(uuid.uuid4()),
+            message_id=f"{trace_id}-{sequence:04d}",
             trace_id=trace_id,
             trace_id=trace_id,
             role=role,
             role=role,
             sequence=sequence,
             sequence=sequence,

+ 62 - 6
agent/goal/explore.py

@@ -115,24 +115,79 @@ async def explore_tool(
         return_exceptions=True
         return_exceptions=True
     )
     )
 
 
-    # 4. 汇总结果
+    # 4. 收集元数据并汇总结果
+    sub_trace_metadata = {}
     summary_parts = ["## 探索结果\n"]
     summary_parts = ["## 探索结果\n"]
 
 
     for i, (sub_trace, result) in enumerate(zip(sub_traces, results), 1):
     for i, (sub_trace, result) in enumerate(zip(sub_traces, results), 1):
         branch_name = chr(ord('A') + i - 1)  # A, B, C...
         branch_name = chr(ord('A') + i - 1)  # A, B, C...
 
 
         if isinstance(result, Exception):
         if isinstance(result, Exception):
+            # 处理异常情况
             summary_parts.append(f"### 方案 {branch_name}: {sub_trace.task}")
             summary_parts.append(f"### 方案 {branch_name}: {sub_trace.task}")
             summary_parts.append(f"⚠️ 执行出错: {str(result)}\n")
             summary_parts.append(f"⚠️ 执行出错: {str(result)}\n")
+
+            sub_trace_metadata[sub_trace.trace_id] = {
+                "task": sub_trace.task,
+                "status": "failed",
+                "summary": f"执行出错: {str(result)}",
+                "last_message": None,
+                "stats": {
+                    "message_count": 0,
+                    "total_tokens": 0,
+                    "total_cost": 0.0
+                }
+            }
         else:
         else:
             # 获取 Sub-Trace 的最终状态
             # 获取 Sub-Trace 的最终状态
             updated_trace = await store.get_trace(sub_trace.trace_id)
             updated_trace = await store.get_trace(sub_trace.trace_id)
+
+            # 获取最后一条 assistant 消息
+            messages = await store.get_trace_messages(sub_trace.trace_id)
+            last_message = None
+            for msg in reversed(messages):
+                if msg.role == "assistant":
+                    last_message = msg
+                    break
+
+            # 构建元数据
+            # 优先使用 result 中的 summary,否则使用最后一条消息的内容
+            summary_text = None
+            if isinstance(result, dict) and result.get("summary"):
+                summary_text = result.get("summary")
+            elif last_message and last_message.content:
+                # 使用最后一条消息的内容作为 summary(截断至 200 字符)
+                content_text = last_message.content
+                if isinstance(content_text, dict) and "text" in content_text:
+                    content_text = content_text["text"]
+                elif not isinstance(content_text, str):
+                    content_text = str(content_text)
+                summary_text = content_text[:200] if content_text else "执行完成"
+            else:
+                summary_text = "执行完成"
+
+            sub_trace_metadata[sub_trace.trace_id] = {
+                "task": sub_trace.task,
+                "status": updated_trace.status if updated_trace else "unknown",
+                "summary": summary_text,
+                "last_message": {
+                    "role": last_message.role,
+                    "description": last_message.description,
+                    "content": last_message.content[:500] if last_message.content else None,
+                    "created_at": last_message.created_at.isoformat()
+                } if last_message else None,
+                "stats": {
+                    "message_count": updated_trace.total_messages if updated_trace else 0,
+                    "total_tokens": updated_trace.total_tokens if updated_trace else 0,
+                    "total_cost": updated_trace.total_cost if updated_trace else 0.0
+                }
+            }
+
+            # 组装摘要文本
             summary_parts.append(f"### 方案 {branch_name}: {sub_trace.task}")
             summary_parts.append(f"### 方案 {branch_name}: {sub_trace.task}")
 
 
             if updated_trace and updated_trace.status == "completed":
             if updated_trace and updated_trace.status == "completed":
-                # 从 Sub-Trace 获取总结
-                summary = result.get("summary", "执行完成") if isinstance(result, dict) else "执行完成"
-                summary_parts.append(f"{summary}\n")
+                summary_parts.append(f"{summary_text}\n")
                 summary_parts.append(f"📊 统计: {updated_trace.total_messages} 条消息, "
                 summary_parts.append(f"📊 统计: {updated_trace.total_messages} 条消息, "
                                    f"{updated_trace.total_tokens} tokens, "
                                    f"{updated_trace.total_tokens} tokens, "
                                    f"成本 ${updated_trace.total_cost:.4f}\n")
                                    f"成本 ${updated_trace.total_cost:.4f}\n")
@@ -151,10 +206,11 @@ async def explore_tool(
 
 
     summary = "\n".join(summary_parts)
     summary = "\n".join(summary_parts)
 
 
-    # 5. 完成主 Goal
+    # 5. 完成主 Goal,保存元数据
     await store.update_goal(current_trace_id, current_goal_id,
     await store.update_goal(current_trace_id, current_goal_id,
                            status="completed",
                            status="completed",
-                           summary=f"探索了 {len(branches)} 个方案")
+                           summary=f"探索了 {len(branches)} 个方案",
+                           sub_trace_metadata=sub_trace_metadata)
 
 
     return summary
     return summary
 
 

+ 93 - 3
agent/goal/models.py

@@ -64,6 +64,7 @@ class Goal:
     # agent_call 特有
     # agent_call 特有
     sub_trace_ids: Optional[List[str]] = None      # 启动的 Sub-Trace IDs
     sub_trace_ids: Optional[List[str]] = None      # 启动的 Sub-Trace IDs
     agent_call_mode: Optional[str] = None          # "explore" | "delegate" | "sequential"
     agent_call_mode: Optional[str] = None          # "explore" | "delegate" | "sequential"
+    sub_trace_metadata: Optional[Dict[str, Dict[str, Any]]] = None  # Sub-Trace 元数据
 
 
     # 统计(后端维护,用于可视化边的数据)
     # 统计(后端维护,用于可视化边的数据)
     self_stats: GoalStats = field(default_factory=GoalStats)          # 自身统计(仅直接关联的 messages)
     self_stats: GoalStats = field(default_factory=GoalStats)          # 自身统计(仅直接关联的 messages)
@@ -83,6 +84,7 @@ class Goal:
             "summary": self.summary,
             "summary": self.summary,
             "sub_trace_ids": self.sub_trace_ids,
             "sub_trace_ids": self.sub_trace_ids,
             "agent_call_mode": self.agent_call_mode,
             "agent_call_mode": self.agent_call_mode,
+            "sub_trace_metadata": self.sub_trace_metadata,
             "self_stats": self.self_stats.to_dict(),
             "self_stats": self.self_stats.to_dict(),
             "cumulative_stats": self.cumulative_stats.to_dict(),
             "cumulative_stats": self.cumulative_stats.to_dict(),
             "created_at": self.created_at.isoformat() if self.created_at else None,
             "created_at": self.created_at.isoformat() if self.created_at else None,
@@ -113,6 +115,7 @@ class Goal:
             summary=data.get("summary"),
             summary=data.get("summary"),
             sub_trace_ids=data.get("sub_trace_ids"),
             sub_trace_ids=data.get("sub_trace_ids"),
             agent_call_mode=data.get("agent_call_mode"),
             agent_call_mode=data.get("agent_call_mode"),
+            sub_trace_metadata=data.get("sub_trace_metadata"),
             self_stats=self_stats,
             self_stats=self_stats,
             cumulative_stats=cumulative_stats,
             cumulative_stats=cumulative_stats,
             created_at=created_at or datetime.now(),
             created_at=created_at or datetime.now(),
@@ -218,6 +221,42 @@ class GoalTree:
 
 
         return new_goals
         return new_goals
 
 
+    def add_goals_after(
+        self,
+        target_id: str,
+        descriptions: List[str],
+        reasons: Optional[List[str]] = None
+    ) -> List[Goal]:
+        """
+        在指定 Goal 后面添加兄弟节点
+
+        新创建的 goals 与 target 有相同的 parent_id,
+        并插入到 goals 列表中 target 的后面。
+        """
+        target = self.find(target_id)
+        if not target:
+            raise ValueError(f"Target goal not found: {target_id}")
+
+        # 创建新 goals(parent_id 与 target 相同)
+        new_goals = []
+        for i, desc in enumerate(descriptions):
+            goal_id = self._generate_id()
+            reason = reasons[i] if reasons and i < len(reasons) else ""
+            goal = Goal(
+                id=goal_id,
+                description=desc.strip(),
+                reason=reason,
+                parent_id=target.parent_id  # 同层级
+            )
+            new_goals.append(goal)
+
+        # 插入到 target 后面(调整 goals 列表顺序)
+        target_index = self.goals.index(target)
+        for i, goal in enumerate(new_goals):
+            self.goals.insert(target_index + 1 + i, goal)
+
+        return new_goals
+
     def focus(self, goal_id: str) -> Goal:
     def focus(self, goal_id: str) -> Goal:
         """切换焦点到指定 Goal,并将其状态设为 in_progress"""
         """切换焦点到指定 Goal,并将其状态设为 in_progress"""
         goal = self.find(goal_id)
         goal = self.find(goal_id)
@@ -277,7 +316,11 @@ class GoalTree:
         """
         """
         格式化为 Prompt 注入文本
         格式化为 Prompt 注入文本
 
 
-        过滤掉 abandoned 目标,重新生成连续显示序号
+        展示策略:
+        - 过滤掉 abandoned 目标(除非明确要求)
+        - 完整展示所有顶层目标
+        - 完整展示当前 focus 目标的父链及其所有子孙
+        - 其他分支的子目标折叠显示(只显示数量和状态)
         """
         """
         lines = []
         lines = []
         lines.append(f"**Mission**: {self.mission}")
         lines.append(f"**Mission**: {self.mission}")
@@ -291,6 +334,17 @@ class GoalTree:
         lines.append("")
         lines.append("")
         lines.append("**Progress**:")
         lines.append("**Progress**:")
 
 
+        # 获取当前焦点的祖先链(从根到当前节点的路径)
+        current_path = set()
+        if self.current_id:
+            goal = self.find(self.current_id)
+            while goal:
+                current_path.add(goal.id)
+                if goal.parent_id:
+                    goal = self.find(goal.parent_id)
+                else:
+                    break
+
         def format_goal(goal: Goal, indent: int = 0) -> List[str]:
         def format_goal(goal: Goal, indent: int = 0) -> List[str]:
             # 跳过废弃的目标(除非明确要求包含)
             # 跳过废弃的目标(除非明确要求包含)
             if goal.status == "abandoned" and not include_abandoned:
             if goal.status == "abandoned" and not include_abandoned:
@@ -322,8 +376,35 @@ class GoalTree:
 
 
             # 递归处理子目标
             # 递归处理子目标
             children = self.get_children(goal.id)
             children = self.get_children(goal.id)
-            for child in children:
-                result.extend(format_goal(child, indent + 1))
+
+            # 判断是否需要折叠
+            # 如果当前 goal 或其子孙在焦点路径上,完整展示
+            should_expand = goal.id in current_path or any(
+                child.id in current_path for child in self._get_all_descendants(goal.id)
+            )
+
+            if should_expand or not children:
+                # 完整展示子目标
+                for child in children:
+                    result.extend(format_goal(child, indent + 1))
+            else:
+                # 折叠显示:只显示子目标的统计
+                non_abandoned = [c for c in children if c.status != "abandoned"]
+                if non_abandoned:
+                    completed = sum(1 for c in non_abandoned if c.status == "completed")
+                    in_progress = sum(1 for c in non_abandoned if c.status == "in_progress")
+                    pending = sum(1 for c in non_abandoned if c.status == "pending")
+
+                    status_parts = []
+                    if completed > 0:
+                        status_parts.append(f"{completed} completed")
+                    if in_progress > 0:
+                        status_parts.append(f"{in_progress} in progress")
+                    if pending > 0:
+                        status_parts.append(f"{pending} pending")
+
+                    status_str = ", ".join(status_parts)
+                    result.append(f"{prefix}    ({len(non_abandoned)} subtasks: {status_str})")
 
 
             return result
             return result
 
 
@@ -334,6 +415,15 @@ class GoalTree:
 
 
         return "\n".join(lines)
         return "\n".join(lines)
 
 
+    def _get_all_descendants(self, goal_id: str) -> List[Goal]:
+        """获取指定 Goal 的所有子孙节点"""
+        descendants = []
+        children = self.get_children(goal_id)
+        for child in children:
+            descendants.append(child)
+            descendants.extend(self._get_all_descendants(child.id))
+        return descendants
+
     def to_dict(self) -> Dict[str, Any]:
     def to_dict(self) -> Dict[str, Any]:
         """转换为字典"""
         """转换为字典"""
         return {
         return {

+ 78 - 16
agent/goal/tool.py

@@ -17,6 +17,8 @@ async def goal_tool(
     trace_id: Optional[str] = None,
     trace_id: Optional[str] = None,
     add: Optional[str] = None,
     add: Optional[str] = None,
     reason: Optional[str] = None,
     reason: Optional[str] = None,
+    after: Optional[str] = None,
+    under: Optional[str] = None,
     done: Optional[str] = None,
     done: Optional[str] = None,
     abandon: Optional[str] = None,
     abandon: Optional[str] = None,
     focus: Optional[str] = None,
     focus: Optional[str] = None,
@@ -28,8 +30,10 @@ async def goal_tool(
         tree: GoalTree 实例
         tree: GoalTree 实例
         store: TraceStore 实例(用于推送事件)
         store: TraceStore 实例(用于推送事件)
         trace_id: 当前 Trace ID
         trace_id: 当前 Trace ID
-        add: 添加目标(逗号分隔多个)。添加到当前 focus 的 goal 下作为子目标。
+        add: 添加目标(逗号分隔多个)
         reason: 创建理由(逗号分隔多个,与 add 一一对应)
         reason: 创建理由(逗号分隔多个,与 add 一一对应)
+        after: 在指定目标后面添加(同层级)
+        under: 为指定目标添加子目标
         done: 完成当前目标,值为 summary
         done: 完成当前目标,值为 summary
         abandon: 放弃当前目标,值为原因
         abandon: 放弃当前目标,值为原因
         focus: 切换焦点到指定内部 id
         focus: 切换焦点到指定内部 id
@@ -99,6 +103,10 @@ async def goal_tool(
 
 
     # 4. 处理 add
     # 4. 处理 add
     if add is not None:
     if add is not None:
+        # 检查 after 和 under 互斥
+        if after is not None and under is not None:
+            return "错误:after 和 under 参数不能同时指定"
+
         descriptions = [d.strip() for d in add.split(",") if d.strip()]
         descriptions = [d.strip() for d in add.split(",") if d.strip()]
         if descriptions:
         if descriptions:
             # 解析 reasons(与 descriptions 一一对应)
             # 解析 reasons(与 descriptions 一一对应)
@@ -109,9 +117,51 @@ async def goal_tool(
                 while len(reasons) < len(descriptions):
                 while len(reasons) < len(descriptions):
                     reasons.append("")
                     reasons.append("")
 
 
-            # 添加到当前焦点下(如果有焦点),否则添加到顶层
-            parent_id = tree.current_id
-            new_goals = tree.add_goals(descriptions, reasons=reasons, parent_id=parent_id)
+            # 确定添加位置
+            if after is not None:
+                # 在指定 goal 后面添加(同层级)
+                # after 参数可以是内部 ID 或显示 ID
+                target_goal = tree.find(after)
+                if not target_goal:
+                    # 尝试根据显示 ID 查找
+                    for g in tree.goals:
+                        if tree._generate_display_id(g) == after:
+                            target_goal = g
+                            break
+
+                if not target_goal:
+                    return f"错误:找不到目标 {after}"
+
+                new_goals = tree.add_goals_after(target_goal.id, descriptions, reasons=reasons)
+                changes.append(f"在 {tree._generate_display_id(target_goal)} 后面添加 {len(new_goals)} 个同级目标")
+
+            elif under is not None:
+                # 为指定 goal 添加子目标
+                # under 参数可以是内部 ID 或显示 ID
+                parent_goal = tree.find(under)
+                if not parent_goal:
+                    # 尝试根据显示 ID 查找
+                    for g in tree.goals:
+                        if tree._generate_display_id(g) == under:
+                            parent_goal = g
+                            break
+
+                if not parent_goal:
+                    return f"错误:找不到目标 {under}"
+
+                new_goals = tree.add_goals(descriptions, reasons=reasons, parent_id=parent_goal.id)
+                changes.append(f"在 {tree._generate_display_id(parent_goal)} 下添加 {len(new_goals)} 个子目标")
+
+            else:
+                # 默认行为:添加到当前焦点下(如果有焦点),否则添加到顶层
+                parent_id = tree.current_id
+                new_goals = tree.add_goals(descriptions, reasons=reasons, parent_id=parent_id)
+
+                if parent_id:
+                    parent_display_id = tree._generate_display_id(tree.find(parent_id))
+                    changes.append(f"在 {parent_display_id} 下添加 {len(new_goals)} 个子目标")
+                else:
+                    changes.append(f"添加 {len(new_goals)} 个顶层目标")
 
 
             # 推送事件
             # 推送事件
             if store and trace_id:
             if store and trace_id:
@@ -121,12 +171,6 @@ async def goal_tool(
             else:
             else:
                 print(f"[DEBUG] goal_tool: skip event push (store={store}, trace_id={trace_id})")
                 print(f"[DEBUG] goal_tool: skip event push (store={store}, trace_id={trace_id})")
 
 
-            if parent_id:
-                parent_display_id = tree._generate_display_id(tree.find(parent_id))
-                changes.append(f"在 {parent_display_id} 下添加 {len(new_goals)} 个子目标")
-            else:
-                changes.append(f"添加 {len(new_goals)} 个顶层目标")
-
             # 如果没有焦点且添加了目标,自动 focus 到第一个新目标
             # 如果没有焦点且添加了目标,自动 focus 到第一个新目标
             if not tree.current_id and new_goals:
             if not tree.current_id and new_goals:
                 tree.focus(new_goals[0].id)
                 tree.focus(new_goals[0].id)
@@ -152,20 +196,30 @@ def create_goal_tool_schema() -> dict:
         "name": "goal",
         "name": "goal",
         "description": """管理执行计划。
         "description": """管理执行计划。
 
 
-- add: 添加目标(逗号分隔多个)。添加到当前 focus 的 goal 下作为子目标。
+- add: 添加目标(逗号分隔多个)
 - reason: 创建理由(逗号分隔多个,与 add 一一对应)。说明为什么要做这些目标。
 - reason: 创建理由(逗号分隔多个,与 add 一一对应)。说明为什么要做这些目标。
+- after: 在指定目标后面添加(同层级)。可以是内部 ID 或显示 ID。
+- under: 为指定目标添加子目标。可以是内部 ID 或显示 ID。如已有子目标,追加到最后。
 - done: 完成当前目标,值为 summary
 - done: 完成当前目标,值为 summary
 - abandon: 放弃当前目标,值为原因(会触发 context 压缩)
 - abandon: 放弃当前目标,值为原因(会触发 context 压缩)
 - focus: 切换焦点到指定 id(可以是内部 ID 或显示 ID)
 - focus: 切换焦点到指定 id(可以是内部 ID 或显示 ID)
 
 
+位置控制(优先使用 after):
+- 不指定 after/under: 添加到当前 focus 下作为子目标(无 focus 时添加到顶层)
+- after="X": 在目标 X 后面添加兄弟节点(同层级)
+- under="X": 为目标 X 添加子目标
+- after 和 under 不能同时指定
+
 示例:
 示例:
-- goal(add="分析代码, 实现功能, 测试", reason="了解现有结构, 完成需求, 确保质量") - 添加顶层目标
-- goal(focus="2", add="设计接口, 实现代码", reason="明确API规范, 编写核心逻辑") - 切换到目标2,并添加子目标
+- goal(add="分析代码, 实现功能, 测试") - 添加顶层目标
+- goal(add="设计接口, 实现代码", under="2") - 为目标2添加子目标
+- goal(add="编写文档", after="3") - 在目标3后面添加同级任务
+- goal(add="集成测试", after="2.2") - 在目标2.2后面添加同级任务
 - goal(done="发现用户模型在 models/user.py") - 完成当前目标
 - goal(done="发现用户模型在 models/user.py") - 完成当前目标
-- goal(abandon="方案A需要Redis,环境没有", add="实现方案B", reason="使用现有技术栈") - 放弃当前并添加新目标
+- goal(abandon="方案A需要Redis,环境没有") - 放弃当前目标
 
 
 注意:内部 ID 是纯自增数字("1", "2", "3"),显示 ID 是带层级的("1", "2.1", "2.2")。
 注意:内部 ID 是纯自增数字("1", "2", "3"),显示 ID 是带层级的("1", "2.1", "2.2")。
-focus 参数可以使用任意格式的 ID。
+所有 ID 参数都可以使用任意格式的 ID。
 reason 应该与 add 的目标数量一致,如果数量不一致,缺少的 reason 将为空。
 reason 应该与 add 的目标数量一致,如果数量不一致,缺少的 reason 将为空。
 """,
 """,
         "parameters": {
         "parameters": {
@@ -173,12 +227,20 @@ reason 应该与 add 的目标数量一致,如果数量不一致,缺少的 r
             "properties": {
             "properties": {
                 "add": {
                 "add": {
                     "type": "string",
                     "type": "string",
-                    "description": "添加目标(逗号分隔多个)。添加到当前 focus 的 goal 下作为子目标。"
+                    "description": "添加目标(逗号分隔多个)"
                 },
                 },
                 "reason": {
                 "reason": {
                     "type": "string",
                     "type": "string",
                     "description": "创建理由(逗号分隔多个,与 add 一一对应)。说明为什么要做这些目标。"
                     "description": "创建理由(逗号分隔多个,与 add 一一对应)。说明为什么要做这些目标。"
                 },
                 },
+                "after": {
+                    "type": "string",
+                    "description": "在指定目标后面添加(同层级)。可以是内部 ID 或显示 ID。"
+                },
+                "under": {
+                    "type": "string",
+                    "description": "为指定目标添加子目标。可以是内部 ID 或显示 ID。"
+                },
                 "done": {
                 "done": {
                     "type": "string",
                     "type": "string",
                     "description": "完成当前目标,值为 summary"
                     "description": "完成当前目标,值为 summary"

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

@@ -29,7 +29,6 @@ async def bash_command(
     workdir: Optional[str] = None,
     workdir: Optional[str] = None,
     env: Optional[Dict[str, str]] = None,
     env: Optional[Dict[str, str]] = None,
     description: str = "",
     description: str = "",
-    uid: str = "",
     context: Optional[ToolContext] = None
     context: Optional[ToolContext] = None
 ) -> ToolResult:
 ) -> ToolResult:
     """
     """
@@ -41,7 +40,6 @@ async def bash_command(
         workdir: 工作目录,默认为当前目录
         workdir: 工作目录,默认为当前目录
         env: 环境变量字典(会合并到系统环境变量)
         env: 环境变量字典(会合并到系统环境变量)
         description: 命令描述(5-10 个词)
         description: 命令描述(5-10 个词)
-        uid: 用户 ID
         context: 工具上下文
         context: 工具上下文
 
 
     Returns:
     Returns:

+ 31 - 193
agent/tools/builtin/browser/baseClass.py

@@ -15,6 +15,12 @@ Native Browser-Use Tools Adapter
 1. 在 Agent 初始化时调用 init_browser_session()
 1. 在 Agent 初始化时调用 init_browser_session()
 2. 使用各个工具函数执行浏览器操作
 2. 使用各个工具函数执行浏览器操作
 3. 任务结束时调用 cleanup_browser_session()
 3. 任务结束时调用 cleanup_browser_session()
+
+文件操作说明:
+- 浏览器专用文件目录:.browser_use_files/ (在当前工作目录下)
+  用于存储浏览器会话产生的临时文件(下载、上传、截图等)
+- 一般文件操作:请使用 agent.tools.builtin 中的文件工具 (read_file, write_file, edit_file)
+  这些工具功能更完善,支持diff预览、智能匹配、分页读取等
 """
 """
 
 
 import sys
 import sys
@@ -119,7 +125,9 @@ async def init_browser_session(
     # 创建工具实例
     # 创建工具实例
     _browser_tools = Tools()
     _browser_tools = Tools()
 
 
-    # 创建文件系统实例(用于文件操作)
+    # 创建文件系统实例(用于浏览器会话产生的文件)
+    # 注意:这个目录仅用于浏览器操作相关的临时文件(下载、上传、截图等)
+    # 对于一般文件读写操作,请使用 agent.tools.builtin 中的文件工具
     base_dir = Path.cwd() / ".browser_use_files"
     base_dir = Path.cwd() / ".browser_use_files"
     base_dir.mkdir(parents=True, exist_ok=True)
     base_dir.mkdir(parents=True, exist_ok=True)
     _file_system = FileSystem(base_dir=str(base_dir))
     _file_system = FileSystem(base_dir=str(base_dir))
@@ -315,7 +323,7 @@ def _fetch_profile_id(cookie_type: str) -> Optional[str]:
 # ============================================================
 # ============================================================
 
 
 @tool()
 @tool()
-async def navigate_to_url(url: str, new_tab: bool = False, uid: str = "") -> ToolResult:
+async def navigate_to_url(url: str, new_tab: bool = False) -> ToolResult:
     """
     """
     导航到指定的 URL
     导航到指定的 URL
     Navigate to a specific URL
     Navigate to a specific URL
@@ -325,7 +333,6 @@ async def navigate_to_url(url: str, new_tab: bool = False, uid: str = "") -> Too
     Args:
     Args:
         url: 要访问的 URL 地址
         url: 要访问的 URL 地址
         new_tab: 是否在新标签页中打开(默认 False)
         new_tab: 是否在新标签页中打开(默认 False)
-        uid: 用户 ID(由框架自动注入)
 
 
     Returns:
     Returns:
         ToolResult: 包含导航结果的工具返回对象
         ToolResult: 包含导航结果的工具返回对象
@@ -356,7 +363,7 @@ async def navigate_to_url(url: str, new_tab: bool = False, uid: str = "") -> Too
 
 
 
 
 @tool()
 @tool()
-async def search_web(query: str, engine: str = "google", uid: str = "") -> ToolResult:
+async def search_web(query: str, engine: str = "google") -> ToolResult:
     """
     """
     使用搜索引擎搜索
     使用搜索引擎搜索
     Search the web using a search engine
     Search the web using a search engine
@@ -364,7 +371,6 @@ async def search_web(query: str, engine: str = "google", uid: str = "") -> ToolR
     Args:
     Args:
         query: 搜索关键词
         query: 搜索关键词
         engine: 搜索引擎 (google, duckduckgo, bing) - 默认: google
         engine: 搜索引擎 (google, duckduckgo, bing) - 默认: google
-        uid: 用户 ID(由框架自动注入)
 
 
     Returns:
     Returns:
         ToolResult: 搜索结果
         ToolResult: 搜索结果
@@ -394,16 +400,13 @@ async def search_web(query: str, engine: str = "google", uid: str = "") -> ToolR
 
 
 
 
 @tool()
 @tool()
-async def go_back(uid: str = "") -> ToolResult:
+async def go_back() -> ToolResult:
     """
     """
     返回到上一个页面
     返回到上一个页面
     Go back to the previous page
     Go back to the previous page
 
 
     模拟浏览器的"后退"按钮功能。
     模拟浏览器的"后退"按钮功能。
 
 
-    Args:
-        uid: 用户 ID(由框架自动注入)
-
     Returns:
     Returns:
         ToolResult: 包含返回操作结果的工具返回对象
         ToolResult: 包含返回操作结果的工具返回对象
     """
     """
@@ -424,7 +427,7 @@ async def go_back(uid: str = "") -> ToolResult:
 
 
 
 
 @tool()
 @tool()
-async def wait(seconds: int = 3, uid: str = "") -> ToolResult:
+async def wait(seconds: int = 3) -> ToolResult:
     """
     """
     等待指定的秒数
     等待指定的秒数
     Wait for a specified number of seconds
     Wait for a specified number of seconds
@@ -433,7 +436,6 @@ async def wait(seconds: int = 3, uid: str = "") -> ToolResult:
 
 
     Args:
     Args:
         seconds: 等待时间(秒),最大30秒
         seconds: 等待时间(秒),最大30秒
-        uid: 用户 ID(由框架自动注入)
 
 
     Returns:
     Returns:
         ToolResult: 包含等待操作结果的工具返回对象
         ToolResult: 包含等待操作结果的工具返回对象
@@ -462,14 +464,13 @@ async def wait(seconds: int = 3, uid: str = "") -> ToolResult:
 # ============================================================
 # ============================================================
 
 
 @tool()
 @tool()
-async def click_element(index: int, uid: str = "") -> ToolResult:
+async def click_element(index: int) -> ToolResult:
     """
     """
     通过索引点击页面元素
     通过索引点击页面元素
     Click an element by index
     Click an element by index
 
 
     Args:
     Args:
         index: 元素索引(从浏览器状态中获取)
         index: 元素索引(从浏览器状态中获取)
-        uid: 用户 ID(由框架自动注入)
 
 
     Returns:
     Returns:
         ToolResult: 包含点击操作结果的工具返回对象
         ToolResult: 包含点击操作结果的工具返回对象
@@ -500,7 +501,7 @@ async def click_element(index: int, uid: str = "") -> ToolResult:
 
 
 
 
 @tool()
 @tool()
-async def input_text(index: int, text: str, clear: bool = True, uid: str = "") -> ToolResult:
+async def input_text(index: int, text: str, clear: bool = True) -> ToolResult:
     """
     """
     在指定元素中输入文本
     在指定元素中输入文本
     Input text into an element
     Input text into an element
@@ -509,7 +510,6 @@ async def input_text(index: int, text: str, clear: bool = True, uid: str = "") -
         index: 元素索引(从浏览器状态中获取)
         index: 元素索引(从浏览器状态中获取)
         text: 要输入的文本内容
         text: 要输入的文本内容
         clear: 是否先清除现有文本(默认 True)
         clear: 是否先清除现有文本(默认 True)
-        uid: 用户 ID(由框架自动注入)
 
 
     Returns:
     Returns:
         ToolResult: 包含输入操作结果的工具返回对象
         ToolResult: 包含输入操作结果的工具返回对象
@@ -539,7 +539,7 @@ async def input_text(index: int, text: str, clear: bool = True, uid: str = "") -
 
 
 
 
 @tool()
 @tool()
-async def send_keys(keys: str, uid: str = "") -> ToolResult:
+async def send_keys(keys: str) -> ToolResult:
     """
     """
     发送键盘按键或快捷键
     发送键盘按键或快捷键
     Send keyboard keys or shortcuts
     Send keyboard keys or shortcuts
@@ -551,7 +551,6 @@ async def send_keys(keys: str, uid: str = "") -> ToolResult:
               - 单个按键: "Enter", "Escape", "PageDown", "Tab"
               - 单个按键: "Enter", "Escape", "PageDown", "Tab"
               - 组合键: "Control+o", "Shift+Tab", "Alt+F4"
               - 组合键: "Control+o", "Shift+Tab", "Alt+F4"
               - 功能键: "F1", "F2", ..., "F12"
               - 功能键: "F1", "F2", ..., "F12"
-        uid: 用户 ID(由框架自动注入)
 
 
     Returns:
     Returns:
         ToolResult: 包含按键操作结果的工具返回对象
         ToolResult: 包含按键操作结果的工具返回对象
@@ -580,7 +579,7 @@ async def send_keys(keys: str, uid: str = "") -> ToolResult:
 
 
 
 
 @tool()
 @tool()
-async def upload_file(index: int, path: str, uid: str = "") -> ToolResult:
+async def upload_file(index: int, path: str) -> ToolResult:
     """
     """
     上传文件到文件输入元素
     上传文件到文件输入元素
     Upload a file to a file input element
     Upload a file to a file input element
@@ -588,7 +587,6 @@ async def upload_file(index: int, path: str, uid: str = "") -> ToolResult:
     Args:
     Args:
         index: 文件输入框的元素索引
         index: 文件输入框的元素索引
         path: 要上传的文件路径(绝对路径)
         path: 要上传的文件路径(绝对路径)
-        uid: 用户 ID(由框架自动注入)
 
 
     Returns:
     Returns:
         ToolResult: 包含上传操作结果的工具返回对象
         ToolResult: 包含上传操作结果的工具返回对象
@@ -627,7 +625,7 @@ async def upload_file(index: int, path: str, uid: str = "") -> ToolResult:
 
 
 @tool()
 @tool()
 async def scroll_page(down: bool = True, pages: float = 1.0,
 async def scroll_page(down: bool = True, pages: float = 1.0,
-                     index: Optional[int] = None, uid: str = "") -> ToolResult:
+                     index: Optional[int] = None) -> ToolResult:
     """
     """
     滚动页面或元素
     滚动页面或元素
     Scroll the page or a specific element
     Scroll the page or a specific element
@@ -636,7 +634,6 @@ async def scroll_page(down: bool = True, pages: float = 1.0,
         down: True 向下滚动,False 向上滚动
         down: True 向下滚动,False 向上滚动
         pages: 滚动页数(0.5=半页,1=全页,10=滚动到底部/顶部)
         pages: 滚动页数(0.5=半页,1=全页,10=滚动到底部/顶部)
         index: 可选,滚动特定元素(如下拉框内部)
         index: 可选,滚动特定元素(如下拉框内部)
-        uid: 用户 ID(由框架自动注入)
 
 
     Returns:
     Returns:
         ToolResult: 滚动结果
         ToolResult: 滚动结果
@@ -668,7 +665,7 @@ async def scroll_page(down: bool = True, pages: float = 1.0,
 
 
 
 
 @tool()
 @tool()
-async def find_text(text: str, uid: str = "") -> ToolResult:
+async def find_text(text: str) -> ToolResult:
     """
     """
     查找页面中的文本并滚动到该位置
     查找页面中的文本并滚动到该位置
     Find text on the page and scroll to it
     Find text on the page and scroll to it
@@ -677,7 +674,6 @@ async def find_text(text: str, uid: str = "") -> ToolResult:
 
 
     Args:
     Args:
         text: 要查找的文本内容
         text: 要查找的文本内容
-        uid: 用户 ID(由框架自动注入)
 
 
     Returns:
     Returns:
         ToolResult: 包含查找结果的工具返回对象
         ToolResult: 包含查找结果的工具返回对象
@@ -705,16 +701,13 @@ async def find_text(text: str, uid: str = "") -> ToolResult:
 
 
 
 
 @tool()
 @tool()
-async def screenshot(uid: str = "") -> ToolResult:
+async def screenshot() -> ToolResult:
     """
     """
     请求在下次观察中包含页面截图
     请求在下次观察中包含页面截图
     Request a screenshot to be included in the next observation
     Request a screenshot to be included in the next observation
 
 
     用于视觉检查页面状态,帮助理解页面布局和内容。
     用于视觉检查页面状态,帮助理解页面布局和内容。
 
 
-    Args:
-        uid: 用户 ID(由框架自动注入)
-
     Returns:
     Returns:
         ToolResult: 包含截图请求结果的工具返回对象
         ToolResult: 包含截图请求结果的工具返回对象
 
 
@@ -745,14 +738,13 @@ async def screenshot(uid: str = "") -> ToolResult:
 # ============================================================
 # ============================================================
 
 
 @tool()
 @tool()
-async def switch_tab(tab_id: str, uid: str = "") -> ToolResult:
+async def switch_tab(tab_id: str) -> ToolResult:
     """
     """
     切换到指定标签页
     切换到指定标签页
     Switch to a different browser tab
     Switch to a different browser tab
 
 
     Args:
     Args:
         tab_id: 4字符标签ID(target_id 的最后4位)
         tab_id: 4字符标签ID(target_id 的最后4位)
-        uid: 用户 ID(由框架自动注入)
 
 
     Returns:
     Returns:
         ToolResult: 切换结果
         ToolResult: 切换结果
@@ -781,14 +773,13 @@ async def switch_tab(tab_id: str, uid: str = "") -> ToolResult:
 
 
 
 
 @tool()
 @tool()
-async def close_tab(tab_id: str, uid: str = "") -> ToolResult:
+async def close_tab(tab_id: str) -> ToolResult:
     """
     """
     关闭指定标签页
     关闭指定标签页
     Close a browser tab
     Close a browser tab
 
 
     Args:
     Args:
         tab_id: 4字符标签ID
         tab_id: 4字符标签ID
-        uid: 用户 ID(由框架自动注入)
 
 
     Returns:
     Returns:
         ToolResult: 关闭结果
         ToolResult: 关闭结果
@@ -821,14 +812,13 @@ async def close_tab(tab_id: str, uid: str = "") -> ToolResult:
 # ============================================================
 # ============================================================
 
 
 @tool()
 @tool()
-async def get_dropdown_options(index: int, uid: str = "") -> ToolResult:
+async def get_dropdown_options(index: int) -> ToolResult:
     """
     """
     获取下拉框的所有选项
     获取下拉框的所有选项
     Get options from a dropdown element
     Get options from a dropdown element
 
 
     Args:
     Args:
         index: 下拉框的元素索引
         index: 下拉框的元素索引
-        uid: 用户 ID(由框架自动注入)
 
 
     Returns:
     Returns:
         ToolResult: 包含所有选项的结果
         ToolResult: 包含所有选项的结果
@@ -856,7 +846,7 @@ async def get_dropdown_options(index: int, uid: str = "") -> ToolResult:
 
 
 
 
 @tool()
 @tool()
-async def select_dropdown_option(index: int, text: str, uid: str = "") -> ToolResult:
+async def select_dropdown_option(index: int, text: str) -> ToolResult:
     """
     """
     选择下拉框选项
     选择下拉框选项
     Select an option from a dropdown
     Select an option from a dropdown
@@ -864,7 +854,6 @@ async def select_dropdown_option(index: int, text: str, uid: str = "") -> ToolRe
     Args:
     Args:
         index: 下拉框的元素索引
         index: 下拉框的元素索引
         text: 要选择的选项文本(精确匹配)
         text: 要选择的选项文本(精确匹配)
-        uid: 用户 ID(由框架自动注入)
 
 
     Returns:
     Returns:
         ToolResult: 选择结果
         ToolResult: 选择结果
@@ -898,7 +887,7 @@ async def select_dropdown_option(index: int, text: str, uid: str = "") -> ToolRe
 
 
 @tool()
 @tool()
 async def extract_content(query: str, extract_links: bool = False,
 async def extract_content(query: str, extract_links: bool = False,
-                         start_from_char: int = 0, uid: str = "") -> ToolResult:
+                         start_from_char: int = 0) -> ToolResult:
     """
     """
     使用 LLM 从页面提取结构化数据
     使用 LLM 从页面提取结构化数据
     Extract content from the current page using LLM
     Extract content from the current page using LLM
@@ -907,7 +896,6 @@ async def extract_content(query: str, extract_links: bool = False,
         query: 提取查询(告诉 LLM 要提取什么内容)
         query: 提取查询(告诉 LLM 要提取什么内容)
         extract_links: 是否提取链接(默认 False,节省 token)
         extract_links: 是否提取链接(默认 False,节省 token)
         start_from_char: 从哪个字符开始提取(用于分页提取大内容)
         start_from_char: 从哪个字符开始提取(用于分页提取大内容)
-        uid: 用户 ID(由框架自动注入)
 
 
     Returns:
     Returns:
         ToolResult: 提取的内容
         ToolResult: 提取的内容
@@ -946,16 +934,13 @@ async def extract_content(query: str, extract_links: bool = False,
 
 
 
 
 @tool()
 @tool()
-async def get_page_html(uid: str = "") -> ToolResult:
+async def get_page_html() -> ToolResult:
     """
     """
     获取当前页面的完整 HTML
     获取当前页面的完整 HTML
     Get the full HTML of the current page
     Get the full HTML of the current page
 
 
     返回当前页面的完整 HTML 源代码。
     返回当前页面的完整 HTML 源代码。
 
 
-    Args:
-        uid: 用户 ID(由框架自动注入)
-
     Returns:
     Returns:
         ToolResult: 包含页面 HTML 的工具返回对象
         ToolResult: 包含页面 HTML 的工具返回对象
 
 
@@ -1011,16 +996,13 @@ async def get_page_html(uid: str = "") -> ToolResult:
 
 
 
 
 @tool()
 @tool()
-async def get_selector_map(uid: str = "") -> ToolResult:
+async def get_selector_map() -> ToolResult:
     """
     """
     获取当前页面的元素索引映射
     获取当前页面的元素索引映射
     Get the selector map of interactive elements on the current page
     Get the selector map of interactive elements on the current page
 
 
     返回页面所有可交互元素的索引字典,用于后续的元素操作。
     返回页面所有可交互元素的索引字典,用于后续的元素操作。
 
 
-    Args:
-        uid: 用户 ID(由框架自动注入)
-
     Returns:
     Returns:
         ToolResult: 包含元素映射的工具返回对象
         ToolResult: 包含元素映射的工具返回对象
 
 
@@ -1070,7 +1052,7 @@ async def get_selector_map(uid: str = "") -> ToolResult:
 # ============================================================
 # ============================================================
 
 
 @tool()
 @tool()
-async def evaluate(code: str, uid: str = "") -> ToolResult:
+async def evaluate(code: str) -> ToolResult:
     """
     """
     在页面中执行 JavaScript 代码
     在页面中执行 JavaScript 代码
     Execute JavaScript code in the page context
     Execute JavaScript code in the page context
@@ -1079,7 +1061,6 @@ async def evaluate(code: str, uid: str = "") -> ToolResult:
 
 
     Args:
     Args:
         code: 要执行的 JavaScript 代码字符串
         code: 要执行的 JavaScript 代码字符串
-        uid: 用户 ID(由框架自动注入)
 
 
     Returns:
     Returns:
         ToolResult: 包含执行结果的工具返回对象
         ToolResult: 包含执行结果的工具返回对象
@@ -1113,7 +1094,7 @@ async def evaluate(code: str, uid: str = "") -> ToolResult:
 
 
 
 
 @tool()
 @tool()
-async def ensure_login_with_cookies(cookie_type: str, url: str = "https://www.xiaohongshu.com", uid: str = "") -> ToolResult:
+async def ensure_login_with_cookies(cookie_type: str, url: str = "https://www.xiaohongshu.com") -> ToolResult:
     """
     """
     检查登录状态并在需要时注入 cookies
     检查登录状态并在需要时注入 cookies
     """
     """
@@ -1204,149 +1185,13 @@ async def ensure_login_with_cookies(cookie_type: str, url: str = "https://www.xi
         )
         )
 
 
 
 
-# ============================================================
-# 文件系统工具 (File System Tools)
-# ============================================================
-
-@tool()
-async def write_file(file_name: str, content: str, append: bool = False, uid: str = "") -> ToolResult:
-    """
-    写入文件到本地文件系统
-    Write content to a local file
-
-    支持多种文件格式的写入操作。
-
-    Args:
-        file_name: 文件名(包含扩展名)
-        content: 要写入的文件内容
-        append: 是否追加模式(默认 False,覆盖写入)
-        uid: 用户 ID(由框架自动注入)
-
-    Returns:
-        ToolResult: 包含写入结果的工具返回对象
-
-    Example:
-        write_file("output.txt", "Hello World")
-        write_file("data.json", '{"key": "value"}')
-
-    Note:
-        支持的文件格式: .txt, .md, .json, .jsonl, .csv, .pdf
-    """
-    try:
-        browser, tools = await get_browser_session()
-
-        result = await tools.write_file(
-            file_name=file_name,
-            content=content,
-            append=append,
-            file_system=_file_system
-        )
-
-        return action_result_to_tool_result(result, f"写入文件: {file_name}")
-
-    except Exception as e:
-        return ToolResult(
-            title="写入文件失败",
-            output="",
-            error=f"Failed to write file: {str(e)}",
-            long_term_memory=f"写入文件 {file_name} 失败"
-        )
-
-
-@tool()
-async def read_file(file_name: str, uid: str = "") -> ToolResult:
-    """
-    读取文件内容
-    Read content from a local file
-
-    支持多种文件格式的读取操作。
-
-    Args:
-        file_name: 文件名(包含扩展名)
-        uid: 用户 ID(由框架自动注入)
-
-    Returns:
-        ToolResult: 包含文件内容的工具返回对象
-
-    Example:
-        read_file("input.txt")
-        read_file("data.json")
-
-    Note:
-        支持的文件格式: 文本文件、PDF、DOCX、图片等
-    """
-    try:
-        browser, tools = await get_browser_session()
-
-        result = await tools.read_file(
-            file_name=file_name,
-            available_file_paths=[],
-            file_system=_file_system
-        )
-
-        return action_result_to_tool_result(result, f"读取文件: {file_name}")
-
-    except Exception as e:
-        return ToolResult(
-            title="读取文件失败",
-            output="",
-            error=f"Failed to read file: {str(e)}",
-            long_term_memory=f"读取文件 {file_name} 失败"
-        )
-
-
-@tool()
-async def replace_file(file_name: str, old_str: str, new_str: str, uid: str = "") -> ToolResult:
-    """
-    替换文件中的特定文本
-    Replace specific text in a file
-
-    在文件中查找并替换指定的文本内容。
-
-    Args:
-        file_name: 文件名(包含扩展名)
-        old_str: 要替换的文本
-        new_str: 新文本
-        uid: 用户 ID(由框架自动注入)
-
-    Returns:
-        ToolResult: 包含替换结果的工具返回对象
-
-    Example:
-        replace_file("config.txt", "old_value", "new_value")
-
-    Note:
-        - 会替换文件中所有匹配的文本
-        - 如果找不到要替换的文本,会返回警告
-    """
-    try:
-        browser, tools = await get_browser_session()
-
-        result = await tools.replace_file(
-            file_name=file_name,
-            old_str=old_str,
-            new_str=new_str,
-            file_system=_file_system
-        )
-
-        return action_result_to_tool_result(result, f"替换文件内容: {file_name}")
-
-    except Exception as e:
-        return ToolResult(
-            title="替换文件失败",
-            output="",
-            error=f"Failed to replace file content: {str(e)}",
-            long_term_memory=f"替换文件 {file_name} 失败"
-        )
-
-
 # ============================================================
 # ============================================================
 # 等待用户操作工具 (Wait for User Action)
 # 等待用户操作工具 (Wait for User Action)
 # ============================================================
 # ============================================================
 
 
 @tool()
 @tool()
 async def wait_for_user_action(message: str = "Please complete the action in browser",
 async def wait_for_user_action(message: str = "Please complete the action in browser",
-                               timeout: int = 300, uid: str = "") -> ToolResult:
+                               timeout: int = 300) -> ToolResult:
     """
     """
     等待用户在浏览器中完成操作(如登录)
     等待用户在浏览器中完成操作(如登录)
     Wait for user to complete an action in the browser (e.g., login)
     Wait for user to complete an action in the browser (e.g., login)
@@ -1356,7 +1201,6 @@ async def wait_for_user_action(message: str = "Please complete the action in bro
     Args:
     Args:
         message: 提示用户需要完成的操作
         message: 提示用户需要完成的操作
         timeout: 最大等待时间(秒),默认 300 秒(5 分钟)
         timeout: 最大等待时间(秒),默认 300 秒(5 分钟)
-        uid: 用户 ID(由框架自动注入)
 
 
     Returns:
     Returns:
         ToolResult: 包含等待结果的工具返回对象
         ToolResult: 包含等待结果的工具返回对象
@@ -1419,7 +1263,7 @@ async def wait_for_user_action(message: str = "Please complete the action in bro
 
 
 @tool()
 @tool()
 async def done(text: str, success: bool = True,
 async def done(text: str, success: bool = True,
-              files_to_display: Optional[List[str]] = None, uid: str = "") -> ToolResult:
+              files_to_display: Optional[List[str]] = None) -> ToolResult:
     """
     """
     标记任务完成并返回最终消息
     标记任务完成并返回最终消息
     Mark the task as complete and return final message to user
     Mark the task as complete and return final message to user
@@ -1428,7 +1272,6 @@ async def done(text: str, success: bool = True,
         text: 给用户的最终消息
         text: 给用户的最终消息
         success: 任务是否成功完成
         success: 任务是否成功完成
         files_to_display: 可选的要显示的文件路径列表
         files_to_display: 可选的要显示的文件路径列表
-        uid: 用户 ID(由框架自动注入)
 
 
     Returns:
     Returns:
         ToolResult: 完成结果
         ToolResult: 完成结果
@@ -1634,11 +1477,6 @@ __all__ = [
     'evaluate',
     'evaluate',
     'ensure_login_with_cookies',
     'ensure_login_with_cookies',
 
 
-    # 文件系统工具
-    'write_file',
-    'read_file',
-    'replace_file',
-
     # 等待用户操作
     # 等待用户操作
     'wait_for_user_action',
     'wait_for_user_action',
 
 

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

@@ -23,7 +23,6 @@ async def edit_file(
     old_string: str,
     old_string: str,
     new_string: str,
     new_string: str,
     replace_all: bool = False,
     replace_all: bool = False,
-    uid: str = "",
     context: Optional[ToolContext] = None
     context: Optional[ToolContext] = None
 ) -> ToolResult:
 ) -> ToolResult:
     """
     """
@@ -45,7 +44,6 @@ async def edit_file(
         old_string: 要替换的文本
         old_string: 要替换的文本
         new_string: 替换后的文本
         new_string: 替换后的文本
         replace_all: 是否替换所有匹配(默认 False,只替换唯一匹配)
         replace_all: 是否替换所有匹配(默认 False,只替换唯一匹配)
-        uid: 用户 ID
         context: 工具上下文
         context: 工具上下文
 
 
     Returns:
     Returns:

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

@@ -23,7 +23,6 @@ LIMIT = 100  # 最大返回数量(参考 opencode glob.ts:35)
 async def glob_files(
 async def glob_files(
     pattern: str,
     pattern: str,
     path: Optional[str] = None,
     path: Optional[str] = None,
-    uid: str = "",
     context: Optional[ToolContext] = None
     context: Optional[ToolContext] = None
 ) -> ToolResult:
 ) -> ToolResult:
     """
     """
@@ -34,7 +33,6 @@ async def glob_files(
     Args:
     Args:
         pattern: glob 模式(如 "*.py", "src/**/*.ts")
         pattern: glob 模式(如 "*.py", "src/**/*.ts")
         path: 搜索目录(默认当前目录)
         path: 搜索目录(默认当前目录)
-        uid: 用户 ID
         context: 工具上下文
         context: 工具上下文
 
 
     Returns:
     Returns:

+ 0 - 1
agent/tools/builtin/goal.py

@@ -30,7 +30,6 @@ async def goal(
     done: Optional[str] = None,
     done: Optional[str] = None,
     abandon: Optional[str] = None,
     abandon: Optional[str] = None,
     focus: Optional[str] = None,
     focus: Optional[str] = None,
-    uid: str = "",
     context: Optional[dict] = None
     context: Optional[dict] = None
 ) -> str:
 ) -> str:
     """
     """

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

@@ -26,7 +26,6 @@ async def grep_content(
     pattern: str,
     pattern: str,
     path: Optional[str] = None,
     path: Optional[str] = None,
     include: Optional[str] = None,
     include: Optional[str] = None,
-    uid: str = "",
     context: Optional[ToolContext] = None
     context: Optional[ToolContext] = None
 ) -> ToolResult:
 ) -> ToolResult:
     """
     """
@@ -40,7 +39,6 @@ async def grep_content(
         pattern: 正则表达式模式
         pattern: 正则表达式模式
         path: 搜索目录(默认当前目录)
         path: 搜索目录(默认当前目录)
         include: 文件模式(如 "*.py", "*.{ts,tsx}")
         include: 文件模式(如 "*.py", "*.{ts,tsx}")
-        uid: 用户 ID
         context: 工具上下文
         context: 工具上下文
 
 
     Returns:
     Returns:

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

@@ -28,7 +28,6 @@ async def read_file(
     file_path: str,
     file_path: str,
     offset: int = 0,
     offset: int = 0,
     limit: int = DEFAULT_READ_LIMIT,
     limit: int = DEFAULT_READ_LIMIT,
-    uid: str = "",
     context: Optional[ToolContext] = None
     context: Optional[ToolContext] = None
 ) -> ToolResult:
 ) -> ToolResult:
     """
     """
@@ -40,7 +39,6 @@ async def read_file(
         file_path: 文件路径(绝对路径或相对路径)
         file_path: 文件路径(绝对路径或相对路径)
         offset: 起始行号(从 0 开始)
         offset: 起始行号(从 0 开始)
         limit: 读取行数(默认 2000 行)
         limit: 读取行数(默认 2000 行)
-        uid: 用户 ID(自动注入)
         context: 工具上下文
         context: 工具上下文
 
 
     Returns:
     Returns:

+ 0 - 4
agent/tools/builtin/sandbox.py

@@ -54,7 +54,6 @@ async def sandbox_create_environment(
     server_url: str = None,
     server_url: str = None,
     timeout: float = DEFAULT_TIMEOUT,
     timeout: float = DEFAULT_TIMEOUT,
     context: Optional[ToolContext] = None,
     context: Optional[ToolContext] = None,
-    uid: str = "",
 ) -> ToolResult:
 ) -> ToolResult:
     """
     """
     创建一个隔离的 Docker 开发环境。
     创建一个隔离的 Docker 开发环境。
@@ -150,7 +149,6 @@ async def sandbox_run_shell(
     server_url: str = None,
     server_url: str = None,
     request_timeout: float = DEFAULT_TIMEOUT,
     request_timeout: float = DEFAULT_TIMEOUT,
     context: Optional[ToolContext] = None,
     context: Optional[ToolContext] = None,
-    uid: str = "",
 ) -> ToolResult:
 ) -> ToolResult:
     """
     """
     在指定的沙盒中执行 Shell 命令。
     在指定的沙盒中执行 Shell 命令。
@@ -271,7 +269,6 @@ async def sandbox_rebuild_with_ports(
     server_url: str = None,
     server_url: str = None,
     timeout: float = DEFAULT_TIMEOUT,
     timeout: float = DEFAULT_TIMEOUT,
     context: Optional[ToolContext] = None,
     context: Optional[ToolContext] = None,
-    uid: str = "",
 ) -> ToolResult:
 ) -> ToolResult:
     """
     """
     重建沙盒并应用新的端口映射。
     重建沙盒并应用新的端口映射。
@@ -367,7 +364,6 @@ async def sandbox_destroy_environment(
     server_url: str = None,
     server_url: str = None,
     timeout: float = DEFAULT_TIMEOUT,
     timeout: float = DEFAULT_TIMEOUT,
     context: Optional[ToolContext] = None,
     context: Optional[ToolContext] = None,
-    uid: str = "",
 ) -> ToolResult:
 ) -> ToolResult:
     """
     """
     销毁沙盒环境,释放资源。
     销毁沙盒环境,释放资源。

+ 0 - 4
agent/tools/builtin/search.py

@@ -77,7 +77,6 @@ async def search_posts(
     channel: str = "xhs",
     channel: str = "xhs",
     cursor: str = "0",
     cursor: str = "0",
     max_count: int = 5,
     max_count: int = 5,
-    uid: str = "",
 ) -> ToolResult:
 ) -> ToolResult:
     """
     """
     帖子搜索
     帖子搜索
@@ -98,7 +97,6 @@ async def search_posts(
             - weibo: 微博
             - weibo: 微博
         cursor: 分页游标,默认为 "0"(第一页)
         cursor: 分页游标,默认为 "0"(第一页)
         max_count: 返回的最大条数,默认为 5
         max_count: 返回的最大条数,默认为 5
-        uid: 用户ID(自动注入)
 
 
     Returns:
     Returns:
         ToolResult 包含搜索结果:
         ToolResult 包含搜索结果:
@@ -185,7 +183,6 @@ async def search_posts(
 async def get_search_suggestions(
 async def get_search_suggestions(
     keyword: str,
     keyword: str,
     channel: str = "xhs",
     channel: str = "xhs",
-    uid: str = "",
 ) -> ToolResult:
 ) -> ToolResult:
     """
     """
     获取搜索关键词补全建议
     获取搜索关键词补全建议
@@ -202,7 +199,6 @@ async def get_search_suggestions(
             - douyin: 抖音
             - douyin: 抖音
             - bili: B站
             - bili: B站
             - zhihu: 知乎
             - zhihu: 知乎
-        uid: 用户ID(自动注入)
 
 
     Returns:
     Returns:
         ToolResult 包含建议词数据:
         ToolResult 包含建议词数据:

+ 0 - 4
agent/tools/builtin/skill.py

@@ -71,7 +71,6 @@ def _check_skill_setup(skill_name: str) -> Optional[str]:
 async def skill(
 async def skill(
     skill_name: str,
     skill_name: str,
     skills_dir: Optional[str] = None,
     skills_dir: Optional[str] = None,
-    uid: str = ""
 ) -> ToolResult:
 ) -> ToolResult:
     """
     """
     加载指定的 skill 文档
     加载指定的 skill 文档
@@ -79,7 +78,6 @@ async def skill(
     Args:
     Args:
         skill_name: Skill 名称(如 "browser-use", "error-handling")
         skill_name: Skill 名称(如 "browser-use", "error-handling")
         skills_dir: Skills 目录路径(可选,默认按优先级查找)
         skills_dir: Skills 目录路径(可选,默认按优先级查找)
-        uid: 用户 ID(自动注入)
 
 
     Returns:
     Returns:
         ToolResult: 包含 skill 的详细内容
         ToolResult: 包含 skill 的详细内容
@@ -187,14 +185,12 @@ async def skill(
 )
 )
 async def list_skills(
 async def list_skills(
     skills_dir: Optional[str] = None,
     skills_dir: Optional[str] = None,
-    uid: str = ""
 ) -> ToolResult:
 ) -> ToolResult:
     """
     """
     列出所有可用的 skills
     列出所有可用的 skills
 
 
     Args:
     Args:
         skills_dir: Skills 目录路径(可选)
         skills_dir: Skills 目录路径(可选)
-        uid: 用户 ID(自动注入)
 
 
     Returns:
     Returns:
         ToolResult: 包含所有 skills 的列表
         ToolResult: 包含所有 skills 的列表

+ 25 - 9
agent/tools/builtin/write.py

@@ -5,6 +5,7 @@ Write Tool - 文件写入工具
 
 
 核心功能:
 核心功能:
 - 创建新文件或覆盖现有文件
 - 创建新文件或覆盖现有文件
+- 支持追加模式(append)
 - 生成 diff 预览
 - 生成 diff 预览
 """
 """
 
 
@@ -15,22 +16,22 @@ import difflib
 from agent.tools import tool, ToolResult, ToolContext
 from agent.tools import tool, ToolResult, ToolContext
 
 
 
 
-@tool(description="写入文件内容(创建新文件或覆盖现有文件)")
+@tool(description="写入文件内容(创建新文件、覆盖现有文件或追加内容)")
 async def write_file(
 async def write_file(
     file_path: str,
     file_path: str,
     content: str,
     content: str,
-    uid: str = "",
+    append: bool = False,
     context: Optional[ToolContext] = None
     context: Optional[ToolContext] = None
 ) -> ToolResult:
 ) -> ToolResult:
     """
     """
     写入文件
     写入文件
 
 
-    参考 OpenCode 实现
+    参考 OpenCode 实现,并添加追加模式支持
 
 
     Args:
     Args:
         file_path: 文件路径
         file_path: 文件路径
         content: 文件内容
         content: 文件内容
-        uid: 用户 ID
+        append: 是否追加模式(默认 False,覆盖写入)
         context: 工具上下文
         context: 工具上下文
 
 
     Returns:
     Returns:
@@ -59,9 +60,15 @@ async def write_file(
         except Exception:
         except Exception:
             old_content = ""
             old_content = ""
 
 
+    # 确定最终内容
+    if append and existed:
+        new_content = old_content + content
+    else:
+        new_content = content
+
     # 生成 diff
     # 生成 diff
     if existed and old_content:
     if existed and old_content:
-        diff = _create_diff(str(path), old_content, content)
+        diff = _create_diff(str(path), old_content, new_content)
     else:
     else:
         diff = f"(新建文件: {path.name})"
         diff = f"(新建文件: {path.name})"
 
 
@@ -71,7 +78,7 @@ async def write_file(
     # 写入文件
     # 写入文件
     try:
     try:
         with open(path, 'w', encoding='utf-8') as f:
         with open(path, 'w', encoding='utf-8') as f:
-            f.write(content)
+            f.write(new_content)
     except Exception as e:
     except Exception as e:
         return ToolResult(
         return ToolResult(
             title="写入失败",
             title="写入失败",
@@ -80,17 +87,26 @@ async def write_file(
         )
         )
 
 
     # 统计
     # 统计
-    lines = content.count('\n')
+    lines = new_content.count('\n')
+
+    # 构建操作描述
+    if append and existed:
+        operation = "追加内容到"
+    elif existed:
+        operation = "覆盖"
+    else:
+        operation = "创建"
 
 
     return ToolResult(
     return ToolResult(
         title=path.name,
         title=path.name,
-        output=f"文件写入成功\n\n{diff}",
+        output=f"文件写入成功 ({operation})\n\n{diff}",
         metadata={
         metadata={
             "existed": existed,
             "existed": existed,
+            "append": append,
             "lines": lines,
             "lines": lines,
             "diff": diff
             "diff": diff
         },
         },
-        long_term_memory=f"{'覆盖' if existed else '创建'}文件 {path.name}"
+        long_term_memory=f"{operation}文件 {path.name}"
     )
     )
 
 
 
 

+ 7 - 3
agent/tools/registry.py

@@ -211,11 +211,15 @@ class ToolRegistry:
 				current_url = context.get("page_url") if context else None
 				current_url = context.get("page_url") if context else None
 				arguments = replace_sensitive_data(arguments, sensitive_data, current_url)
 				arguments = replace_sensitive_data(arguments, sensitive_data, current_url)
 
 
-			# 注入 uid
-			kwargs = {**arguments, "uid": uid}
+			# 准备参数:只注入函数需要的参数
+			kwargs = {**arguments}
+			sig = inspect.signature(func)
+
+			# 注入 uid(如果函数接受)
+			if "uid" in sig.parameters:
+				kwargs["uid"] = uid
 
 
 			# 注入 context(如果函数接受)
 			# 注入 context(如果函数接受)
-			sig = inspect.signature(func)
 			if "context" in sig.parameters:
 			if "context" in sig.parameters:
 				kwargs["context"] = context
 				kwargs["context"] = context
 
 

+ 5 - 2
docs/README.md

@@ -246,11 +246,14 @@ class Message:
 ### [Context 管理与可视化](./context-management.md)
 ### [Context 管理与可视化](./context-management.md)
 - GoalTree:层级目标管理(嵌套 JSON,注入 LLM)
 - GoalTree:层级目标管理(嵌套 JSON,注入 LLM)
 - Goal ID 设计:内部 ID(稳定)vs 显示序号(连续,给 LLM)
 - Goal ID 设计:内部 ID(稳定)vs 显示序号(连续,给 LLM)
-- goal 工具:计划管理(add, done, abandon, focus)
-- explore 工具:并行探索-合并
+- goal 工具:计划管理(add, after, under, done, abandon, focus)
+- 位置控制:after(同层级追加)、under(子任务拆解)
+- Plan 注入策略:完整展示当前目标及其父链
+- explore 工具:并行探索-合并,收集每个 Sub-Trace 的最后消息作为元数据
 - 回溯机制:未执行直接修改 / 已执行标记废弃+新分支
 - 回溯机制:未执行直接修改 / 已执行标记废弃+新分支
 - DAG 可视化:节点=结果,边=动作,边可展开/折叠
 - DAG 可视化:节点=结果,边=动作,边可展开/折叠
 - 数据结构:GoalTree + Messages(扁平列表,goal_id 关联)
 - 数据结构:GoalTree + Messages(扁平列表,goal_id 关联)
+- Sub-Trace 元数据:last_message, summary, stats(用于辅助决策和可视化)
 
 
 ### [工具系统](./tools.md)
 ### [工具系统](./tools.md)
 - 工具定义和注册
 - 工具定义和注册

+ 224 - 18
docs/context-management.md

@@ -356,6 +356,7 @@ class Goal:
     # agent_call 特有
     # agent_call 特有
     sub_trace_ids: Optional[List[str]] = None  # 启动的 Sub-Trace IDs
     sub_trace_ids: Optional[List[str]] = None  # 启动的 Sub-Trace IDs
     agent_call_mode: Optional[str] = None      # "explore" | "delegate" | "sequential"
     agent_call_mode: Optional[str] = None      # "explore" | "delegate" | "sequential"
+    sub_trace_metadata: Optional[Dict[str, Dict[str, Any]]] = None  # Sub-Trace 元数据
 
 
     # 统计(后端维护,用于可视化边的数据)
     # 统计(后端维护,用于可视化边的数据)
     self_stats: GoalStats                # 自身统计(仅直接关联的 messages)
     self_stats: GoalStats                # 自身统计(仅直接关联的 messages)
@@ -369,6 +370,35 @@ class Goal:
 - **层级关系**:通过 `parent_id` 字段维护
 - **层级关系**:通过 `parent_id` 字段维护
 - **显示序号**:`to_prompt()` 时动态生成连续有意义的编号("1", "2", "2.1", "2.2"...)
 - **显示序号**:`to_prompt()` 时动态生成连续有意义的编号("1", "2", "2.1", "2.2"...)
 
 
+**sub_trace_metadata 字段**(`agent_call` 类型 Goal 专用):
+存储各 Sub-Trace 的关键信息,用于辅助决策和可视化:
+```python
+{
+    "sub_trace_id_1": {
+        "task": "JWT 方案",                    # Sub-Trace 任务描述
+        "status": "completed",                  # Sub-Trace 状态
+        "summary": "实现完成,使用 JWT token",   # Sub-Trace 总结
+        "last_message": {                       # 最后一条 assistant 消息
+            "role": "assistant",
+            "description": "生成 JWT token 并返回",
+            "content": "...",                   # 截断至 500 字符
+            "created_at": "2026-02-05T10:30:00"
+        },
+        "stats": {                              # 统计信息
+            "message_count": 8,
+            "total_tokens": 4000,
+            "total_cost": 0.05
+        }
+    },
+    # ... 其他 Sub-Trace
+}
+```
+
+**用途**:
+- 帮助主 Agent 决策:基于各分支的最终输出,决定是否需要展开查看详细信息
+- 前端可视化:在折叠视图中显示关键信息,用户快速判断是否需要展开
+- 调试追踪:快速了解每个分支的执行结果
+
 **统计更新逻辑**:
 **统计更新逻辑**:
 - 每次添加 Message 时,更新对应 Goal 的 `self_stats`,并沿祖先链向上更新所有祖先的 `cumulative_stats`
 - 每次添加 Message 时,更新对应 Goal 的 `self_stats`,并沿祖先链向上更新所有祖先的 `cumulative_stats`
 - 可视化中,折叠边使用 target Goal 的 `cumulative_stats`,展开边使用 `self_stats`
 - 可视化中,折叠边使用 target Goal 的 `cumulative_stats`,展开边使用 `self_stats`
@@ -395,7 +425,7 @@ class Message:
     trace_id: str                        # 所属 Trace ID
     trace_id: str                        # 所属 Trace ID
     role: Literal["assistant", "tool"]   # 和 LLM API 一致
     role: Literal["assistant", "tool"]   # 和 LLM API 一致
     sequence: int                        # 当前 Trace 内的顺序
     sequence: int                        # 当前 Trace 内的顺序
-    goal_id: str                         # 关联的 Goal 内部 ID(如 "1", "2"
+    goal_id: Optional[str] = None        # 关联的 Goal 内部 ID(None = 还没有创建 Goal
     tool_call_id: Optional[str]          # tool 消息关联对应的 tool_call
     tool_call_id: Optional[str]          # tool 消息关联对应的 tool_call
     content: Any                         # 消息内容(和 LLM API 格式一致)
     content: Any                         # 消息内容(和 LLM API 格式一致)
     description: str                     # 消息描述(系统自动生成)
     description: str                     # 消息描述(系统自动生成)
@@ -411,6 +441,11 @@ class Message:
 - `assistant` 消息:优先取 content 中的 text,若无 text 则生成 "tool call: XX, XX"
 - `assistant` 消息:优先取 content 中的 text,若无 text 则生成 "tool call: XX, XX"
 - `tool` 消息:使用 tool name
 - `tool` 消息:使用 tool name
 
 
+**goal_id 说明**:
+- 通常关联到某个 Goal 的内部 ID(如 "1", "2")
+- 可以为 None:在 Trace 开始、还没有创建任何 Goal 时
+- 前端通过虚拟 START 节点展示 goal_id=None 的 messages
+
 **实现**:`agent/execution/models.py:Message`
 **实现**:`agent/execution/models.py:Message`
 
 
 **Message 类型说明**:
 **Message 类型说明**:
@@ -434,34 +469,72 @@ GET /api/traces/2f8d3a1c-4b6e-4f9a-8c2d-1e5b7a9f3c4d@explore-20260204220012-001/
 @tool
 @tool
 def goal(
 def goal(
     add: Optional[str] = None,       # 添加目标(逗号分隔多个)
     add: Optional[str] = None,       # 添加目标(逗号分隔多个)
+    reason: Optional[str] = None,    # 创建理由(逗号分隔多个,与 add 一一对应)
+    after: Optional[str] = None,     # 在指定目标后面添加(同层级)
+    under: 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,     # 切换焦点到指定显示序号
+    focus: Optional[str] = None,     # 切换焦点到指定 ID
 ) -> str:
 ) -> str:
     """管理执行计划。"""
     """管理执行计划。"""
 ```
 ```
 
 
 **实现**:`agent/goal/tool.py:goal_tool`
 **实现**:`agent/goal/tool.py:goal_tool`
 
 
-**层级支持**:`add` 添加到当前 focus 的 goal 下作为子目标。
+**位置控制**:
+- 不指定 `after`/`under`:添加到当前 focus 的 goal 下作为子目标(无 focus 时添加到顶层)
+- `after="X"`:在目标 X 后面添加兄弟节点(同层级)
+- `under="X"`:为目标 X 添加子目标(如已有子目标,追加到最后)
+- `after` 和 `under` 互斥,不可同时指定
+
+**设计原则**:优先使用 `after` 明确指定位置,`under` 用于首次拆解或追加子任务。
+
+**示例**:
 
 
 ```python
 ```python
-# 没有 focus 时,添加到顶层
+# 1. 默认行为:添加顶层目标(无 focus)
 goal(add="分析代码, 实现功能, 测试")
 goal(add="分析代码, 实现功能, 测试")
-# 结果:
 # [ ] 1. 分析代码
 # [ ] 1. 分析代码
 # [ ] 2. 实现功能
 # [ ] 2. 实现功能
 # [ ] 3. 测试
 # [ ] 3. 测试
 
 
-# focus 到某个 goal 后,add 添加为其子目标
-goal(focus="2")
-goal(add="设计接口, 实现代码")
-# 结果:
+# 2. 拆解子任务
+goal(add="设计接口, 实现代码", under="2")
+# [ ] 1. 分析代码
+# [ ] 2. 实现功能
+#     [ ] 2.1 设计接口
+#     [ ] 2.2 实现代码
+# [ ] 3. 测试
+
+# 3. 追加同层级任务
+goal(add="编写文档", after="3")
+# [ ] 1. 分析代码
+# [ ] 2. 实现功能
+#     [ ] 2.1 设计接口
+#     [ ] 2.2 实现代码
+# [ ] 3. 测试
+# [ ] 4. 编写文档
+
+# 4. 追加子任务(目标2已有子任务)
+goal(add="编写单元测试", under="2")
+# [ ] 1. 分析代码
+# [ ] 2. 实现功能
+#     [ ] 2.1 设计接口
+#     [ ] 2.2 实现代码
+#     [ ] 2.3 编写单元测试
+# [ ] 3. 测试
+# [ ] 4. 编写文档
+
+# 5. 使用 after 在子任务中间插入
+goal(add="代码审查", after="2.2")
 # [ ] 1. 分析代码
 # [ ] 1. 分析代码
-# [→] 2. 实现功能
+# [ ] 2. 实现功能
 #     [ ] 2.1 设计接口
 #     [ ] 2.1 设计接口
 #     [ ] 2.2 实现代码
 #     [ ] 2.2 实现代码
+#     [ ] 2.3 代码审查
+#     [ ] 2.4 编写单元测试
 # [ ] 3. 测试
 # [ ] 3. 测试
+# [ ] 4. 编写文档
 ```
 ```
 
 
 **状态流转**:
 **状态流转**:
@@ -535,9 +608,55 @@ async def explore_tool(branches: List[str], background: Optional[str] = None) ->
         *[run_agent(st, background) for st in sub_traces]
         *[run_agent(st, background) for st in sub_traces]
     )
     )
 
 
-    # 4. 汇总返回
-    summary = format_explore_results(results)
-    return summary
+    # 4. 收集元数据并汇总
+    sub_trace_metadata = {}
+    summary_parts = []
+
+    for sub_trace, result in zip(sub_traces, results):
+        # 获取 Sub-Trace 最新状态
+        updated_trace = await store.get_trace(sub_trace.trace_id)
+
+        # 获取最后一条 assistant 消息
+        messages = await store.get_messages(sub_trace.trace_id)
+        last_message = None
+        for msg in reversed(messages):
+            if msg.role == "assistant":
+                last_message = msg
+                break
+
+        # 构建元数据
+        sub_trace_metadata[sub_trace.trace_id] = {
+            "task": sub_trace.task,
+            "status": updated_trace.status if updated_trace else "unknown",
+            "summary": result.get("summary", "") if isinstance(result, dict) else "",
+            "last_message": {
+                "role": last_message.role,
+                "description": last_message.description,
+                "content": last_message.content[:500] if last_message.content else None,
+                "created_at": last_message.created_at.isoformat()
+            } if last_message else None,
+            "stats": {
+                "message_count": updated_trace.total_messages,
+                "total_tokens": updated_trace.total_tokens,
+                "total_cost": updated_trace.total_cost
+            }
+        }
+
+        # 组装摘要文本
+        summary_parts.append(f"### {sub_trace.task}")
+        summary_parts.append(result.get("summary", "执行完成"))
+
+    # 5. 更新 Goal,包含元数据
+    await store.update_goal(
+        current_trace_id,
+        current_goal_id,
+        status="completed",
+        summary=f"探索了 {len(branches)} 个方案",
+        sub_trace_metadata=sub_trace_metadata  # 保存元数据
+    )
+
+    # 6. 返回文本摘要(给 LLM)
+    return "\n".join(summary_parts)
 ```
 ```
 
 
 **权限配置**:
 **权限配置**:
@@ -641,7 +760,12 @@ def get_delegate_context() -> Dict[str, Any]:
 
 
 #### 1. Plan 注入
 #### 1. Plan 注入
 
 
-每次 LLM 调用时,在 system prompt 末尾注入当前计划状态。注入时过滤掉 abandoned 目标,使用连续的显示序号:
+每次 LLM 调用时,在 system prompt 末尾注入当前计划状态。注入时过滤掉 abandoned 目标,使用连续的显示序号。
+
+**展示策略**:
+- **完整展示**:所有顶层目标、当前 focus 目标的完整父链及其子树
+- **折叠其他**:非关键路径的子目标可折叠显示(显示节点数和状态)
+- **连续编号**:显示 ID 连续且有意义("1", "2", "2.1", "2.2")
 
 
 ```markdown
 ```markdown
 ## Current Plan
 ## Current Plan
@@ -654,13 +778,17 @@ def get_delegate_context() -> Dict[str, Any]:
     → 用户模型在 models/user.py,使用 bcrypt 加密
     → 用户模型在 models/user.py,使用 bcrypt 加密
 [→] 2. 实现功能
 [→] 2. 实现功能
     [✓] 2.1 设计接口
     [✓] 2.1 设计接口
+        → API 设计文档完成,使用 REST 风格
     [→] 2.2 实现登录接口  ← current
     [→] 2.2 实现登录接口  ← current
     [ ] 2.3 实现注册接口
     [ ] 2.3 实现注册接口
 [ ] 3. 测试
 [ ] 3. 测试
+    (3 subtasks)
 ```
 ```
 
 
 **实现**:`agent/goal/models.py:GoalTree.to_prompt`
 **实现**:`agent/goal/models.py:GoalTree.to_prompt`
 
 
+**注意**:当前 focus 目标的所有父目标及其子目标都会完整展示,确保 LLM 理解当前上下文。这样在使用 `after` 或 `under` 参数时,LLM 可以准确引用目标 ID。
+
 #### 2. 完成时压缩
 #### 2. 完成时压缩
 
 
 当调用 `goal(done="...")` 时:
 当调用 `goal(done="...")` 时:
@@ -718,6 +846,49 @@ Messages:
 
 
 **实现**:`agent/goal/compaction.py`
 **实现**:`agent/goal/compaction.py`
 
 
+#### 4. 用户编辑计划
+
+用户可以通过编辑 Goal 内的 goal() 工具调用来修改执行计划。
+
+**核心思路**:利用现有的 GoalTree 结构和 Message sequence,无需额外追踪字段。
+
+**编辑流程**:
+1. 用户选择某个 Goal,查看其内所有 messages
+2. 找到 goal() 调用,编辑其参数
+3. 系统废弃该 message 之后的所有内容:
+   - 废弃 sequence >= 该 message 的所有 messages
+   - 根据 GoalTree 结构,废弃受影响的 Goals(当前 Goal 的子孙、后续同级 Goals)
+4. 从该 Goal 重新执行
+
+**废弃判断**:
+- 基于 Message sequence:废弃 seq >= 编辑点的所有 messages
+- 基于 GoalTree 结构:废弃当前 Goal 的所有子节点、以及后续创建的 Goals
+- 不需要追踪"哪个 message 创建了哪个 Goal",结构本身就能判断
+
+**UI 展示**:
+- Goal 详情显示其内所有 messages
+- 突出显示 goal() 调用(assistant message 中包含 tool_calls)
+- 提供"编辑并重新执行"按钮
+
+**API**:
+```
+GET /api/traces/{trace_id}/messages?goal_id=X
+  # 返回 Goal X 的所有 messages
+
+PATCH /api/traces/{trace_id}/replay
+{
+  "from_message_id": "msg_123",
+  "modified_tool_call": {
+    "tool_call_id": "call_abc",
+    "new_arguments": {"add": "新任务列表", "under": "2"}
+  }
+}
+```
+
+**实现**:`agent/goal/replay.py`
+
+**注意**:这是最通用、最简单的编辑方式,完全基于现有数据结构。
+
 ### 可视化
 ### 可视化
 
 
 #### DAG 模型
 #### DAG 模型
@@ -1080,7 +1251,41 @@ GET /api/traces/2f8d3a1c-4b6e-4f9a-8c2d-1e5b7a9f3c4d
         "sub_trace_ids": [
         "sub_trace_ids": [
           "2f8d3a1c-4b6e-4f9a-8c2d-1e5b7a9f3c4d@explore-20260204220012-001",
           "2f8d3a1c-4b6e-4f9a-8c2d-1e5b7a9f3c4d@explore-20260204220012-001",
           "2f8d3a1c-4b6e-4f9a-8c2d-1e5b7a9f3c4d@explore-20260204220012-002"
           "2f8d3a1c-4b6e-4f9a-8c2d-1e5b7a9f3c4d@explore-20260204220012-002"
-        ]
+        ],
+        "sub_trace_metadata": {
+          "2f8d3a1c-4b6e-4f9a-8c2d-1e5b7a9f3c4d@explore-20260204220012-001": {
+            "task": "JWT 方案",
+            "status": "completed",
+            "summary": "实现完成,使用 JWT token",
+            "last_message": {
+              "role": "assistant",
+              "description": "生成 JWT token 并返回",
+              "content": "JWT 方案实现完成。使用 jsonwebtoken 库生成 token,包含 user_id 和过期时间...",
+              "created_at": "2026-02-05T10:30:00"
+            },
+            "stats": {
+              "message_count": 8,
+              "total_tokens": 4000,
+              "total_cost": 0.05
+            }
+          },
+          "2f8d3a1c-4b6e-4f9a-8c2d-1e5b7a9f3c4d@explore-20260204220012-002": {
+            "task": "Session 方案",
+            "status": "completed",
+            "summary": "实现完成,使用 Redis 存储 session",
+            "last_message": {
+              "role": "assistant",
+              "description": "配置 Redis 并实现 session 管理",
+              "content": "Session 方案实现完成。使用 Redis 存储 session,支持过期自动清理...",
+              "created_at": "2026-02-05T10:32:00"
+            },
+            "stats": {
+              "message_count": 12,
+              "total_tokens": 4000,
+              "total_cost": 0.05
+            }
+          }
+        }
       },
       },
       {"id": "3", "type": "normal", "description": "完善实现"}
       {"id": "3", "type": "normal", "description": "完善实现"}
     ]
     ]
@@ -1234,7 +1439,7 @@ Sub-Trace 完成后的压缩策略:
 | Trace 数据模型 | `agent/execution/models.py` | 待调整(增加父子关系、移除 branch_id) |
 | Trace 数据模型 | `agent/execution/models.py` | 待调整(增加父子关系、移除 branch_id) |
 | Goal 数据模型 | `agent/goal/models.py` | 待调整(移除 branch_id、BranchContext) |
 | Goal 数据模型 | `agent/goal/models.py` | 待调整(移除 branch_id、BranchContext) |
 | Trace ID 生成 | `agent/execution/trace_id.py` | 待实现(generate_sub_trace_id) |
 | Trace ID 生成 | `agent/execution/trace_id.py` | 待实现(generate_sub_trace_id) |
-| goal 工具 | `agent/goal/tool.py` | 待调整 |
+| goal 工具 | `agent/goal/tool.py` | 待调整(新增 after/under 参数) |
 | explore 工具 | `agent/goal/explore.py` | 待实现 |
 | explore 工具 | `agent/goal/explore.py` | 待实现 |
 | delegate 工具 | `agent/goal/delegate.py` | 待实现 |
 | delegate 工具 | `agent/goal/delegate.py` | 待实现 |
 | Context 压缩 | `agent/goal/compaction.py` | 待调整 |
 | Context 压缩 | `agent/goal/compaction.py` | 待调整 |
@@ -1250,8 +1455,9 @@ Sub-Trace 完成后的压缩策略:
 
 
 ### Phase 1: 基础 goal 工具
 ### Phase 1: 基础 goal 工具
 - GoalTree 数据结构(含 ID 映射)
 - GoalTree 数据结构(含 ID 映射)
-- goal 工具(add, done, focus)
-- Plan 注入到 system prompt(含显示序号生成)
+- goal 工具(add, after, under, done, focus)
+- 位置控制逻辑:after(同层级)、under(子任务)
+- Plan 注入到 system prompt(含显示序号生成、完整展示策略)
 - Message 模型(替代 Step)
 - Message 模型(替代 Step)
 
 
 ### Phase 2: 回溯支持
 ### Phase 2: 回溯支持

+ 330 - 2
frontend/API.md

@@ -101,7 +101,7 @@ GET /api/traces?status=running&limit=20
 GET /api/traces/{trace_id}
 GET /api/traces/{trace_id}
 ```
 ```
 
 
-**响应示例**(主 Trace):
+**响应示例 1**(主 Trace,explore 进行中):
 ```json
 ```json
 {
 {
   "trace_id": "abc123",
   "trace_id": "abc123",
@@ -140,6 +140,7 @@ GET /api/traces/{trace_id}
         "status": "in_progress",
         "status": "in_progress",
         "agent_call_mode": "explore",
         "agent_call_mode": "explore",
         "sub_trace_ids": ["abc123.A", "abc123.B"],
         "sub_trace_ids": ["abc123.A", "abc123.B"],
+        "sub_trace_metadata": null,
         "self_stats": { "message_count": 0, "total_tokens": 0, "total_cost": 0.0, "preview": 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 }
         "cumulative_stats": { "message_count": 0, "total_tokens": 0, "total_cost": 0.0, "preview": null }
       },
       },
@@ -182,6 +183,90 @@ GET /api/traces/{trace_id}
 }
 }
 ```
 ```
 
 
+**响应示例 2**(主 Trace,explore 已完成并合并):
+```json
+{
+  "trace_id": "abc123",
+  "mode": "agent",
+  "task": "实现用户认证功能",
+  "status": "running",
+  "goal_tree": {
+    "mission": "实现用户认证功能",
+    "current_id": "3",
+    "goals": [
+      {
+        "id": "1",
+        "type": "normal",
+        "description": "分析代码",
+        "status": "completed",
+        "summary": "用户模型在 models/user.py"
+      },
+      {
+        "id": "2",
+        "type": "agent_call",
+        "description": "并行探索认证方案",
+        "status": "completed",
+        "agent_call_mode": "explore",
+        "sub_trace_ids": [
+          "abc123@explore-20260204220012-001",
+          "abc123@explore-20260204220012-002"
+        ],
+        "sub_trace_metadata": {
+          "abc123@explore-20260204220012-001": {
+            "task": "JWT 方案",
+            "status": "completed",
+            "summary": "实现完成,使用 JWT token,无状态但 token 较大",
+            "last_message": {
+              "role": "assistant",
+              "description": "生成 JWT token 并返回",
+              "content": "JWT 方案实现完成。使用 jsonwebtoken 库生成 token,包含 user_id 和过期时间。优点:无状态,易扩展。缺点:token 较大(约 200 字节),无法主动失效。",
+              "created_at": "2026-02-05T10:30:00"
+            },
+            "stats": {
+              "message_count": 8,
+              "total_tokens": 4000,
+              "total_cost": 0.05
+            }
+          },
+          "abc123@explore-20260204220012-002": {
+            "task": "Session 方案",
+            "status": "completed",
+            "summary": "实现完成,使用 Redis 存储 session,支持主动失效",
+            "last_message": {
+              "role": "assistant",
+              "description": "配置 Redis 并实现 session 管理",
+              "content": "Session 方案实现完成。使用 Redis 存储 session,支持过期自动清理和主动失效。优点:token 小(约 32 字节),可主动失效。缺点:需要 Redis,有网络开销。",
+              "created_at": "2026-02-05T10:32:00"
+            },
+            "stats": {
+              "message_count": 12,
+              "total_tokens": 4000,
+              "total_cost": 0.05
+            }
+          }
+        },
+        "summary": "探索了 2 个方案,JWT 和 Session 均可行",
+        "cumulative_stats": {
+          "message_count": 20,
+          "total_tokens": 8000,
+          "total_cost": 0.10
+        }
+      },
+      {
+        "id": "3",
+        "type": "normal",
+        "description": "完善实现",
+        "status": "in_progress"
+      }
+    ]
+  },
+  "sub_traces": {
+    "abc123@explore-20260204220012-001": { /* ... */ },
+    "abc123@explore-20260204220012-002": { /* ... */ }
+  }
+}
+```
+
 ---
 ---
 
 
 ### 3. 获取 Sub-Trace 详情
 ### 3. 获取 Sub-Trace 详情
@@ -609,9 +694,36 @@ if (data.event === 'sub_trace_completed') {
 | `summary` | string \| null | 完成/放弃时的总结 |
 | `summary` | string \| null | 完成/放弃时的总结 |
 | `sub_trace_ids` | string[] \| null | 启动的 Sub-Trace IDs(仅 agent_call)|
 | `sub_trace_ids` | string[] \| null | 启动的 Sub-Trace IDs(仅 agent_call)|
 | `agent_call_mode` | string \| null | "explore" / "delegate" / "sequential"(仅 agent_call)|
 | `agent_call_mode` | string \| null | "explore" / "delegate" / "sequential"(仅 agent_call)|
+| `sub_trace_metadata` | object \| null | Sub-Trace 元数据(仅 agent_call,包含最后消息等)|
 | `self_stats` | GoalStats | 自身统计 |
 | `self_stats` | GoalStats | 自身统计 |
 | `cumulative_stats` | GoalStats | 累计统计 |
 | `cumulative_stats` | GoalStats | 累计统计 |
 
 
+**Goal 类型说明**:
+
+**`normal` Goal**:
+- 普通执行目标,对应一系列顺序执行的 Messages
+- 可以有子 Goal(通过 `parent_id` 构建层级)
+
+**`agent_call` Goal**:
+- 表示启动了 Sub-Agent 的特殊 Goal
+- 不直接关联 Messages,而是启动一个或多个独立的 Sub-Trace
+- 通过 `sub_trace_ids` 关联启动的所有 Sub-Traces
+- 通过 `agent_call_mode` 标记执行模式:
+  - `"explore"` - 并行探索:启动多个 Sub-Trace 并行执行,汇总结果后合并
+  - `"delegate"` - 单线委托:将大任务委托给单个 Sub-Trace 执行
+  - `"sequential"` - 顺序执行:按顺序启动多个 Sub-Trace
+
+**分支探索与合并(`explore` 模式)**:
+1. **分支开始**:`agent_call` Goal 同时启动多个 Sub-Trace(如 2-5 个)
+2. **并行执行**:各 Sub-Trace 独立运行,有各自的 GoalTree 和 Messages
+3. **收集元数据**:执行完成后,收集每个 Sub-Trace 的:
+   - 最后一条 assistant 消息(`last_message`)
+   - 执行总结(`summary`)
+   - 统计信息(`stats`)
+4. **合并节点**:`agent_call` Goal 完成时,表示所有分支已合并
+   - explore 工具返回文本摘要(给主 Agent 的 LLM 查看)
+   - `sub_trace_metadata` 保存详细元数据(给前端和后续决策使用)
+
 **ID 设计**:
 **ID 设计**:
 - 每个 Trace 内部独立编号("1", "2", "3")
 - 每个 Trace 内部独立编号("1", "2", "3")
 - 层级关系通过 `parent_id` 维护
 - 层级关系通过 `parent_id` 维护
@@ -619,6 +731,70 @@ if (data.event === 'sub_trace_completed') {
 
 
 ---
 ---
 
 
+### SubTraceMetadata(agent_call Goal 专用)
+
+`agent_call` 类型的 Goal 会在完成时填充 `sub_trace_metadata` 字段,格式如下:
+
+```json
+{
+  "sub_trace_metadata": {
+    "abc123@explore-20260204220012-001": {
+      "task": "JWT 方案",
+      "status": "completed",
+      "summary": "实现完成,使用 JWT token",
+      "last_message": {
+        "role": "assistant",
+        "description": "生成 JWT token 并返回",
+        "content": "JWT 方案实现完成。使用 jsonwebtoken 库生成 token...",
+        "created_at": "2026-02-05T10:30:00"
+      },
+      "stats": {
+        "message_count": 8,
+        "total_tokens": 4000,
+        "total_cost": 0.05
+      }
+    },
+    "abc123@explore-20260204220012-002": {
+      "task": "Session 方案",
+      "status": "completed",
+      "summary": "实现完成,使用 Redis 存储",
+      "last_message": {
+        "role": "assistant",
+        "description": "配置 Redis 并实现 session 管理",
+        "content": "Session 方案实现完成。使用 Redis 存储 session...",
+        "created_at": "2026-02-05T10:32:00"
+      },
+      "stats": {
+        "message_count": 12,
+        "total_tokens": 4000,
+        "total_cost": 0.05
+      }
+    }
+  }
+}
+```
+
+**字段说明**:
+- `task` - Sub-Trace 的任务描述
+- `status` - Sub-Trace 的最终状态(completed/failed)
+- `summary` - Sub-Trace 的执行总结
+  - 优先使用 `run_agent()` 返回的 `summary` 字段(如果有)
+  - 否则使用最后一条 assistant 消息的内容(截断至 200 字符)
+  - 如果都没有,默认为 "执行完成"
+- `last_message` - 最后一条 assistant 消息(内容截断至 500 字符)
+  - `role` - 固定为 "assistant"
+  - `description` - 消息的简短描述
+  - `content` - 消息内容(可能被截断)
+  - `created_at` - 消息创建时间
+- `stats` - 统计信息(message_count, total_tokens, total_cost)
+
+**用途**:
+1. **辅助决策**:主 Agent 可以基于各分支的最终输出,决定是否需要展开查看详细信息
+2. **前端展示**:在折叠视图中显示每个分支的关键信息,用户快速判断是否需要展开
+3. **调试追踪**:快速了解每个分支的执行结果,无需加载完整 Messages
+
+---
+
 ### GoalStats
 ### GoalStats
 
 
 | 字段 | 类型 | 说明 |
 | 字段 | 类型 | 说明 |
@@ -638,7 +814,7 @@ if (data.event === 'sub_trace_completed') {
 | `trace_id` | string | 所属 Trace ID(可能是主 Trace 或 Sub-Trace)|
 | `trace_id` | string | 所属 Trace ID(可能是主 Trace 或 Sub-Trace)|
 | `role` | string | `assistant` / `tool` |
 | `role` | string | `assistant` / `tool` |
 | `sequence` | int | 当前 Trace 内的顺序 |
 | `sequence` | int | 当前 Trace 内的顺序 |
-| `goal_id` | string | 关联的 Goal 内部 ID(如 "1", "2")|
+| `goal_id` | string \| null | 关联的 Goal 内部 ID(如 "1", "2")。**可以为 `null`**(初始阶段)|
 | `tool_call_id` | string \| null | tool 消息关联的 tool_call ID |
 | `tool_call_id` | string \| null | tool 消息关联的 tool_call ID |
 | `content` | any | 消息内容(和 LLM API 格式一致)|
 | `content` | any | 消息内容(和 LLM API 格式一致)|
 | `description` | string | 消息描述(系统自动生成)|
 | `description` | string | 消息描述(系统自动生成)|
@@ -646,6 +822,158 @@ if (data.event === 'sub_trace_completed') {
 | `cost` | float \| null | 成本 |
 | `cost` | float \| null | 成本 |
 | `created_at` | string | 创建时间 |
 | `created_at` | string | 创建时间 |
 
 
+**goal_id 说明**:
+- 通常关联到某个 Goal 的内部 ID(如 "1", "2")
+- **可以为 `null`**:在 Trace 开始时,Agent 还没有创建任何 Goal
+- 初始阶段的 messages 用于分析任务、创建第一批 Goals
+
+**查询初始阶段 Messages**:
+```http
+GET /api/traces/{trace_id}/messages?goal_id=_init
+```
+- 使用特殊值 `_init` 或 `null` 查询 goal_id 为 null 的 messages
+- 用于前端展示 START 节点的详情
+
+---
+
+## 前端展示:START 虚拟节点
+
+**设计原则**:诚实反映数据结构,不在后端创建额外节点。
+
+### 概念
+
+在 Trace 开始阶段,Agent 还没有创建任何 Goal,这期间产生的 Messages 的 `goal_id` 为 `null`。前端应该创建一个虚拟的 **START 节点**来展示这些初始阶段的 messages。
+
+### DAG 结构
+
+```
+[START] ────────→ [1:分析完成] ──→ [2:设计完成] ──→ [3:实现完成]
+   │
+   └─ goal_id=null 的 messages
+      (分析任务、创建计划等)
+```
+
+**START 节点**:
+- 类型:虚拟节点(前端创建,后端不存在)
+- ID:`"START"` 或 `null`
+- 数据:Mission 信息、初始阶段统计
+
+**START → Goal 1 的边**:
+- source: `"START"`
+- target: 第一个真实 Goal 的 ID
+- 数据:goal_id=null 的所有 messages 的聚合统计
+
+### 实现示例
+
+```javascript
+// 1. 创建 START 节点
+function buildDAG(goalTree, messages) {
+  const nodes = []
+  const edges = []
+
+  // 创建虚拟 START 节点
+  nodes.push({
+    id: 'START',
+    type: 'start',
+    data: {
+      label: 'START',
+      mission: goalTree.mission,
+    }
+  })
+
+  // 计算初始阶段的统计
+  const initMessages = messages.filter(m => m.goal_id === null)
+  const initStats = {
+    message_count: initMessages.length,
+    total_tokens: initMessages.reduce((sum, m) => sum + (m.tokens || 0), 0),
+    total_cost: initMessages.reduce((sum, m) => sum + (m.cost || 0), 0),
+    preview: generatePreview(initMessages)
+  }
+
+  // 创建 START 到第一个 Goal 的边
+  const firstGoal = goalTree.goals.find(g => g.parent_id === null)
+  if (firstGoal) {
+    edges.push({
+      source: 'START',
+      target: firstGoal.id,
+      data: {
+        messages: initMessages,
+        stats: initStats
+      }
+    })
+  }
+
+  // 创建其他 goals 的节点和边...
+  // ...
+
+  return { nodes, edges }
+}
+```
+
+### 用户交互
+
+**悬停 START 节点**:
+```
+┌─────────────────────────────┐
+│ START                       │
+│ Mission: 实现用户认证系统   │
+│                             │
+│ 初始阶段统计:              │
+│ • 2 messages               │
+│ • 500 tokens               │
+│ • $0.005                   │
+└─────────────────────────────┘
+```
+
+**点击 START 节点**:
+- 侧边栏显示 Mission 描述
+- 显示初始阶段的元信息
+
+**点击 START → Goal 1 边**:
+- 展开详情,显示所有 goal_id=null 的 messages
+- 内容示例:
+  ```
+  [assistant] 我需要先分析这个任务...
+  [tool: goal] 创建了3个顶层目标
+  ```
+
+**查询 API**:
+```javascript
+// 获取初始阶段的 messages
+async function getInitMessages(traceId) {
+  const resp = await fetch(`/api/traces/${traceId}/messages?goal_id=_init`)
+  const data = await resp.json()
+  return data.messages  // goal_id 全部为 null
+}
+```
+
+### 特殊情况
+
+**情况 1:没有初始阶段 messages**
+- 如果 Agent 一启动就创建了 Goal(没有 goal_id=null 的 messages)
+- 仍然显示 START 节点,但边的统计为空
+
+**情况 2:所有 messages 都是初始阶段**
+- 如果 Agent 还没有创建任何 Goal
+- 只显示 START 节点,没有其他节点
+
+```
+[START]
+  (分析中,3 msgs)
+```
+
+### 与编辑功能的集成
+
+用户可以在 START → Goal 1 的边上编辑第一次 goal() 调用的参数:
+
+1. 点击 START → 1 边,展开 messages
+2. 找到 goal() 工具调用(通常是第一个或第二个 message)
+3. 点击 [编辑] 按钮
+4. 修改 add 参数(如修改初始的任务列表)
+5. 系统废弃 Goal 1, 2, 3...,重新执行
+
+这样用户就能方便地修改整体计划。
+
 ---
 ---
 
 
 ## DAG 可视化(前端实现)
 ## DAG 可视化(前端实现)