فهرست منبع

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

max_liu 3 هفته پیش
والد
کامیت
c548b52c5f
9فایلهای تغییر یافته به همراه190 افزوده شده و 102 حذف شده
  1. 1 1
      README.md
  2. 34 27
      agent/core/runner.py
  3. 23 12
      agent/trace/api.py
  4. 3 2
      agent/trace/protocols.py
  5. 27 25
      agent/trace/run_api.py
  6. 4 6
      agent/trace/store.py
  7. 22 19
      docs/README.md
  8. 57 0
      docs/decisions.md
  9. 19 10
      docs/trace-api.md

+ 1 - 1
README.md

@@ -150,7 +150,7 @@ RunConfig(
     system_prompt=None,       # None=从 skills 自动构建
     system_prompt=None,       # None=从 skills 自动构建
     agent_type="default",     # 预设类型:default / explore / analyst
     agent_type="default",     # 预设类型:default / explore / analyst
     trace_id=None,            # 续跑/回溯时传入已有 trace ID
     trace_id=None,            # 续跑/回溯时传入已有 trace ID
-    insert_after=None,        # 回溯插入点(message sequence)
+    after_sequence=None,      # 从哪条消息后续跑(message sequence)
 )
 )
 ```
 ```
 
 

+ 34 - 27
agent/core/runner.py

@@ -10,7 +10,7 @@ Agent Runner - Agent 执行引擎
 
 
 参数分层:
 参数分层:
 - Infrastructure: AgentRunner 构造时设置(trace_store, llm_call 等)
 - Infrastructure: AgentRunner 构造时设置(trace_store, llm_call 等)
-- RunConfig: 每次 run 时指定(model, trace_id, insert_after 等)
+- RunConfig: 每次 run 时指定(model, trace_id, after_sequence 等)
 - Messages: OpenAI SDK 格式的任务消息
 - Messages: OpenAI SDK 格式的任务消息
 """
 """
 
 
@@ -63,7 +63,7 @@ class RunConfig:
     parent_goal_id: Optional[str] = None
     parent_goal_id: Optional[str] = None
 
 
     # --- 续跑控制 ---
     # --- 续跑控制 ---
-    insert_after: Optional[int] = None         # 回溯插入点(message sequence)
+    after_sequence: Optional[int] = None       # 从哪条消息后续跑(message sequence)
 
 
     # --- 额外 LLM 参数(传给 llm_call 的 **kwargs)---
     # --- 额外 LLM 参数(传给 llm_call 的 **kwargs)---
     extra_llm_params: Dict[str, Any] = field(default_factory=dict)
     extra_llm_params: Dict[str, Any] = field(default_factory=dict)
@@ -161,8 +161,8 @@ class AgentRunner:
 
 
     支持三种运行模式(通过 RunConfig 区分):
     支持三种运行模式(通过 RunConfig 区分):
     1. 新建:trace_id=None
     1. 新建:trace_id=None
