ソースを参照

重构(运行时): 按完成策略隔离计划上下文与压缩流程

legacy_auto 继续创建和恢复 GoalTree;explicit_validation 不再创建、加载或向工具上下文暴露 goal_tree 与 goal_id。

显式模式通过 Coordinator 读取 TaskLedger 当前计划,跳过基于已完成 Goal 的 Level 1 确定性压缩,并在保留的 Level 2 总结中注入 TaskLedger 执行状态。

压缩提示词统一改用“当前执行状态”,使同一套 Level 2 流程同时服务 legacy GoalTree 和 explicit TaskLedger。
SamLee 15 時間 前
コミット
871dabc777

+ 8 - 8
agent/agent/core/prompts/compression.py

@@ -15,8 +15,8 @@ COMPRESSION_PROMPT_TEMPLATE = """请对以上对话历史进行压缩总结。
 4. 使用结构化格式(标题 + 要点 + 相关资源引用,若有)
 5. 控制在 2000 字以内
 
-当前 GoalTree 状态:
-{goal_tree_prompt}
+当前执行状态:
+{execution_context}
 
 格式要求:
 [[SUMMARY]]
@@ -37,8 +37,8 @@ SINGLE_TURN_PROMPT = """请对以上对话历史进行压缩总结。
 4. 使用结构化格式(标题 + 要点 + 相关资源引用,若有)
 5. 控制在 2000 字以内
 
-当前 GoalTree 状态:
-{goal_tree_prompt}
+当前执行状态:
+{execution_context}
 
 格式要求:
 [[SUMMARY]]
