Просмотр исходного кода

feat(Agent验证): 为验证计划增加确定性预检规则

在 ValidationPlan 中持久化 preflight_rule_ids,并补齐唯一性、空值校验、序列化和 Wire 视图字段。该字段允许 Agent Validator 启动前先执行不依赖模型的硬规则。
SamLee 1 день назад
Родитель
Сommit
1713af8588
2 измененных файлов с 52 добавлено и 42 удалено
  1. 51 42
      agent/agent/orchestration/models.py
  2. 1 0
      agent/agent/orchestration/wire.py

+ 51 - 42
agent/agent/orchestration/models.py

@@ -89,12 +89,14 @@ class ValidationPlan:
     mode: ValidationMode = ValidationMode.AGENT
     validator_preset: Optional[str] = "validator"
     rule_ids: Tuple[str, ...] = ()
+    preflight_rule_ids: Tuple[str, ...] = ()
     max_evidence_queries: int = 10
     max_evidence_items_per_query: int = 50
     evidence_timeout_seconds: float = 10.0
 
     def __post_init__(self) -> None:
         object.__setattr__(self, "rule_ids", tuple(self.rule_ids))
+        object.__setattr__(self, "preflight_rule_ids", tuple(self.preflight_rule_ids))
         if self.mode == ValidationMode.AGENT and self.rule_ids:
             raise ValueError("Agent validation plans cannot contain deterministic rules")
         if (
@@ -109,6 +111,10 @@ class ValidationPlan:
             raise ValueError("Validation rule_ids must be unique")
         if any(not item.strip() for item in self.rule_ids):
             raise ValueError("Validation rule_ids cannot be empty")
+        if len(set(self.preflight_rule_ids)) != len(self.preflight_rule_ids):
+            raise ValueError("Validation preflight_rule_ids must be unique")
+        if any(not item.strip() for item in self.preflight_rule_ids):
+            raise ValueError("Validation preflight_rule_ids cannot be empty")
         if (
             isinstance(self.max_evidence_queries, bool)
             or not isinstance(self.max_evidence_queries, int)
@@ -147,10 +153,9 @@ class ValidationPlan:
                 "validator" if mode == ValidationMode.AGENT else None,
             ),
             rule_ids=tuple(data.get("rule_ids", [])),
+            preflight_rule_ids=tuple(data.get("preflight_rule_ids", [])),
             max_evidence_queries=int(data.get("max_evidence_queries", 10)),
-            max_evidence_items_per_query=int(
-                data.get("max_evidence_items_per_query", 50)
-            ),
+            max_evidence_items_per_query=int(data.get("max_evidence_items_per_query", 50)),
             evidence_timeout_seconds=float(data.get("evidence_timeout_seconds", 10.0)),
         )
 
@@ -206,13 +211,9 @@ class ExecutionStats:
     def __post_init__(self) -> None:
         if self.primary_model is not None:
             if not isinstance(self.primary_model, str) or not self.primary_model.strip():
-                raise ValueError(
-                    "ExecutionStats.primary_model must be a non-blank string"
-                )
+                raise ValueError("ExecutionStats.primary_model must be a non-blank string")
         if self.total_tokens is not None and (
-            isinstance(self.total_tokens, bool)
-            or not isinstance(self.total_tokens, int)
-            or self.total_tokens < 0
+            isinstance(self.total_tokens, bool) or not isinstance(self.total_tokens, int) or self.total_tokens < 0
         ):
             raise ValueError("ExecutionStats.total_tokens must be a non-negative integer")
         if self.total_cost is not None and (
@@ -222,9 +223,7 @@ class ExecutionStats:
             or self.total_cost < 0
         ):
             raise ValueError("ExecutionStats.total_cost must be finite and non-negative")
-        if self.failure_code is not None and not isinstance(
-            self.failure_code, FailureCode
-        ):
+        if self.failure_code is not None and not isinstance(self.failure_code, FailureCode):
             try:
                 object.__setattr__(self, "failure_code", FailureCode(self.failure_code))
             except (TypeError, ValueError) as exc:
@@ -285,11 +284,7 @@ class TaskSpec:
     created_at: str = field(default_factory=utc_now)
 
     def __post_init__(self) -> None:
-        if (
-            isinstance(self.version, bool)
-            or not isinstance(self.version, int)
-            or self.version <= 0
-        ):
+        if isinstance(self.version, bool) or not isinstance(self.version, int) or self.version <= 0:
             raise ValueError("TaskSpec.version must be a positive integer")
         if not isinstance(self.objective, str) or not self.objective.strip():
             raise ValueError("TaskSpec.objective must be a non-empty string")
@@ -303,9 +298,7 @@ class TaskSpec:
         if not acceptance_criteria:
             raise ValueError("TaskSpec.acceptance_criteria must contain at least one criterion")
         if any(not isinstance(item, AcceptanceCriterion) for item in acceptance_criteria):
-            raise ValueError(
-                "TaskSpec.acceptance_criteria must contain AcceptanceCriterion values"
-            )
+            raise ValueError("TaskSpec.acceptance_criteria must contain AcceptanceCriterion values")
         criterion_ids = [item.criterion_id for item in acceptance_criteria]
         if len(set(criterion_ids)) != len(criterion_ids):
             raise ValueError("TaskSpec acceptance criterion IDs must be unique")