-    2. 续跑:trace_id=已有ID, insert_after=None
-    3. 回溯:trace_id=已有ID, insert_after=N
+    2. 续跑:trace_id=已有ID, after_sequence=None 或 == head
+    3. 回溯:trace_id=已有ID, after_sequence=N(N < head_sequence)
     """
     """
 
 
     def __init__(
     def __init__(
@@ -456,9 +456,11 @@ class AgentRunner:
 
 
         goal_tree = await self.trace_store.get_goal_tree(config.trace_id)
         goal_tree = await self.trace_store.get_goal_tree(config.trace_id)
 
 
-        if config.insert_after is not None:
+        # 自动判断行为:after_sequence 为 None 或 == head → 续跑;< head → 回溯
+        after_seq = config.after_sequence
+        if after_seq is not None and after_seq < trace_obj.head_sequence:
             # 回溯模式
             # 回溯模式
-            sequence = await self._rewind(config.trace_id, config.insert_after, goal_tree)
+            sequence = await self._rewind(config.trace_id, after_seq, goal_tree)
         else:
         else:
             # 续跑模式:从 last_sequence + 1 开始
             # 续跑模式:从 last_sequence + 1 开始
             sequence = trace_obj.last_sequence + 1
             sequence = trace_obj.last_sequence + 1
@@ -775,7 +777,7 @@ class AgentRunner:
     async def _rewind(
     async def _rewind(
         self,
         self,
         trace_id: str,
         trace_id: str,
-        insert_after: int,
+        after_sequence: int,
         goal_tree: Optional[GoalTree],
         goal_tree: Optional[GoalTree],
     ) -> int:
     ) -> int:
         """
         """
@@ -789,33 +791,33 @@ class AgentRunner:
         if not self.trace_store:
         if not self.trace_store:
             raise ValueError("trace_store required for rewind")
             raise ValueError("trace_store required for rewind")
 
 
-        # 1. 加载所有 messages
-        all_messages = await self.trace_store.get_trace_messages(
-            trace_id, include_abandoned=True
-        )
+        # 1. 加载所有 messages(用于 safe cutoff 和 max sequence)
+        all_messages = await self.trace_store.get_trace_messages(trace_id)
 
 
         if not all_messages:
         if not all_messages:
             return 1
             return 1
 
 
         # 2. 找到安全截断点(确保不截断在 tool_call 和 tool response 之间)
         # 2. 找到安全截断点(确保不截断在 tool_call 和 tool response 之间)
-        cutoff = self._find_safe_cutoff(all_messages, insert_after)
+        cutoff = self._find_safe_cutoff(all_messages, after_sequence)
 
 
         # 3. 快照并重建 GoalTree
         # 3. 快照并重建 GoalTree
         if goal_tree:
         if goal_tree:
-            # 找出 rewind 点之前已完成的 goal IDs
-            # 通过主路径消息来判断:cutoff 之前的消息引用的 completed goals
-            messages_before = [m for m in all_messages if m.sequence <= cutoff]
+            # 通过主路径消息来判断:从 cutoff 沿 parent_sequence 回溯,只检查实际在主路径上的消息
+            main_path_before = await self.trace_store.get_main_path_messages(
+                trace_id, cutoff
+            )
             completed_goal_ids = set()
             completed_goal_ids = set()
             for goal in goal_tree.goals:
             for goal in goal_tree.goals:
                 if goal.status == "completed":
                 if goal.status == "completed":
-                    # 检查该 goal 是否在 rewind 点之前就已完成(有关联消息在 cutoff 之前
-                    goal_msgs = [m for m in messages_before if m.goal_id == goal.id]
+                    # 检查该 goal 是否在主路径上有关联消息(即确实在 rewind 点之前就存在
+                    goal_msgs = [m for m in main_path_before if m.goal_id == goal.id]
                     if goal_msgs:
                     if goal_msgs:
                         completed_goal_ids.add(goal.id)
                         completed_goal_ids.add(goal.id)
 
 
-            # 快照到 events
+            # 快照到 events(含 head_sequence 供前端感知分支切换)
             await self.trace_store.append_event(trace_id, "rewind", {
             await self.trace_store.append_event(trace_id, "rewind", {
-                "insert_after_sequence": cutoff,
+                "after_sequence": cutoff,
+                "head_sequence": cutoff,
                 "goal_tree_snapshot": goal_tree.to_dict(),
                 "goal_tree_snapshot": goal_tree.to_dict(),
             })
             })
 
 
@@ -834,19 +836,19 @@ class AgentRunner:
         max_seq = max((m.sequence for m in all_messages), default=0)
         max_seq = max((m.sequence for m in all_messages), default=0)
         return max_seq + 1
         return max_seq + 1
 
 
-    def _find_safe_cutoff(self, messages: List[Message], insert_after: int) -> int:
+    def _find_safe_cutoff(self, messages: List[Message], after_sequence: int) -> int:
         """
         """
         找到安全的截断点。
         找到安全的截断点。
 
 
-        如果 insert_after 指向一条带 tool_calls 的 assistant message,
+        如果 after_sequence 指向一条带 tool_calls 的 assistant message,
         则自动扩展到其所有对应的 tool response 之后。
         则自动扩展到其所有对应的 tool response 之后。
         """
         """
-        cutoff = insert_after
+        cutoff = after_sequence
 
 
-        # 找到 insert_after 对应的 message
+        # 找到 after_sequence 对应的 message
         target_msg = None
         target_msg = None
         for msg in messages:
         for msg in messages:
-            if msg.sequence == insert_after:
+            if msg.sequence == after_sequence:
                 target_msg = msg
                 target_msg = msg
                 break
                 break
 
 
@@ -918,6 +920,9 @@ class AgentRunner:
                     tool_names.append(t)
                     tool_names.append(t)
         return self.tools.get_schemas(tool_names)
         return self.tools.get_schemas(tool_names)
 
 
+    # 默认 system prompt 前缀(当 config.system_prompt 和前端都未提供 system message 时使用)
+    DEFAULT_SYSTEM_PREFIX = "你是最顶尖的AI助手,可以拆分并调用工具逐步解决复杂问题。"
+
     async def _build_system_prompt(self, config: RunConfig) -> Optional[str]:
     async def _build_system_prompt(self, config: RunConfig) -> Optional[str]:
         """构建 system prompt(注入 skills)"""
         """构建 system prompt(注入 skills)"""
         system_prompt = config.system_prompt
         system_prompt = config.system_prompt
@@ -928,12 +933,14 @@ class AgentRunner:
         if skills:
         if skills:
             skills_text = self._format_skills(skills)
             skills_text = self._format_skills(skills)
 
 
-        # 拼装
+        # 拼装:有自定义 system_prompt 则用它,否则用默认前缀
         if system_prompt:
         if system_prompt:
             if skills_text:
             if skills_text:
                 system_prompt += f"\n\n## Skills\n{skills_text}"
                 system_prompt += f"\n\n## Skills\n{skills_text}"
-        elif skills_text:
-            system_prompt = f"## Skills\n{skills_text}"
+        else:
+            system_prompt = self.DEFAULT_SYSTEM_PREFIX
+            if skills_text:
+                system_prompt += f"\n\n## Skills\n{skills_text}"
 
 
         return system_prompt
         return system_prompt
 
 

+ 23 - 12
agent/trace/api.py

@@ -124,19 +124,23 @@ 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,
+    mode: str = Query("main_path", description="查询模式:main_path(当前主路径消息)或 all(全部消息含所有分支)"),
+    head: Optional[int] = Query(None, description="主路径的 head sequence(仅 mode=main_path 有效,默认用 trace.head_sequence)"),
     goal_id: Optional[str] = Query(None, description="过滤指定 Goal 的消息。使用 '_init' 查询初始阶段(goal_id=None)的消息"),
     goal_id: Optional[str] = Query(None, description="过滤指定 Goal 的消息。使用 '_init' 查询初始阶段(goal_id=None)的消息"),
-    include_abandoned: bool = Query(False, description="是否包含已 abandoned 的消息(回溯后被标记的)"),
 ):
 ):
     """
     """
     获取 Messages
     获取 Messages
 
 
     Args:
     Args:
         trace_id: Trace ID
         trace_id: Trace ID
