|
|
@@ -13,6 +13,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.validation import RetryFrom
|
|
|
|
|
|
|
|
|
@@ -27,6 +28,8 @@ ReviewDecision = Literal[
|
|
|
"ASCEND",
|
|
|
"FAIL",
|
|
|
]
|
|
|
+TaskValidationScope = Literal["evidence", "hypothesis", "output", "task"]
|
|
|
+_VALIDATION_SCOPE_ORDER = ("evidence", "hypothesis", "output", "task")
|
|
|
|
|
|
|
|
|
class StrictProtocolModel(BaseModel):
|
|
|
@@ -78,6 +81,7 @@ class TaskBrief(StrictProtocolModel):
|
|
|
context: dict[str, Any] = Field(default_factory=dict)
|
|
|
constraints: list[str] = Field(default_factory=list)
|
|
|
context_refs: list[ContextRef] = Field(default_factory=list)
|
|
|
+ validation_scopes: list[TaskValidationScope] = Field(default_factory=list)
|
|
|
|
|
|
@field_validator(
|
|
|
"completion_criteria",
|
|
|
@@ -91,6 +95,15 @@ class TaskBrief(StrictProtocolModel):
|
|
|
raise ValueError("items must be non-empty strings")
|
|
|
return items
|
|
|
|
|
|
+ @field_validator("validation_scopes")
|
|
|
+ @classmethod
|
|
|
+ def normalize_validation_scopes(
|
|
|
+ cls,
|
|
|
+ scopes: list[TaskValidationScope],
|
|
|
+ ) -> list[TaskValidationScope]:
|
|
|
+ selected = set(scopes)
|
|
|
+ return [scope for scope in _VALIDATION_SCOPE_ORDER if scope in selected]
|
|
|
+
|
|
|
|
|
|
class Validation(StrictProtocolModel):
|
|
|
"""子 Agent 在 TaskReport 中提交的执行者自检。
|
|
|
@@ -140,6 +153,8 @@ class TaskReportSubmission(StrictProtocolModel):
|
|
|
next_step_suggestion: NextStepSuggestion
|
|
|
outputs: list[dict[str, Any]] = Field(default_factory=list)
|
|
|
evidence: list[dict[str, Any]] = Field(default_factory=list)
|
|
|
+ artifact_refs: list[ArtifactRef] = Field(default_factory=list, max_length=20)
|
|
|
+ source_urls: list[str] = Field(default_factory=list, max_length=20)
|
|
|
remaining_issues: list[str] = Field(default_factory=list)
|
|
|
|
|
|
@field_validator("remaining_issues")
|
|
|
@@ -149,6 +164,20 @@ class TaskReportSubmission(StrictProtocolModel):
|
|
|
raise ValueError("remaining_issues must contain non-empty strings")
|
|
|
return items
|
|
|
|
|
|
+ @field_validator("source_urls")
|
|
|
+ @classmethod
|
|
|
+ def validate_source_urls(cls, items: list[str]) -> list[str]:
|
|
|
+ normalized: list[str] = []
|
|
|
+ for item in items:
|
|
|
+ value = item.strip()
|
|
|
+ if not value:
|
|
|
+ raise ValueError("source_urls must contain non-empty strings")
|
|
|
+ if not value.lower().startswith(("http://", "https://")):
|
|
|
+ raise ValueError("source_urls only accepts HTTP(S) URLs")
|
|
|
+ if value not in normalized:
|
|
|
+ normalized.append(value)
|
|
|
+ return normalized
|
|
|
+
|
|
|
@model_validator(mode="after")
|
|
|
def validate_outcome_consistency(self) -> "TaskReportSubmission":
|
|
|
if self.outcome == "satisfied" and (
|
|
|
@@ -410,6 +439,13 @@ def format_task_brief(
|
|
|
)
|
|
|
if constraints:
|
|
|
parts.append(f"\n## Constraints\n{constraints}")
|
|
|
+ effective_scopes = [*task_brief.validation_scopes]
|
|
|
+ if "task" not in effective_scopes:
|
|
|
+ effective_scopes.append("task")
|
|
|
+ parts.append(
|
|
|
+ "\n## Validation Scopes\n"
|
|
|
+ + ", ".join(effective_scopes)
|
|
|
+ )
|
|
|
if available_context_refs:
|
|
|
parts.append(
|
|
|
"\n## Available Context References\n"
|