@@ -323,9 +316,7 @@ class TaskSpec:
         if isinstance(criteria_data, (str, bytes, dict)) or criteria_data is None:
             raise ValueError("TaskSpec.acceptance_criteria must be a collection")
         try:
-            acceptance_criteria = tuple(
-                AcceptanceCriterion.from_dict(item) for item in criteria_data
-            )
+            acceptance_criteria = tuple(AcceptanceCriterion.from_dict(item) for item in criteria_data)
         except TypeError as exc:
             raise ValueError("TaskSpec.acceptance_criteria must be a collection") from exc
         return cls(
@@ -482,9 +473,7 @@ class TaskAttempt:
         if self.accepted_child_decision_ids is not None:
             values = tuple(self.accepted_child_decision_ids)
             if any(not isinstance(item, str) or not item for item in values):
-                raise ValueError(
-                    "TaskAttempt.accepted_child_decision_ids must contain non-empty strings"
-                )
+                raise ValueError("TaskAttempt.accepted_child_decision_ids must contain non-empty strings")
             object.__setattr__(self, "accepted_child_decision_ids", values)
 
     @classmethod
@@ -509,9 +498,7 @@ class TaskAttempt:
             snapshot_id=data.get("snapshot_id"),
             submission=AttemptSubmission.from_dict(submission) if submission else None,
             execution_stats=(
-                ExecutionStats.from_dict(data["execution_stats"])
-                if data.get("execution_stats") is not None
-                else None
+                ExecutionStats.from_dict(data["execution_stats"]) if data.get("execution_stats") is not None else None
             ),
             error=data.get("error"),
             started_at=data.get("started_at"),
@@ -585,8 +572,7 @@ class ValidationReport:
             validation_plan=ValidationPlan.from_dict(data.get("validation_plan", {})),
             evidence_queries_used=int(data.get("evidence_queries_used", 0)),
             evidence_query_results={
-                str(key): dict(value)
-                for key, value in data.get("evidence_query_results", {}).items()
+                str(key): dict(value) for key, value in data.get("evidence_query_results", {}).items()
             },
             status=ValidationRunStatus(data.get("status", ValidationRunStatus.PENDING.value)),
             operation_id=data.get("operation_id"),
@@ -599,9 +585,7 @@ class ValidationReport:
             risks=list(data.get("risks", [])),
             recommendation=data.get("recommendation", ""),
             execution_stats=(
-                ExecutionStats.from_dict(data["execution_stats"])
-                if data.get("execution_stats") is not None
-                else None
+                ExecutionStats.from_dict(data["execution_stats"]) if data.get("execution_stats") is not None else None
             ),
             error=data.get("error"),
             started_at=data.get("started_at"),
@@ -853,12 +837,37 @@ def json_values(value: Any) -> Any:
 
 
 __all__ = [
-    "CompletionPolicy", "AgentRole", "TaskStatus", "AttemptStatus",
-    "ValidationRunStatus", "ValidationVerdict", "ValidationMode", "ValidationPlan", "DecisionAction",
-    "OperationKind", "OperationStatus", "FailureCode", "ExecutionStats",
-    "AcceptanceCriterion", "TaskSpec", "TaskRecord", "TaskAttempt",
-    "ArtifactRef", "ArtifactSnapshot", "AttemptSubmission", "CriterionResult",
-    "ValidationReport", "PlannerDecision", "BackgroundOperation", "CommandRecord",
-    "EventDraft", "OrchestrationEvent", "EventPage", "TaskLedger", "TaskCycleResult",
-    "json_values", "new_id", "utc_now",
+    "CompletionPolicy",
+    "AgentRole",
+    "TaskStatus",
+    "AttemptStatus",
+    "ValidationRunStatus",
+    "ValidationVerdict",
+    "ValidationMode",
+    "ValidationPlan",
+    "DecisionAction",
+    "OperationKind",
+    "OperationStatus",
+    "FailureCode",
+    "ExecutionStats",
+    "AcceptanceCriterion",
+    "TaskSpec",
+    "TaskRecord",
+    "TaskAttempt",
+    "ArtifactRef",
+    "ArtifactSnapshot",
+    "AttemptSubmission",
+    "CriterionResult",
+    "ValidationReport",
+    "PlannerDecision",
+    "BackgroundOperation",
+    "CommandRecord",
+    "EventDraft",
+    "OrchestrationEvent",
+    "EventPage",
+    "TaskLedger",
+    "TaskCycleResult",
+    "json_values",
+    "new_id",
+    "utc_now",
 ]

+ 1 - 0
agent/agent/orchestration/wire.py

@@ -117,6 +117,7 @@ class ValidationPlanView(WireModel):
     mode: ValidationMode
     validator_preset: Optional[str] = None
     rule_ids: List[str]
+    preflight_rule_ids: List[str]
     max_evidence_queries: int
     max_evidence_items_per_query: int
     evidence_timeout_seconds: float