+        mode: 查询模式
+              - "main_path"(默认): 从 head 沿 parent_sequence 链回溯的主路径消息
+              - "all": 返回所有消息(包含所有分支)
+        head: 可选,指定主路径的 head sequence(仅 mode=main_path 有效)
         goal_id: 可选,过滤指定 Goal 的消息
         goal_id: 可选,过滤指定 Goal 的消息
-                - 不指定: 返回所有消息
+                - 不指定: 不按 goal 过滤
                 - "_init" 或 "null": 返回初始阶段(goal_id=None)的消息
                 - "_init" 或 "null": 返回初始阶段(goal_id=None)的消息
                 - 其他值: 返回指定 Goal 的消息
                 - 其他值: 返回指定 Goal 的消息
-        include_abandoned: 是否包含已 abandoned 的消息(默认 False)
     """
     """
     store = get_trace_store()
     store = get_trace_store()
 
 
@@ -146,16 +150,23 @@ 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 is None:
-        # 没有指定 goal_id,返回所有消息
-        messages = await store.get_trace_messages(trace_id, include_abandoned=include_abandoned)
-    elif goal_id in ("_init", "null"):
-        # 特殊值:查询初始阶段的消息(goal_id=None)
-        all_messages = await store.get_trace_messages(trace_id, include_abandoned=include_abandoned)
-        messages = [m for m in all_messages if m.goal_id is None]
-    else:
-        # 查询指定 Goal 的消息
+    if goal_id and goal_id not in ("_init", "null"):
+        # 按 Goal 过滤(独立查询)
         messages = await store.get_messages_by_goal(trace_id, goal_id)
         messages = await store.get_messages_by_goal(trace_id, goal_id)
+    elif mode == "main_path":
+        # 主路径模式
+        head_seq = head if head is not None else trace.head_sequence
+        if head_seq > 0:
+            messages = await store.get_main_path_messages(trace_id, head_seq)
+        else:
+            messages = []
+    else:
+        # all 模式:返回所有消息
+        messages = await store.get_trace_messages(trace_id)
+
+    # goal_id 过滤(_init 表示 goal_id=None 的消息)
+    if goal_id in ("_init", "null"):
+        messages = [m for m in messages if m.goal_id is None]
 
 
     return MessagesResponse(
     return MessagesResponse(
         messages=[m.to_dict() for m in messages]
         messages=[m.to_dict() for m in messages]

+ 3 - 2
agent/trace/protocols.py

@@ -121,14 +121,15 @@ class TraceStore(Protocol):
     async def get_trace_messages(
     async def get_trace_messages(
         self,
         self,
         trace_id: str,
         trace_id: str,
-        include_abandoned: bool = False
     ) -> List[Message]:
     ) -> List[Message]:
         """
         """
         获取 Trace 的所有 Messages(按 sequence 排序)
         获取 Trace 的所有 Messages(按 sequence 排序)
 
 
+        返回该 Trace 下所有消息(包含所有分支)。
+        如需获取特定主路径的消息,使用 get_main_path_messages()。
+
         Args:
         Args:
             trace_id: Trace ID
             trace_id: Trace ID
-            include_abandoned: 是否包含已 abandoned 的消息(默认 False,仅返回 active)
 
 
         Returns:
         Returns:
             Message 列表
             Message 列表

+ 27 - 25
agent/trace/run_api.py

@@ -55,11 +55,13 @@ def _get_runner():
 
 
 class CreateRequest(BaseModel):
 class CreateRequest(BaseModel):
     """新建执行"""
     """新建执行"""
-    messages: List[Dict[str, Any]] = Field(..., description="OpenAI SDK 格式的输入消息")
+    messages: List[Dict[str, Any]] = Field(
+        ...,
+        description="OpenAI SDK 格式的输入消息。可包含 system + user 消息;若无 system 消息则从 skills 自动构建",
+    )
     model: str = Field("gpt-4o", description="模型名称")
     model: str = Field("gpt-4o", description="模型名称")
     temperature: float = Field(0.3)
     temperature: float = Field(0.3)
     max_iterations: int = Field(200)
     max_iterations: int = Field(200)
-    system_prompt: Optional[str] = Field(None, description="自定义 system prompt(None = 从 skills 自动构建)")
     tools: Optional[List[str]] = Field(None, description="工具白名单(None = 全部)")
     tools: Optional[List[str]] = Field(None, description="工具白名单(None = 全部)")
     name: Optional[str] = Field(None, description="任务名称(None = 自动生成)")
     name: Optional[str] = Field(None, description="任务名称(None = 自动生成)")
     uid: Optional[str] = Field(None)
     uid: Optional[str] = Field(None)
@@ -71,9 +73,9 @@ class TraceRunRequest(BaseModel):
         default_factory=list,
         default_factory=list,
         description="追加的新消息(可为空,用于重新生成场景)",
         description="追加的新消息(可为空,用于重新生成场景)",
     )
     )
-    insert_after: Optional[int] = Field(
+    after_sequence: Optional[int] = Field(
         None,
         None,
-        description="回溯插入点的 message sequence。None = 从末尾续跑,int = 回溯到该 sequence 后运行",
+        description="从哪条消息后续跑。None = 从末尾续跑,int = 从该 sequence 后运行(自动判断续跑/回溯)",
     )
     )
 
 
 
 
@@ -159,7 +161,6 @@ async def create_and_run(req: CreateRequest):
         model=req.model,
         model=req.model,
         temperature=req.temperature,
         temperature=req.temperature,
         max_iterations=req.max_iterations,
         max_iterations=req.max_iterations,