@@ -56,17 +56,17 @@ SUMMARY_HEADER_TEMPLATE = """## 对话历史摘要(自动压缩)
 # ===== 辅助函数 =====
 
 def build_compression_eval_prompt(
-    goal_tree_prompt: str,
+    execution_context: str,
     ex_reference_list: str = "",
 ) -> str:
     return COMPRESSION_EVAL_PROMPT_TEMPLATE.format(
-        goal_tree_prompt=goal_tree_prompt,
+        execution_context=execution_context,
         ex_reference_list=ex_reference_list,
     )
 
 
-def build_single_turn_prompt(goal_tree_prompt: str) -> str:
-    return SINGLE_TURN_PROMPT.format(goal_tree_prompt=goal_tree_prompt)
+def build_single_turn_prompt(execution_context: str) -> str:
+    return SINGLE_TURN_PROMPT.format(execution_context=execution_context)
 
 
 def build_summary_header(summary_text: str) -> str:

+ 82 - 30
agent/agent/core/runner.py

@@ -628,6 +628,7 @@ class AgentRunner:
         resolved_policy = self._resolve_run_policy(config)
         tool_schemas = self._get_run_tool_schemas(config, resolved_policy)
 
+        is_legacy = resolved_policy.completion_policy == CompletionPolicy.LEGACY_AUTO
         trace_obj = Trace(
             trace_id=trace_id,
             mode="agent",
@@ -635,7 +636,7 @@ class AgentRunner:
             agent_type=config.agent_type,
             agent_role=resolved_policy.role.value,
             parent_trace_id=config.parent_trace_id,
-            parent_goal_id=config.parent_goal_id,
+            parent_goal_id=config.parent_goal_id if is_legacy else None,
             uid=config.uid,
             model=config.model,
             tools=tool_schemas,
@@ -644,11 +645,12 @@ class AgentRunner:
             status="running",
         )
 
-        goal_tree = self.goal_tree or GoalTree(mission=task_name)
+        goal_tree = (self.goal_tree or GoalTree(mission=task_name)) if is_legacy else None
 
         if self.trace_store:
             await self.trace_store.create_trace(trace_obj)
-            await self.trace_store.update_goal_tree(trace_id, goal_tree)
+            if goal_tree is not None:
+                await self.trace_store.update_goal_tree(trace_id, goal_tree)
 
         return trace_obj, goal_tree, 1
 
@@ -664,11 +666,14 @@ class AgentRunner:
         if not trace_obj:
             raise ValueError(f"Trace not found: {config.trace_id}")
 
-        goal_tree = await self.trace_store.get_goal_tree(config.trace_id)
-        if goal_tree is None:
-            # 防御性兜底:trace 存在但 goal.json 丢失时,创建空树
-            goal_tree = GoalTree(mission=trace_obj.task or "Agent task")
-            await self.trace_store.update_goal_tree(config.trace_id, goal_tree)
+        policy = self._resolve_run_policy(config, trace_obj).completion_policy
+        goal_tree = None
+        if policy == CompletionPolicy.LEGACY_AUTO:
+            goal_tree = await self.trace_store.get_goal_tree(config.trace_id)
+            if goal_tree is None:
+                # Legacy traces retain the historical defensive empty tree.
+                goal_tree = GoalTree(mission=trace_obj.task or "Agent task")
+                await self.trace_store.update_goal_tree(config.trace_id, goal_tree)
 
         # 自动判断行为:after_sequence 为 None 或 == head → 续跑;< head → 回溯
         after_seq = config.after_sequence
@@ -927,9 +932,16 @@ class AgentRunner:
             config.force_side_branch = ["reflection", "compression"]
             return history, head_seq, sequence, True
 
-        # 以下为未启用反思、需要压缩的情况,直接进行level 1压缩,并检查是否需要进行level 2压缩(进入侧分支)
-        # Level 1 压缩:Goal 完成压缩
-        if config.goal_compression != "none" and self.trace_store and goal_tree:
+        # Level 1 是 legacy GoalTree 的确定性压缩;explicit 直接保留 Level 2。
+        is_legacy = (
+            CompletionPolicy(config.completion_policy) == CompletionPolicy.LEGACY_AUTO
+        )
+        if (
+            is_legacy
+            and config.goal_compression != "none"
+            and self.trace_store
+            and goal_tree
+        ):
             if head_seq > 0:
                 main_path_msgs = await self.trace_store.get_main_path_messages(
                     trace_id, head_seq
@@ -947,7 +959,7 @@ class AgentRunner:
                         "Level 1 压缩: 无可过滤消息 (%d 条全部保留)",
                         len(main_path_msgs),
                     )
-        elif needs_compression:
+        elif is_legacy:
             self.log.warning(
                 "Token 数 (%d) 超过阈值,但无法执行 Level 1 压缩(缺少 store 或 goal_tree,或 goal_compression=none)",
                 token_count,
@@ -960,8 +972,10 @@ class AgentRunner:
         needs_level2 = token_count_after > max_tokens
 
         if needs_level2:
+            source = "Level 1 后" if is_legacy else "explicit 模式"
             self.log.info(
-                "Level 1 后仍超阈值 (token=%d/%d),需要进入压缩侧分支",
+                "%s仍超阈值 (token=%d/%d),需要进入压缩侧分支",
+                source,
                 token_count_after,
                 max_tokens,
             )
@@ -972,7 +986,7 @@ class AgentRunner:
             return history, head_seq, sequence, True
 
         # 压缩完成后,输出最终发给模型的消息列表
-        self.log.info("Level 1 压缩完成,发送给模型的消息列表:")
+        self.log.info("上下文压缩检查完成,发送给模型的消息列表:")
         for idx, msg in enumerate(history):
             role = msg.get("role", "unknown")
             content = msg.get("content", "")
@@ -1070,8 +1084,12 @@ class AgentRunner:
         # 构建压缩 prompt(使用 SINGLE_TURN_PROMPT)
         from agent.core.prompts import build_single_turn_prompt
 
-        goal_prompt = goal_tree.to_prompt(include_summary=True) if goal_tree else ""
-        compress_prompt = build_single_turn_prompt(goal_prompt)
+        execution_context = await self._current_execution_context(
+            trace_id,
+            goal_tree,
+            config,
+        )
+        compress_prompt = build_single_turn_prompt(execution_context)
         compress_messages = list(history) + [
             {"role": "user", "content": compress_prompt}
         ]
@@ -1393,7 +1411,12 @@ class AgentRunner:
                 else:  # compression
                     from agent.trace.compaction import build_compression_prompt
 
-                    prompt = build_compression_prompt(goal_tree)
+                    execution_context = await self._current_execution_context(
+                        trace_id,
+                        goal_tree,
+                        config,
+                    )
+                    prompt = build_compression_prompt(execution_context)
 
                 branch_user_msg = Message.create(
                     trace_id=trace_id,
@@ -1720,11 +1743,13 @@ class AgentRunner:
 
                         summary_content = build_summary_header(summary_text)
 
-                        if goal_tree and goal_tree.goals:
-                            goal_tree_detail = goal_tree.to_prompt(include_summary=True)
-                            summary_content += (
-                                f"\n\n## Current Plan\n\n{goal_tree_detail}"
-                            )
+                        execution_context = await self._current_execution_context(
+                            trace_id,
+                            goal_tree,
+                            config,
+                        )
+                        if execution_context:
+                            summary_content += f"\n\n{execution_context}"
 
                         carried_systems: Optional[List[Dict[str, Any]]] = None
                         if self.trace_store:
@@ -3084,16 +3109,20 @@ class AgentRunner:
 
     def _build_context_injection(
         self,
-        trace: Trace,
+        trace: Optional[Trace],
         goal_tree: Optional[GoalTree],
+        *,
+        task_context: str = "",
     ) -> str:
-        """构建周期性注入的上下文(GoalTree + Active Collaborators + Focus 提醒 + IM 消息通知)"""
+        """构建周期上下文;计划来源由运行模式决定。"""
         from datetime import datetime
 
         parts = [f"## Current Time\n\n{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"]
+        trace_context = trace.context if trace and trace.context else {}
 
-        # GoalTree
-        if goal_tree and goal_tree.goals:
+        if task_context:
+            parts.append(task_context)
+        elif goal_tree and goal_tree.goals:
             parts.append(f"## Current Plan\n\n{goal_tree.to_prompt()}")
 
             if goal_tree.current_id:
@@ -3117,7 +3146,7 @@ class AgentRunner:
                 )
 
         # Active Collaborators
-        collaborators = trace.context.get("collaborators", [])
+        collaborators = trace_context.get("collaborators", [])
         if collaborators:
             lines = ["## Active Collaborators"]
             for c in collaborators:
@@ -3129,7 +3158,7 @@ class AgentRunner:
             parts.append("\n".join(lines))
 
         # IM 消息通知(Research Agent)
-        im_config = trace.context.get("im_config")
+        im_config = trace_context.get("im_config")
         if im_config:
             contact_id = im_config.get("contact_id")
             chat_id = im_config.get("chat_id")
@@ -3155,9 +3184,9 @@ class AgentRunner:
                     pass
 
         # Knowledge Manager 队列状态
-        km_queue_size = trace.context.get("km_queue_size")
+        km_queue_size = trace_context.get("km_queue_size")
         if km_queue_size is not None:
-            current_sender = trace.context.get("current_sender", "unknown")
+            current_sender = trace_context.get("current_sender", "unknown")
             if km_queue_size > 0:
                 parts.append(
                     f"## 消息队列状态\n\n"
@@ -3173,6 +3202,29 @@ class AgentRunner:
 
         return "\n\n".join(parts)
 
+    async def _build_task_context(self, trace: Optional[Trace]) -> str:
+        """从 explicit 模式的权威 TaskLedger 构建只读计划摘要。"""
+
+        if trace is None or self.task_coordinator is None:
+            return ""
+        root_trace_id = (trace.context or {}).get("root_trace_id", trace.trace_id)
+        return str(await self.task_coordinator.task_context(root_trace_id))
+
+    async def _current_execution_context(
+        self,
+        trace_id: str,
+        goal_tree: Optional[GoalTree],
+        config: RunConfig,
+    ) -> str:
+        """Return the active mode's plan read model for Level 2 summaries."""
+
+        if CompletionPolicy(config.completion_policy) == CompletionPolicy.LEGACY_AUTO:
+            if not goal_tree:
+                return ""
+            return f"## Current Plan\n\n{goal_tree.to_prompt(include_summary=True)}"
+        trace = await self.trace_store.get_trace(trace_id) if self.trace_store else None
+        return await self._build_task_context(trace)
+
     # ===== 辅助方法 =====
 
     async def _optimize_images(self, messages: List[Dict], model: str) -> List[Dict]:

