Преглед изворни кода

fix(task-progress): revision 3 协议损坏不再静默重建

恢复 Recursive revision 3 时要求 task_protocol 为显式 schema_version=1 的完整结构;缺失、非对象或缺少任一必需字段均在状态修改前失败关闭。

新建 Trace 改为由框架显式初始化协议,历史 Legacy/revision 1 和只读 revision 2 的读取兼容仍可补齐旧字段。增加缺失与部分协议恢复回归,确认失败不会改写 Trace。
SamLee пре 15 часа
родитељ
комит
417e3a5aa8
3 измењених фајлова са 34 додато и 0 уклоњено
  1. 2 0
      cyber_agent/core/runner.py
  2. 14 0
      cyber_agent/core/task_protocol.py
  3. 18 0
      tests/test_task_progress.py

+ 2 - 0
cyber_agent/core/runner.py

@@ -78,6 +78,7 @@ from cyber_agent.core.task_protocol import (
     RootTaskAnchor,
     ensure_task_protocol,
     initialize_task_progress,
+    new_task_protocol,
     task_progress_artifact_refs,
     task_progress_at_revision,
     task_progress_readiness_error,
@@ -1690,6 +1691,7 @@ class AgentRunner:
                 validator_settings,
             )
             trace_context[RESOURCE_BUDGET_CONTEXT_KEY] = budget.to_dict()
+            trace_context["task_protocol"] = new_task_protocol()
             state = ensure_task_protocol(trace_context)
             if policy.requires_task_progress:
                 initialize_task_progress(

+ 14 - 0
cyber_agent/core/task_protocol.py

@@ -15,6 +15,7 @@ from typing import Any, Literal
 from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
 
 from cyber_agent.core.artifacts import ArtifactRef
+from cyber_agent.core.agent_mode import policy_from_context
 from cyber_agent.core.task_contract import TaskContractRef
 from cyber_agent.core.validation import RetryFrom
 from cyber_agent.application.candidate import CandidateRef, CandidateReviewAction
@@ -708,10 +709,23 @@ def ensure_task_protocol(context: dict[str, Any]) -> dict[str, Any]:
     Runner、Sub-Agent 和报告/审核工具在读写任务状态前统一调用此入口。
     """
     state = context.get("task_protocol")
+    policy = policy_from_context(context)
+    strict = policy.requires_task_progress
     if not isinstance(state, dict):
+        if strict:
+            raise ValueError(
+                "Recursive revision 3 task_protocol is missing or invalid"
+            )
         state = new_task_protocol()
         context["task_protocol"] = state
     defaults = new_task_protocol()
+    if strict:
+        missing = sorted(set(defaults).difference(state))
+        if missing:
+            raise ValueError(
+                "Recursive revision 3 task_protocol is missing required fields: "
+                + ", ".join(missing)
+            )
     for key, value in defaults.items():
         state.setdefault(key, deepcopy(value))
     if state.get("schema_version") != TASK_PROTOCOL_SCHEMA_VERSION:

+ 18 - 0
tests/test_task_progress.py

@@ -378,6 +378,24 @@ class TaskProtocolServiceTest(unittest.IsolatedAsyncioTestCase):
         self.assertEqual(1, state["task_progress_head_revision"])
         self.assertEqual(2, len(state["task_progress_revisions"]))
 
+    async def test_revision_three_resume_rejects_missing_or_partial_protocol(self):
+        runner, config = await self.prepare_resume_contract()
+        trace = await self.store.get_trace("root")
+        trace.context.pop("task_protocol")
+        await self.store.update_trace("root", context=trace.context)
+
+        with self.assertRaisesRegex(ValueError, "task_protocol is missing"):
+            await runner._prepare_existing_trace(config)
+        persisted = await self.store.get_trace("root")
+        self.assertNotIn("task_protocol", persisted.context)
+        self.assertEqual("stopped", persisted.status)
+
+        trace.context["task_protocol"] = new_task_protocol()
+        trace.context["task_protocol"].pop("task_progress_revisions")
+        await self.store.update_trace("root", context=trace.context)
+        with self.assertRaisesRegex(ValueError, "missing required fields"):
+            await runner._prepare_existing_trace(config)
+
     async def test_resume_rejects_report_bound_to_abandoned_progress_branch(self):
         runner, config = await self.prepare_resume_contract()
         trace = await self.store.get_trace("root")