-        system_prompt=req.system_prompt,
         tools=req.tools,
         tools=req.tools,
         name=req.name,
         name=req.name,
         uid=req.uid,
         uid=req.uid,
@@ -186,11 +187,11 @@ async def run_trace(trace_id: str, req: TraceRunRequest):
     """
     """
     运行已有 Trace(统一续跑 + 回溯)
     运行已有 Trace(统一续跑 + 回溯)
 
 
-    - insert_after 为 null(或省略):从末尾续跑
-    - insert_after 为 int:回溯到该 sequence 后运行
-    - messages 为空 + insert_after 为 int:重新生成(从该位置重跑,不插入新消息)
+    - after_sequence 为 null(或省略):从末尾续跑
+    - after_sequence 为 int:从该 sequence 后运行(Runner 自动判断续跑/回溯)
+    - messages 为空 + after_sequence 为 int:重新生成(从该位置重跑,不插入新消息)
 
 
-    insert_after 的值是 message 的 sequence 号。如果指定的 sequence 是一条带
+    after_sequence 的值是 message 的 sequence 号。如果指定的 sequence 是一条带
     tool_calls 的 assistant 消息,系统会自动扩展截断点到其所有 tool response 之后。
     tool_calls 的 assistant 消息,系统会自动扩展截断点到其所有 tool response 之后。
     """
     """
     from agent.core.runner import RunConfig
     from agent.core.runner import RunConfig
@@ -207,11 +208,11 @@ async def run_trace(trace_id: str, req: TraceRunRequest):
     if trace_id in _running_tasks and not _running_tasks[trace_id].done():
     if trace_id in _running_tasks and not _running_tasks[trace_id].done():
         raise HTTPException(status_code=409, detail="Trace is already running")
         raise HTTPException(status_code=409, detail="Trace is already running")
 
 
-    config = RunConfig(trace_id=trace_id, insert_after=req.insert_after)
+    config = RunConfig(trace_id=trace_id, after_sequence=req.after_sequence)
     task = asyncio.create_task(_run_in_background(trace_id, req.messages, config))
     task = asyncio.create_task(_run_in_background(trace_id, req.messages, config))
     _running_tasks[trace_id] = task
     _running_tasks[trace_id] = task
 
 