+ 9 - 3
agent/agent/core/runner_tools.py

@@ -115,19 +115,18 @@ class RunnerToolRuntime:
         """Merge host context first so framework identity always wins."""
         context = dict(config.context or {})
         trace_context = trace.context or {}
+        completion_policy = CompletionPolicy(config.completion_policy)
         context.update(
             {
                 "store": trace_store,
                 "trace_id": trace.trace_id,
                 "root_trace_id": trace_context.get("root_trace_id", trace.trace_id),
-                "goal_id": goal_tree.current_id if goal_tree else None,
                 "runner": runner,
-                "goal_tree": goal_tree,
                 "knowledge_config": config.knowledge,
                 "sequence": sequence,
                 "side_branch": side_branch,
                 "role": trace.agent_role,
-                "completion_policy": CompletionPolicy(config.completion_policy).value,
+                "completion_policy": completion_policy.value,
                 "task_id": trace_context.get("task_id"),
                 "spec_version": trace_context.get("spec_version"),
                 "attempt_id": trace_context.get("attempt_id"),
@@ -136,6 +135,13 @@ class RunnerToolRuntime:
                 "coordinator": task_coordinator,
             }
         )
+        if completion_policy == CompletionPolicy.LEGACY_AUTO:
+            context.update(
+                {
+                    "goal_id": goal_tree.current_id if goal_tree else None,
+                    "goal_tree": goal_tree,
+                }
+            )
         return context
 
     async def execute_authorized_tool(

+ 25 - 6
agent/agent/tools/builtin/context.py

@@ -2,13 +2,14 @@
 上下文工具 - 获取当前执行上下文
 
 提供 get_current_context 工具,让 Agent 可以主动获取:
-- 当前计划(GoalTree)
+- 当前计划(legacy 为 GoalTree,explicit 为 TaskLedger 读模型
 - 焦点提醒
 - 协作者状态
 
 框架也会在特定轮次自动调用此工具进行周期性上下文刷新。
 """
 
+from agent.orchestration.models import CompletionPolicy
 from agent.tools import tool, ToolResult, ToolContext
 
 
@@ -25,11 +26,14 @@ async def get_current_context(
     获取当前执行上下文
 
     Returns:
-        ToolResult: 包含 GoalTree、焦点提醒、协作者状态等信息
+        ToolResult: 包含当前模式的计划、焦点提醒和协作者状态
     """
     runner = context.get("runner")
     goal_tree = context.get("goal_tree")
     trace_id = context.get("trace_id")
+    policy = CompletionPolicy(
+        context.get("completion_policy", CompletionPolicy.LEGACY_AUTO.value)
+    )
 
     if not runner:
         return ToolResult(
@@ -43,12 +47,27 @@ async def get_current_context(
     if runner.trace_store and trace_id:
         trace = await runner.trace_store.get_trace(trace_id)
 
-    # 构建上下文内容(复用 runner 的 _build_context_injection 方法)
+    task_context = ""
+    if policy == CompletionPolicy.EXPLICIT_VALIDATION:
+        if hasattr(runner, "_build_task_context"):
+            task_context = await runner._build_task_context(trace)
+        else:
+            coordinator = context.get("coordinator")
+            root_trace_id = context.get("root_trace_id")
+            if coordinator and root_trace_id:
+                task_context = await coordinator.task_context(root_trace_id)
+
+    # 构建上下文内容(复用 runner 的周期注入格式)
     if hasattr(runner, '_build_context_injection'):
-        context_content = runner._build_context_injection(trace, goal_tree)
+        context_content = runner._build_context_injection(
+            trace,
+            goal_tree,
+            task_context=task_context,
+        )
     else:
-        # Fallback:只返回 GoalTree
-        if goal_tree and goal_tree.goals:
+        if task_context:
+            context_content = task_context
+        elif goal_tree and goal_tree.goals:
             context_content = f"## Current Plan\n\n{goal_tree.to_prompt()}"
         else:
             context_content = "暂无计划信息"

+ 4 - 10
agent/agent/trace/compaction.py

@@ -358,18 +358,12 @@ def needs_level2_compression(
 # COMPRESSION_EVAL_PROMPT 和 REFLECT_PROMPT 现在从 prompts.py 导入
 
 
-def build_compression_prompt(goal_tree: Optional[GoalTree]) -> str:
-    """构建 Level 2 压缩 prompt"""
-    goal_prompt = ""
-    if goal_tree:
-        goal_prompt = goal_tree.to_prompt(include_summary=True)
-
-    return build_compression_eval_prompt(
-        goal_tree_prompt=goal_prompt,
-    )
+def build_compression_prompt(execution_context: str) -> str:
+    """构建 Level 2 压缩 prompt,计划上下文由运行模式提供。"""
+
+    return build_compression_eval_prompt(execution_context=execution_context)
 
 
 def build_reflect_prompt() -> str:
     """构建反思 prompt"""
     return REFLECT_PROMPT
-