Explorar el Código

框架上下文:刷新受保护角色信息并避免周期结果累积

为通用 Runner 增加 protected_context_refresh,在压缩摘要中重新注入最新角色上下文和任务信息。将 get_current_context 标记为仅保留最近一次输出,并覆盖 Worker、Validator、repair continuation 的压缩哨兵测试,避免旧 Bootstrap 在长会话中覆盖新 Attempt。
SamLee hace 14 horas
padre
commit
8f3fb55c3b

+ 13 - 1
agent/agent/core/runner.py

@@ -200,6 +200,7 @@ class RunConfig:
 
 
     # --- 自定义元数据上下文 ---
     # --- 自定义元数据上下文 ---
     context: Dict[str, Any] = field(default_factory=dict)
     context: Dict[str, Any] = field(default_factory=dict)
+    protected_context_refresh: Optional[Dict[str, Any]] = None
 
 
     # Trusted framework-only marker used by LocalAgentExecutor.  Ordinary
     # Trusted framework-only marker used by LocalAgentExecutor.  Ordinary
     # presets keep their historical temperature/max-iteration precedence.
     # presets keep their historical temperature/max-iteration precedence.
@@ -3790,7 +3791,18 @@ class AgentRunner:
                 return ""
                 return ""
             return f"## Current Plan\n\n{goal_tree.to_prompt(include_summary=True)}"
             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
         trace = await self.trace_store.get_trace(trace_id) if self.trace_store else None
-        return await self._build_task_context(trace)
+        task_context = await self._build_task_context(trace)
+        if not config.protected_context_refresh:
+            return task_context
+        protected = json.dumps(
+            config.protected_context_refresh,
+            ensure_ascii=False,
+            sort_keys=True,
+            separators=(",", ":"),
+            default=str,
+        )
+        parts = [item for item in (task_context, f"## Protected Role Context\n\n{protected}") if item]
+        return "\n\n".join(parts)
 
 
     # ===== 辅助方法 =====
     # ===== 辅助方法 =====
 
 

+ 5 - 0
agent/agent/orchestration/executor.py

@@ -162,6 +162,11 @@ class LocalAgentExecutor:
             enable_research_flow=False,
             enable_research_flow=False,
             knowledge=_disabled_knowledge(),
             knowledge=_disabled_knowledge(),
             context=protected,
             context=protected,
+            protected_context_refresh={
+                "role_context": context.get("role_context", {}),
+                "task_spec": context.get("task_spec", {}),
+                "attempt_id": context.get("attempt_id"),
+            },
             name=name,
             name=name,
         )
         )
         try:
         try:

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

@@ -102,4 +102,5 @@ async def get_current_context(
         title="📋 当前执行上下文",
         title="📋 当前执行上下文",
         output=context_content,
         output=context_content,
         long_term_memory="已刷新执行上下文",
         long_term_memory="已刷新执行上下文",
+        include_output_only_once=True,
     )
     )

+ 37 - 0
agent/tests/test_context_budget.py

@@ -4,6 +4,7 @@ import pytest
 
 
 from agent.core.prompts import build_summary_header
 from agent.core.prompts import build_summary_header
 from agent.core.runner import AgentRunner, RunConfig
 from agent.core.runner import AgentRunner, RunConfig
+from agent.orchestration import CompletionPolicy
 from agent.trace.compaction import (
 from agent.trace.compaction import (
     CompressionConfig,
     CompressionConfig,
     calibrate_prompt_estimate,
     calibrate_prompt_estimate,
@@ -188,6 +189,42 @@ async def test_oversized_minimum_context_fails_before_model_request(tmp_path):
     assert "context_budget_exceeded" in [event["event"] for event in events]
     assert "context_budget_exceeded" in [event["event"] for event in events]
 
 
 
 
+@pytest.mark.asyncio
+@pytest.mark.parametrize("sentinel", ["worker-latest", "validator-latest", "repair-latest"])
+async def test_compression_refreshes_latest_protected_role_context(tmp_path, sentinel):
+    store = FileSystemTraceStore(str(tmp_path))
+    trace = Trace(
+        trace_id=f"trace-{sentinel}",
+        mode="agent",
+        agent_role="worker",
+        context={"root_trace_id": "root"},
+    )
+    await store.create_trace(trace)
+
+    class Coordinator:
+        async def task_context(self, root_trace_id):
+            assert root_trace_id == "root"
+            return '{"state_revision":"ledger:latest"}'
+
+    runner = AgentRunner(trace_store=store, task_coordinator=Coordinator())
+    config = RunConfig(
+        completion_policy=CompletionPolicy.EXPLICIT_VALIDATION,
+        protected_context_refresh={"role_context": {"sentinel": sentinel}},
+    )
+    context = await runner._current_execution_context(trace.trace_id, None, config)
+
+    assert "ledger:latest" in context
+    assert sentinel in context
+    rebuilt = runner._rebuild_history_after_compression(
+        [
+            {"role": "system", "content": "policy"},
+            {"role": "user", "content": "old attempt bootstrap"},
+        ],
+        {"role": "user", "content": f"summary\n\n{context}"},
+    )
+    assert sentinel in rebuilt[-1]["content"]
+
+
 @pytest.mark.parametrize(
 @pytest.mark.parametrize(
     "kwargs",
     "kwargs",
     [
     [

+ 2 - 0
agent/tests/test_mode_isolation.py

@@ -122,6 +122,8 @@ async def test_explicit_current_context_uses_task_ledger_read_model(tmp_path):
     assert "Current Task Plan" in result.output
     assert "Current Task Plan" in result.output
     assert "deliver result" in result.output
     assert "deliver result" in result.output
     assert "GoalTree" not in result.output
     assert "GoalTree" not in result.output
+    assert result.include_output_only_once is True
+    assert "deliver result" not in result.to_llm_message(first_time=False)
     compression_context = await runner._current_execution_context(
     compression_context = await runner._current_execution_context(
         "planner",
         "planner",
         None,
         None,