-    mode = "rewind" if req.insert_after is not None else "continue"
+    mode = "rewind" if req.after_sequence is not None else "continue"
     return RunResponse(
     return RunResponse(
         trace_id=trace_id,
         trace_id=trace_id,
         status="started",
         status="started",
@@ -250,11 +251,11 @@ async def reflect_trace(trace_id: str, req: ReflectRequest):
     """
     """
     触发反思
     触发反思
 
 
-    在 trace 末尾追加一条包含反思 prompt 的 user message,运行 agent 获取反思结果,
+    在 trace 末尾追加一条包含反思 prompt 的 user message,单轮无工具 LLM 调用获取反思结果,
     将结果追加到 experiences 文件(默认 ./cache/experiences.md)。
     将结果追加到 experiences 文件(默认 ./cache/experiences.md)。
 
 
-    反思消息作为侧枝(side branch):运行前保存 head_sequence,运行后恢复。
-    这样反思消息不会出现在主对话路径上
+    反思消息作为侧枝(side branch):运行前保存 head_sequence,运行后恢复(try/finally 保证)
+    使用 max_iterations=1, tools=[] 确保反思不会产生副作用
     """
     """
     from agent.core.runner import RunConfig
     from agent.core.runner import RunConfig
     from agent.trace.compaction import build_reflect_prompt
     from agent.trace.compaction import build_reflect_prompt
@@ -281,17 +282,18 @@ async def reflect_trace(trace_id: str, req: ReflectRequest):
     if req.focus:
     if req.focus:
         prompt += f"\n\n请特别关注:{req.focus}"
         prompt += f"\n\n请特别关注:{req.focus}"
 
 
-    # 以续跑方式运行:追加 user message,agent 回复反思内容
-    config = RunConfig(trace_id=trace_id)
-    result = await runner.run_result(
-        messages=[{"role": "user", "content": prompt}],
-        config=config,
-    )
-
-    reflection_text = result.get("summary", "")
-
-    # 恢复 head_sequence(反思消息成为侧枝,不影响主路径)
-    await runner.trace_store.update_trace(trace_id, head_sequence=saved_head_sequence)
+    # 以续跑方式运行:单轮无工具 LLM 调用
+    config = RunConfig(trace_id=trace_id, max_iterations=1, tools=[])
+    reflection_text = ""
+    try:
+        result = await runner.run_result(
+            messages=[{"role": "user", "content": prompt}],
+            config=config,
+        )
+        reflection_text = result.get("summary", "")
+    finally:
+        # 恢复 head_sequence(反思消息成为侧枝,不影响主路径)
+        await runner.trace_store.update_trace(trace_id, head_sequence=saved_head_sequence)
 
 
     # 追加到 experiences 文件
     # 追加到 experiences 文件
     if reflection_text:
     if reflection_text:

+ 4 - 6
agent/trace/store.py

@@ -469,9 +469,8 @@ class FileSystemTraceStore:
     async def get_trace_messages(
     async def get_trace_messages(
         self,
         self,
         trace_id: str,
         trace_id: str,
-        include_abandoned: bool = False
     ) -> List[Message]:
     ) -> List[Message]:
-        """获取 Trace 的 Messages(默认只返回 active 的)"""
+        """获取 Trace 的所有 Messages(包含所有分支,按 sequence 排序)"""
         messages_dir = self._get_messages_dir(trace_id)
         messages_dir = self._get_messages_dir(trace_id)
 
 
         if not messages_dir.exists():
         if not messages_dir.exists():
@@ -482,8 +481,7 @@ class FileSystemTraceStore:
             try:
             try:
                 data = json.loads(message_file.read_text())
                 data = json.loads(message_file.read_text())
                 msg = Message.from_dict(data)
                 msg = Message.from_dict(data)
-                if include_abandoned or msg.status == "active":
-                    messages.append(msg)
+                messages.append(msg)
             except Exception:
             except Exception:
                 continue
                 continue
 
 
@@ -503,7 +501,7 @@ class FileSystemTraceStore:
             按 sequence 正序排列的主路径 Message 列表
             按 sequence 正序排列的主路径 Message 列表
         """
         """
         # 加载所有消息,建立 sequence -> Message 索引
         # 加载所有消息,建立 sequence -> Message 索引
-        all_messages = await self.get_trace_messages(trace_id, include_abandoned=True)
+        all_messages = await self.get_trace_messages(trace_id)
         messages_by_seq = {m.sequence: m for m in all_messages}
         messages_by_seq = {m.sequence: m for m in all_messages}
 
 
         # 从 head 沿 parent chain 回溯
         # 从 head 沿 parent chain 回溯
@@ -550,7 +548,7 @@ class FileSystemTraceStore:
         将 sequence > cutoff_sequence 的 active messages 标记为 abandoned。
         将 sequence > cutoff_sequence 的 active messages 标记为 abandoned。
         返回被 abandon 的 message_id 列表。
         返回被 abandon 的 message_id 列表。
         """
         """
-        all_messages = await self.get_trace_messages(trace_id, include_abandoned=True)
+        all_messages = await self.get_trace_messages(trace_id)
         abandoned_ids = []
         abandoned_ids = []
         now = datetime.now()
         now = datetime.now()
 
 

+ 22 - 19
docs/README.md

@@ -147,7 +147,7 @@ class RunConfig:
     parent_goal_id: Optional[str] = None
     parent_goal_id: Optional[str] = None
 
 
     # 续跑控制
     # 续跑控制
-    insert_after: Optional[int] = None         # 回溯插入点(message sequence)
+    after_sequence: Optional[int] = None       # 从哪条消息后续跑(message sequence)
 ```
 ```
 
 
 **实现**:`agent/core/runner.py:RunConfig`
 **实现**:`agent/core/runner.py:RunConfig`
@@ -156,11 +156,13 @@ class RunConfig:
 
 
 通过 RunConfig 参数自然区分,统一入口 `run(messages, config)`:
 通过 RunConfig 参数自然区分,统一入口 `run(messages, config)`:
 
 
-| 模式 | trace_id | insert_after | messages 含义 | API 端点 |
-|------|----------|-------------|--------------|----------|
+| 模式 | trace_id | after_sequence | messages 含义 | API 端点 |
+|------|----------|---------------|--------------|----------|
 | 新建 | None | - | 初始任务消息 | `POST /api/traces` |
 | 新建 | None | - | 初始任务消息 | `POST /api/traces` |
-| 续跑 | 已有 ID | None | 追加到末尾的新消息 | `POST /api/traces/{id}/run` |
-| 回溯 | 已有 ID | 指定 sequence | 在插入点之后追加的新消息 | `POST /api/traces/{id}/run` |
+| 续跑 | 已有 ID | None 或 == head | 追加到末尾的新消息 | `POST /api/traces/{id}/run` |
+| 回溯 | 已有 ID | 主路径上 < head | 在插入点之后追加的新消息 | `POST /api/traces/{id}/run` |
+
+Runner 根据 `after_sequence` 与当前 `head_sequence` 的关系自动判断行为,前端无需指定模式。
 
 
 ### 执行流程
 ### 执行流程
 
 
@@ -168,8 +170,8 @@ class RunConfig:
 async def run(messages: List[Dict], config: RunConfig = None) -> AsyncIterator[Union[Trace, Message]]:
 async def run(messages: List[Dict], config: RunConfig = None) -> AsyncIterator[Union[Trace, Message]]:
     # Phase 1: PREPARE TRACE
     # Phase 1: PREPARE TRACE
     #   无 trace_id → 创建新 Trace(生成 name,初始化 GoalTree)
     #   无 trace_id → 创建新 Trace(生成 name,初始化 GoalTree)
-    #   有 trace_id + 无 insert_after → 加载已有 Trace,状态置为 running
-    #   有 trace_id + 有 insert_after → 加载 Trace,执行 rewind(快照 GoalTree,重建,设 parent_sequence)
+    #   有 trace_id + after_sequence 为 None 或 == head → 加载已有 Trace,状态置为 running
+    #   有 trace_id + after_sequence < head → 加载 Trace,执行 rewind(快照 GoalTree,重建,设 parent_sequence)
     trace = await _prepare_trace(config)
     trace = await _prepare_trace(config)
     yield trace
     yield trace
 
 
@@ -202,11 +204,11 @@ async def run(messages: List[Dict], config: RunConfig = None) -> AsyncIterator[U
 
 
 ### 回溯(Rewind)
 ### 回溯(Rewind)
 
 
-回溯通过 `RunConfig(trace_id=..., insert_after=N)` 触发,在 Phase 1 中执行:
+回溯通过 `RunConfig(trace_id=..., after_sequence=N)` 触发(N 在主路径上且 < head_sequence),在 Phase 1 中执行:
 
 
 1. **验证插入点**:确保不截断在 assistant(tool_calls) 和 tool response 之间
 1. **验证插入点**:确保不截断在 assistant(tool_calls) 和 tool response 之间
 2. **快照 GoalTree**:将当前完整 GoalTree 存入 `events.jsonl`(rewind 事件的 `goal_tree_snapshot` 字段)
 2. **快照 GoalTree**:将当前完整 GoalTree 存入 `events.jsonl`(rewind 事件的 `goal_tree_snapshot` 字段)
-3. **重建 GoalTree**:保留 rewind 点之前已 completed 的 goals,丢弃其余,清空 `current_id`
+3. **重建 GoalTree**:保留 rewind 点之前已 completed 的 goals(仅检查主路径上的消息),丢弃其余,清空 `current_id`
 4. **设置 parent_sequence**:新消息的 `parent_sequence` 指向 rewind 点,旧消息自动脱离主路径
 4. **设置 parent_sequence**:新消息的 `parent_sequence` 指向 rewind 点,旧消息自动脱离主路径
 5. **更新 Trace**:`head_sequence` 更新为新消息的 sequence,status 改回 running
 5. **更新 Trace**:`head_sequence` 更新为新消息的 sequence,status 改回 running
 
 
@@ -232,22 +234,22 @@ async for item in runner.run(
     ...
     ...
 
 
 # 回溯:从指定 sequence 处切断,插入新消息重新执行
 # 回溯:从指定 sequence 处切断,插入新消息重新执行
-# insert_after=5 表示保留 sequence ≤ 5 的消息,abandon 之后的,从此处开始
+# after_sequence=5 表示新消息的 parent_sequence=5,从此处开始
 async for item in runner.run(
 async for item in runner.run(
     messages=[{"role": "user", "content": "换一个方案试试"}],
     messages=[{"role": "user", "content": "换一个方案试试"}],
-    config=RunConfig(trace_id="existing-trace-id", insert_after=5),
+    config=RunConfig(trace_id="existing-trace-id", after_sequence=5),
 ):
 ):
     ...
     ...
 
 
 # 重新生成:回溯后不插入新消息,直接基于已有消息重跑
 # 重新生成:回溯后不插入新消息,直接基于已有消息重跑
 async for item in runner.run(
 async for item in runner.run(
     messages=[],
     messages=[],
-    config=RunConfig(trace_id="existing-trace-id", insert_after=5),
+    config=RunConfig(trace_id="existing-trace-id", after_sequence=5),
 ):
 ):
     ...
     ...
 ```
 ```
 
 
-`insert_after` 的值是 message 的 `sequence` 号,可通过 `GET /api/traces/{trace_id}/messages` 查看。如果指定的 sequence 是一条带 `tool_calls` 的 assistant 消息,系统会自动将截断点扩展到其所有对应的 tool response 之后(安全截断)。
+`after_sequence` 的值是 message 的 `sequence` 号,可通过 `GET /api/traces/{trace_id}/messages` 查看。如果指定的 sequence 是一条带 `tool_calls` 的 assistant 消息,系统会自动将截断点扩展到其所有对应的 tool response 之后(安全截断)。
 
 
 **停止运行**:
 **停止运行**:
 
 
@@ -269,7 +271,7 @@ await runner.stop(trace_id)
 |------|------|------|
 |------|------|------|
 | GET  | `/api/traces` | 列出 Traces |
 | GET  | `/api/traces` | 列出 Traces |
 | GET  | `/api/traces/{id}` | 获取 Trace 详情(含 GoalTree、Sub-Traces) |
 | GET  | `/api/traces/{id}` | 获取 Trace 详情(含 GoalTree、Sub-Traces) |
-| GET  | `/api/traces/{id}/messages` | 获取 Messages |
+| GET  | `/api/traces/{id}/messages` | 获取 Messages(支持 mode=main_path/all) |
 | GET  | `/api/traces/running` | 列出正在运行的 Trace |
 | GET  | `/api/traces/running` | 列出正在运行的 Trace |
 | WS   | `/api/traces/{id}/watch` | 实时事件推送 |
 | WS   | `/api/traces/{id}/watch` | 实时事件推送 |
 
 
@@ -292,17 +294,17 @@ curl -X POST http://localhost:8000/api/traces \
   -H "Content-Type: application/json" \
   -H "Content-Type: application/json" \
   -d '{"messages": [{"role": "user", "content": "分析项目架构"}], "model": "gpt-4o"}'
   -d '{"messages": [{"role": "user", "content": "分析项目架构"}], "model": "gpt-4o"}'
 
 
-# 续跑(insert_after 为 null 或省略)
+# 续跑(after_sequence 为 null 或省略)
 curl -X POST http://localhost:8000/api/traces/{trace_id}/run \
 curl -X POST http://localhost:8000/api/traces/{trace_id}/run \
   -d '{"messages": [{"role": "user", "content": "继续深入分析"}]}'
   -d '{"messages": [{"role": "user", "content": "继续深入分析"}]}'
 
 
 # 回溯:从 sequence 5 处截断,插入新消息重新执行
 # 回溯:从 sequence 5 处截断,插入新消息重新执行
 curl -X POST http://localhost:8000/api/traces/{trace_id}/run \
 curl -X POST http://localhost:8000/api/traces/{trace_id}/run \
-  -d '{"insert_after": 5, "messages": [{"role": "user", "content": "换一个方案"}]}'
+  -d '{"after_sequence": 5, "messages": [{"role": "user", "content": "换一个方案"}]}'
 
 
 # 重新生成:回溯到 sequence 5,不插入新消息,直接重跑
 # 重新生成:回溯到 sequence 5,不插入新消息,直接重跑
 curl -X POST http://localhost:8000/api/traces/{trace_id}/run \
 curl -X POST http://localhost:8000/api/traces/{trace_id}/run \
-  -d '{"insert_after": 5, "messages": []}'
+  -d '{"after_sequence": 5, "messages": []}'
 
 
 # 停止
 # 停止
 curl -X POST http://localhost:8000/api/traces/{trace_id}/stop
 curl -X POST http://localhost:8000/api/traces/{trace_id}/stop
@@ -752,8 +754,9 @@ agent/memory/skills/
 通过 `POST /api/traces/{id}/reflect` 触发:
 通过 `POST /api/traces/{id}/reflect` 触发:
 
 
 1. 在 trace 末尾追加一条 user message(内置反思 prompt),**作为侧枝**(parent_sequence 分叉,不在主路径上)
 1. 在 trace 末尾追加一条 user message(内置反思 prompt),**作为侧枝**(parent_sequence 分叉,不在主路径上)
-2. Agent 回顾整个执行过程生成经验总结
+2. 使用 `max_iterations=1, tools=[]` 进行单轮无工具 LLM 调用,Agent 回顾整个执行过程生成经验总结
 3. 将 assistant 的反思内容追加到 `./cache/experiences.md`
 3. 将 assistant 的反思内容追加到 `./cache/experiences.md`
+4. 恢复 head_sequence(try/finally 保证异常时也恢复)
 
 
 反思消息不影响主对话路径。正常 continue/rewind 时看不到反思消息。
 反思消息不影响主对话路径。正常 continue/rewind 时看不到反思消息。
 
 
@@ -827,7 +830,7 @@ class TraceStore(Protocol):
     async def get_trace(self, trace_id: str) -> Trace: ...
     async def get_trace(self, trace_id: str) -> Trace: ...
     async def update_trace(self, trace_id: str, **updates) -> None: ...
     async def update_trace(self, trace_id: str, **updates) -> None: ...
     async def add_message(self, message: Message) -> None: ...
     async def add_message(self, message: Message) -> None: ...
-    async def get_trace_messages(self, trace_id: str, include_abandoned: bool = False) -> List[Message]: ...
+    async def get_trace_messages(self, trace_id: str) -> List[Message]: ...
     async def get_main_path_messages(self, trace_id: str, head_sequence: int) -> List[Message]: ...
     async def get_main_path_messages(self, trace_id: str, head_sequence: int) -> List[Message]: ...
     async def get_messages_by_goal(self, trace_id: str, goal_id: str) -> List[Message]: ...
     async def get_messages_by_goal(self, trace_id: str, goal_id: str) -> List[Message]: ...
     async def append_event(self, trace_id: str, event_type: str, payload: Dict) -> int: ...
     async def append_event(self, trace_id: str, event_type: str, payload: Dict) -> int: ...

+ 57 - 0
docs/decisions.md

@@ -1099,4 +1099,61 @@ Message Tree 解决了消息层面的分支问题,但 GoalTree 是独立的状
 
 
 **实现**:`agent/trace/compaction.py`, `agent/trace/goal_models.py`, `agent/core/runner.py`
 **实现**:`agent/trace/compaction.py`, `agent/trace/goal_models.py`, `agent/core/runner.py`
 
 
+---
+
+## Decision 23: 控制 API 适配消息树
+
+**背景**:Decision 20 引入 parent_sequence 消息树后,控制 API(run_api.py)和查询 API(api.py)的接口语义需要同步更新。原有的 `insert_after`、`include_abandoned` 等概念不再匹配消息树模型。
+
+### 23a. `insert_after` → `after_sequence`(统一续跑/回溯/分支)
+
+**问题**:原 `insert_after` 创建了"续跑"和"回溯"的二分概念,但在消息树模型中,二者本质相同——都是指定新消息的 `parent_sequence`。
+
+**决策**:`TraceRunRequest.insert_after` 重命名为 `after_sequence`。`RunConfig.insert_after` 同步重命名为 `after_sequence`。Runner 根据 `after_sequence` 与当前 `head_sequence` 的关系自动判断行为:
+
+| 情况 | 判断条件 | 行为 |
+|------|---------|------|
+| 续跑 | `after_sequence` 为 None 或 == `head_sequence` | 从末尾追加 |
+| 回溯 | `after_sequence` 在当前主路径上且 < `head_sequence` | 截断后追加,GoalTree 快照+重建 |
+| 分支切换 | `after_sequence` 不在当前主路径上 | 切换到该分支追加(预留,暂不实现) |
+
+前端只需传 `after_sequence`(从哪条消息后面接着跑)和 `messages`(可为空),不需要理解内部模式。
+
+### 23b. `_rewind` 完成目标检测修正
+
+**问题**:`_rewind()` 中使用 `[m for m in all_messages if m.sequence <= cutoff]` 过滤消息来检测 completed goals,但 `all_messages` 包含所有分支的消息,可能把其他分支的消息误判为回溯点之前的消息。
+
+**决策**:改用 `get_main_path_messages(trace_id, cutoff_sequence)` 从 cutoff 沿 parent_sequence 链回溯,只获取实际主路径上的消息来判断哪些 goals 有消息。
+
+### 23c. Reflect 隔离
+
+**问题**:当前 reflect 以续跑方式调用 `run_result()`,会进入完整 agent loop(含工具调用、GoalTree 操作),可能产生副作用。且 head_sequence 恢复不在 try/finally 中,异常时会丢失。
+
+**决策**:
+- reflect 使用 `RunConfig(max_iterations=1, tools=[])` 限制为单轮无工具 LLM 调用
+- head_sequence 恢复放入 try/finally
+- reflect 消息仍为侧枝(parent_sequence 分叉,不在主路径上)
+
+### 23d. Messages 查询 API 适配消息树
+
+**问题**:`GET /api/traces/{id}/messages` 的 `include_abandoned` 参数基于旧的 abandoned 标记概念,不再适用于消息树。
+
+**决策**:替换为两个参数:
+- `mode`: `main_path`(默认)| `all` — 返回当前主路径消息或全部消息
+- `head`: 可选 sequence 值 — 指定从哪个 head 构建主路径(默认用 trace.head_sequence)
+
+`mode=main_path` 调用 `get_main_path_messages()`;`mode=all` 调用 `get_trace_messages()`。
+
+### 23e. Rewind 事件增加 head_sequence
+
+Rewind 事件 payload 中增加 `head_sequence` 字段,便于前端感知分支切换后的新 head 位置。
+
+### 变更范围
+
+- `agent/trace/run_api.py` — `TraceRunRequest.after_sequence`、reflect 隔离
+- `agent/core/runner.py` — `RunConfig.after_sequence`、`_prepare_existing_trace`、`_rewind` 修正
+- `agent/trace/api.py` — messages 查询参数 `mode`/`head`
+
+**实现**:`agent/trace/run_api.py`, `agent/core/runner.py`, `agent/trace/api.py`
+
 ---
 ---

+ 19 - 10
docs/trace-api.md

@@ -204,10 +204,13 @@ GET /api/traces/{trace_id}
 #### 3. 获取 Messages
 #### 3. 获取 Messages
 
 
 ```http
 ```http
-GET /api/traces/{trace_id}/messages?goal_id=3
+GET /api/traces/{trace_id}/messages?mode=main_path&head=15&goal_id=3
 ```
 ```
 
 
-返回指定 Trace 的 Messages,可选按 Goal 过滤。
+返回指定 Trace 的 Messages。参数:
+- `mode`: `main_path`(默认)| `all` — 返回主路径消息或全部消息
+- `head`: 可选 sequence 值 — 指定主路径的 head(默认用 trace.head_sequence,仅 mode=main_path 有效)
+- `goal_id`: 可选,按 Goal 过滤
 
 
 **实现**:`agent/trace/api.py`
 **实现**:`agent/trace/api.py`
 
 
@@ -222,11 +225,13 @@ POST /api/traces
 Content-Type: application/json
 Content-Type: application/json
 
 
 {
 {
-  "messages": [{"role": "user", "content": "分析项目架构"}],
+  "messages": [
+    {"role": "system", "content": "自定义 system prompt(可选,不传则从 skills 自动构建)"},
+    {"role": "user", "content": "分析项目架构"}
+  ],
   "model": "gpt-4o",
   "model": "gpt-4o",
   "temperature": 0.3,
   "temperature": 0.3,
   "max_iterations": 200,
   "max_iterations": 200,
-  "system_prompt": null,
   "tools": null,
   "tools": null,
   "name": "任务名称",
   "name": "任务名称",
   "uid": "user_id"
   "uid": "user_id"
@@ -241,13 +246,15 @@ Content-Type: application/json
 
 
 {
 {
   "messages": [{"role": "user", "content": "..."}],
   "messages": [{"role": "user", "content": "..."}],
-  "insert_after": null
+  "after_sequence": null
 }
 }
 ```
 ```
 
 
-- `insert_after: null`(或省略) → 从末尾续跑
-- `insert_after: N` → 回溯到 sequence N 后运行
-- `messages: []` + `insert_after: N` → 重新生成
+- `after_sequence: null`(或省略)→ 从末尾续跑
+- `after_sequence: N`(主路径上且 < head)→ 回溯到 sequence N 后运行
+- `messages: []` + `after_sequence: N` → 重新生成
+
+Runner 根据 `after_sequence` 与 `head_sequence` 的关系自动判断续跑/回溯行为。
 
 
 #### 6. 停止运行中的 Trace
 #### 6. 停止运行中的 Trace
 
 
@@ -274,8 +281,9 @@ Content-Type: application/json
 }
 }
 ```
 ```
 
 
-在 trace 末尾追加一条包含反思 prompt 的 user message,以续跑方式运行 agent。
-Agent 回顾整个执行过程后生成经验总结,结果自动追加到 `./cache/experiences.md`。
+在 trace 末尾追加一条包含反思 prompt 的 user message,作为侧枝运行。
+使用 `max_iterations=1, tools=[]` 进行单轮无工具 LLM 调用,生成经验总结,
+结果自动追加到 `./cache/experiences.md`。head_sequence 通过 try/finally 保证恢复。
 
 
 ### 经验端点
 ### 经验端点
 
 
@@ -309,6 +317,7 @@ ws://localhost:8000/api/traces/{trace_id}/watch?since_event_id=0
 | `message_added` | 新 Message | message 数据(含 goal_id),affected_goals |
 | `message_added` | 新 Message | message 数据(含 goal_id),affected_goals |
 | `sub_trace_started` | Sub-Trace 开始执行 | trace_id, parent_goal_id, agent_type, task |
 | `sub_trace_started` | Sub-Trace 开始执行 | trace_id, parent_goal_id, agent_type, task |
 | `sub_trace_completed` | Sub-Trace 完成 | trace_id, status, summary, stats |
 | `sub_trace_completed` | Sub-Trace 完成 | trace_id, status, summary, stats |
+| `rewind` | 回溯执行 | after_sequence, head_sequence, goal_tree_snapshot |
 | `trace_completed` | 执行完成 | 统计信息 |
 | `trace_completed` | 执行完成 | 统计信息 |
 
 
 ### Stats 更新逻辑
 ### Stats 更新逻辑