|
@@ -10,7 +10,8 @@ from __future__ import annotations
|
|
|
from dataclasses import asdict, dataclass, field
|
|
from dataclasses import asdict, dataclass, field
|
|
|
from datetime import datetime, timezone
|
|
from datetime import datetime, timezone
|
|
|
from enum import Enum
|
|
from enum import Enum
|
|
|
-from typing import Any, Dict, List, Optional
|
|
|
|
|
|
|
+from math import isfinite
|
|
|
|
|
+from typing import Any, Dict, List, Optional, Tuple
|
|
|
from uuid import uuid4
|
|
from uuid import uuid4
|
|
|
|
|
|
|
|
|
|
|
|
@@ -76,6 +77,84 @@ class ValidationVerdict(StrEnum):
|
|
|
INCONCLUSIVE = "inconclusive"
|
|
INCONCLUSIVE = "inconclusive"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+class ValidationMode(StrEnum):
|
|
|
|
|
+ AGENT = "agent"
|
|
|
|
|
+ DETERMINISTIC = "deterministic"
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+@dataclass(frozen=True)
|
|
|
|
|
+class ValidationPlan:
|
|
|
|
|
+ """Frozen, serializable validation behavior for one ValidationReport."""
|
|
|
|
|
+
|
|
|
|
|
+ mode: ValidationMode = ValidationMode.AGENT
|
|
|
|
|
+ validator_preset: Optional[str] = "validator"
|
|
|
|
|
+ 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))
|
|
|
|
|
+ if self.mode == ValidationMode.AGENT and self.rule_ids:
|
|
|
|
|
+ raise ValueError("Agent validation plans cannot contain deterministic rules")
|
|
|
|
|
+ if (
|
|
|
|
|
+ self.mode == ValidationMode.AGENT
|
|
|
|
|
+ and self.validator_preset is not None
|
|
|
|
|
+ and not self.validator_preset.strip()
|
|
|
|
|
+ ):
|
|
|
|
|
+ raise ValueError("Agent validator_preset cannot be blank")
|
|
|
|
|
+ if self.mode == ValidationMode.DETERMINISTIC and self.validator_preset:
|
|
|
|
|
+ raise ValueError("Deterministic validation plans cannot select an agent preset")
|
|
|
|
|
+ if len(set(self.rule_ids)) != len(self.rule_ids):
|
|
|
|
|
+ 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 (
|
|
|
|
|
+ isinstance(self.max_evidence_queries, bool)
|
|
|
|
|
+ or not isinstance(self.max_evidence_queries, int)
|
|
|
|
|
+ or self.max_evidence_queries < 0
|
|
|
|
|
+ ):
|
|
|
|
|
+ raise ValueError("max_evidence_queries must be a non-negative integer")
|
|
|
|
|
+ if (
|
|
|
|
|
+ isinstance(self.max_evidence_items_per_query, bool)
|
|
|
|
|
+ or not isinstance(self.max_evidence_items_per_query, int)
|
|
|
|
|
+ or self.max_evidence_items_per_query < 1
|
|
|
|
|
+ ):
|
|
|
|
|
+ raise ValueError("max_evidence_items_per_query must be a positive integer")
|
|
|
|
|
+ if (
|
|
|
|
|
+ isinstance(self.evidence_timeout_seconds, bool)
|
|
|
|
|
+ or not isinstance(self.evidence_timeout_seconds, (int, float))
|
|
|
|
|
+ or not isfinite(self.evidence_timeout_seconds)
|
|
|
|
|
+ or self.evidence_timeout_seconds <= 0
|
|
|
|
|
+ ):
|
|
|
|
|
+ raise ValueError("evidence_timeout_seconds must be finite and positive")
|
|
|
|
|
+
|
|
|
|
|
+ def to_dict(self) -> Dict[str, Any]:
|
|
|
|
|
+ return json_values(asdict(self))
|
|
|
|
|
+
|
|
|
|
|
+ @classmethod
|
|
|
|
|
+ def from_dict(
|
|
|
|
|
+ cls,
|
|
|
|
|
+ data: "ValidationPlan | Dict[str, Any]",
|
|
|
|
|
+ ) -> "ValidationPlan":
|
|
|
|
|
+ if isinstance(data, cls):
|
|
|
|
|
+ return data
|
|
|
|
|
+ mode = ValidationMode(data.get("mode", ValidationMode.AGENT.value))
|
|
|
|
|
+ return cls(
|
|
|
|
|
+ mode=mode,
|
|
|
|
|
+ validator_preset=data.get(
|
|
|
|
|
+ "validator_preset",
|
|
|
|
|
+ "validator" if mode == ValidationMode.AGENT else None,
|
|
|
|
|
+ ),
|
|
|
|
|
+ rule_ids=tuple(data.get("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)
|
|
|
|
|
+ ),
|
|
|
|
|
+ evidence_timeout_seconds=float(data.get("evidence_timeout_seconds", 10.0)),
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
class DecisionAction(StrEnum):
|
|
class DecisionAction(StrEnum):
|
|
|
ACCEPT = "accept"
|
|
ACCEPT = "accept"
|
|
|
REPAIR = "repair"
|
|
REPAIR = "repair"
|
|
@@ -89,6 +168,81 @@ class DecisionAction(StrEnum):
|
|
|
SUPERSEDE = "supersede"
|
|
SUPERSEDE = "supersede"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+class OperationKind(StrEnum):
|
|
|
|
|
+ DISPATCH = "dispatch"
|
|
|
|
|
+ REVALIDATE = "revalidate"
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+class OperationStatus(StrEnum):
|
|
|
|
|
+ PENDING = "pending"
|
|
|
|
|
+ RUNNING = "running"
|
|
|
|
|
+ STOP_REQUESTED = "stop_requested"
|
|
|
|
|
+ STOPPED = "stopped"
|
|
|
|
|
+ COMPLETED = "completed"
|
|
|
|
|
+ FAILED = "failed"
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+class FailureCode(StrEnum):
|
|
|
|
|
+ """Stable machine-readable reason for an execution-stage failure."""
|
|
|
|
|
+
|
|
|
|
|
+ UNSUPPORTED = "unsupported"
|
|
|
|
|
+ EXECUTOR_ERROR = "executor_error"
|
|
|
|
|
+ AGENT_FAILED = "agent_failed"
|
|
|
|
|
+ TIMEOUT = "timeout"
|
|
|
|
|
+ STOPPED = "stopped"
|
|
|
|
|
+ INTERRUPTED = "interrupted"
|
|
|
|
|
+ PROTOCOL_VIOLATION = "protocol_violation"
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+@dataclass(frozen=True)
|
|
|
|
|
+class ExecutionStats:
|
|
|
|
|
+ """Per-stage usage; elapsed time is derived from the stage timestamps."""
|
|
|
|
|
+
|
|
|
|
|
+ primary_model: Optional[str] = None
|
|
|
|
|
+ total_tokens: Optional[int] = None
|
|
|
|
|
+ total_cost: Optional[float] = None
|
|
|
|
|
+ failure_code: Optional[FailureCode] = None
|
|
|
|
|
+
|
|
|
|
|
+ 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"
|
|
|
|
|
+ )
|
|
|
|
|
+ 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
|
|
|
|
|
+ ):
|
|
|
|
|
+ raise ValueError("ExecutionStats.total_tokens must be a non-negative integer")
|
|
|
|
|
+ if self.total_cost is not None and (
|
|
|
|
|
+ isinstance(self.total_cost, bool)
|
|
|
|
|
+ or not isinstance(self.total_cost, (int, float))
|
|
|
|
|
+ or not isfinite(self.total_cost)
|
|
|
|
|
+ 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
|
|
|
|
|
+ ):
|
|
|
|
|
+ try:
|
|
|
|
|
+ object.__setattr__(self, "failure_code", FailureCode(self.failure_code))
|
|
|
|
|
+ except (TypeError, ValueError) as exc:
|
|
|
|
|
+ raise ValueError("ExecutionStats.failure_code is invalid") from exc
|
|
|
|
|
+
|
|
|
|
|
+ @classmethod
|
|
|
|
|
+ def from_dict(cls, data: "ExecutionStats | Dict[str, Any]") -> "ExecutionStats":
|
|
|
|
|
+ if isinstance(data, cls):
|
|
|
|
|
+ return data
|
|
|
|
|
+ failure_code = data.get("failure_code")
|
|
|
|
|
+ return cls(
|
|
|
|
|
+ primary_model=data.get("primary_model"),
|
|
|
|
|
+ total_tokens=data.get("total_tokens"),
|
|
|
|
|
+ total_cost=data.get("total_cost"),
|
|
|
|
|
+ failure_code=FailureCode(failure_code) if failure_code else None,
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
@dataclass(frozen=True)
|
|
@dataclass(frozen=True)
|
|
|
class AcceptanceCriterion:
|
|
class AcceptanceCriterion:
|
|
|
criterion_id: str
|
|
criterion_id: str
|
|
@@ -245,13 +399,22 @@ class TaskAttempt:
|
|
|
worker_preset: str
|
|
worker_preset: str
|
|
|
execution_mode: str
|
|
execution_mode: str
|
|
|
status: AttemptStatus = AttemptStatus.RUNNING
|
|
status: AttemptStatus = AttemptStatus.RUNNING
|
|
|
|
|
+ operation_id: Optional[str] = None
|
|
|
|
|
+ execution_epoch: int = 0
|
|
|
continue_from_trace_id: Optional[str] = None
|
|
continue_from_trace_id: Optional[str] = None
|
|
|
snapshot_id: Optional[str] = None
|
|
snapshot_id: Optional[str] = None
|
|
|
submission: Optional[AttemptSubmission] = None
|
|
submission: Optional[AttemptSubmission] = None
|
|
|
|
|
+ execution_stats: Optional[ExecutionStats] = None
|
|
|
error: Optional[str] = None
|
|
error: Optional[str] = None
|
|
|
|
|
+ started_at: Optional[str] = None
|
|
|
|
|
+ completed_at: Optional[str] = None
|
|
|
created_at: str = field(default_factory=utc_now)
|
|
created_at: str = field(default_factory=utc_now)
|
|
|
updated_at: str = field(default_factory=utc_now)
|
|
updated_at: str = field(default_factory=utc_now)
|
|
|
|
|
|
|
|
|
|
+ @property
|
|
|
|
|
+ def duration_ms(self) -> Optional[int]:
|
|
|
|
|
+ return _duration_ms(self.started_at, self.completed_at)
|
|
|
|
|
+
|
|
|
@classmethod
|
|
@classmethod
|
|
|
def from_dict(cls, data: Dict[str, Any]) -> "TaskAttempt":
|
|
def from_dict(cls, data: Dict[str, Any]) -> "TaskAttempt":
|
|
|
submission = data.get("submission")
|
|
submission = data.get("submission")
|
|
@@ -263,10 +426,19 @@ class TaskAttempt:
|
|
|
worker_preset=data.get("worker_preset", "worker"),
|
|
worker_preset=data.get("worker_preset", "worker"),
|
|
|
execution_mode=data.get("execution_mode", "new"),
|
|
execution_mode=data.get("execution_mode", "new"),
|
|
|
status=AttemptStatus(data.get("status", AttemptStatus.RUNNING.value)),
|
|
status=AttemptStatus(data.get("status", AttemptStatus.RUNNING.value)),
|
|
|
|
|
+ operation_id=data.get("operation_id"),
|
|
|
|
|
+ execution_epoch=int(data.get("execution_epoch", 0)),
|
|
|
continue_from_trace_id=data.get("continue_from_trace_id"),
|
|
continue_from_trace_id=data.get("continue_from_trace_id"),
|
|
|
snapshot_id=data.get("snapshot_id"),
|
|
snapshot_id=data.get("snapshot_id"),
|
|
|
submission=AttemptSubmission.from_dict(submission) if submission else None,
|
|
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
|
|
|
|
|
+ ),
|
|
|
error=data.get("error"),
|
|
error=data.get("error"),
|
|
|
|
|
+ started_at=data.get("started_at"),
|
|
|
|
|
+ completed_at=data.get("completed_at"),
|
|
|
created_at=data.get("created_at", utc_now()),
|
|
created_at=data.get("created_at", utc_now()),
|
|
|
updated_at=data.get("updated_at", utc_now()),
|
|
updated_at=data.get("updated_at", utc_now()),
|
|
|
)
|
|
)
|
|
@@ -298,7 +470,12 @@ class ValidationReport:
|
|
|
snapshot_id: str
|
|
snapshot_id: str
|
|
|
validator_trace_id: str
|
|
validator_trace_id: str
|
|
|
validator_preset: str = "validator"
|
|
validator_preset: str = "validator"
|
|
|
|
|
+ validation_plan: ValidationPlan = field(default_factory=ValidationPlan)
|
|
|
|
|
+ evidence_queries_used: int = 0
|
|
|
|
|
+ evidence_query_results: Dict[str, Dict[str, Any]] = field(default_factory=dict)
|
|
|
status: ValidationRunStatus = ValidationRunStatus.PENDING
|
|
status: ValidationRunStatus = ValidationRunStatus.PENDING
|
|
|
|
|
+ operation_id: Optional[str] = None
|
|
|
|
|
+ execution_epoch: int = 0
|
|
|
verdict: Optional[ValidationVerdict] = None
|
|
verdict: Optional[ValidationVerdict] = None
|
|
|
criterion_results: List[CriterionResult] = field(default_factory=list)
|
|
criterion_results: List[CriterionResult] = field(default_factory=list)
|
|
|
summary: str = ""
|
|
summary: str = ""
|
|
@@ -306,10 +483,17 @@ class ValidationReport:
|
|
|
unverified_claims: List[str] = field(default_factory=list)
|
|
unverified_claims: List[str] = field(default_factory=list)
|
|
|
risks: List[str] = field(default_factory=list)
|
|
risks: List[str] = field(default_factory=list)
|
|
|
recommendation: str = ""
|
|
recommendation: str = ""
|
|
|
|
|
+ execution_stats: Optional[ExecutionStats] = None
|
|
|
error: Optional[str] = None
|
|
error: Optional[str] = None
|
|
|
|
|
+ started_at: Optional[str] = None
|
|
|
|
|
+ completed_at: Optional[str] = None
|
|
|
created_at: str = field(default_factory=utc_now)
|
|
created_at: str = field(default_factory=utc_now)
|
|
|
updated_at: str = field(default_factory=utc_now)
|
|
updated_at: str = field(default_factory=utc_now)
|
|
|
|
|
|
|
|
|
|
+ @property
|
|
|
|
|
+ def duration_ms(self) -> Optional[int]:
|
|
|
|
|
+ return _duration_ms(self.started_at, self.completed_at)
|
|
|
|
|
+
|
|
|
@classmethod
|
|
@classmethod
|
|
|
def from_dict(cls, data: Dict[str, Any]) -> "ValidationReport":
|
|
def from_dict(cls, data: Dict[str, Any]) -> "ValidationReport":
|
|
|
verdict = data.get("verdict")
|
|
verdict = data.get("verdict")
|
|
@@ -321,7 +505,15 @@ class ValidationReport:
|
|
|
snapshot_id=data["snapshot_id"],
|
|
snapshot_id=data["snapshot_id"],
|
|
|
validator_trace_id=data["validator_trace_id"],
|
|
validator_trace_id=data["validator_trace_id"],
|
|
|
validator_preset=data.get("validator_preset", "validator"),
|
|
validator_preset=data.get("validator_preset", "validator"),
|
|
|
|
|
+ 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()
|
|
|
|
|
+ },
|
|
|
status=ValidationRunStatus(data.get("status", ValidationRunStatus.PENDING.value)),
|
|
status=ValidationRunStatus(data.get("status", ValidationRunStatus.PENDING.value)),
|
|
|
|
|
+ operation_id=data.get("operation_id"),
|
|
|
|
|
+ execution_epoch=int(data.get("execution_epoch", 0)),
|
|
|
verdict=ValidationVerdict(verdict) if verdict else None,
|
|
verdict=ValidationVerdict(verdict) if verdict else None,
|
|
|
criterion_results=[CriterionResult.from_dict(x) for x in data.get("criterion_results", [])],
|
|
criterion_results=[CriterionResult.from_dict(x) for x in data.get("criterion_results", [])],
|
|
|
summary=data.get("summary", ""),
|
|
summary=data.get("summary", ""),
|
|
@@ -329,7 +521,14 @@ class ValidationReport:
|
|
|
unverified_claims=list(data.get("unverified_claims", [])),
|
|
unverified_claims=list(data.get("unverified_claims", [])),
|
|
|
risks=list(data.get("risks", [])),
|
|
risks=list(data.get("risks", [])),
|
|
|
recommendation=data.get("recommendation", ""),
|
|
recommendation=data.get("recommendation", ""),
|
|
|
|
|
+ execution_stats=(
|
|
|
|
|
+ ExecutionStats.from_dict(data["execution_stats"])
|
|
|
|
|
+ if data.get("execution_stats") is not None
|
|
|
|
|
+ else None
|
|
|
|
|
+ ),
|
|
|
error=data.get("error"),
|
|
error=data.get("error"),
|
|
|
|
|
+ started_at=data.get("started_at"),
|
|
|
|
|
+ completed_at=data.get("completed_at"),
|
|
|
created_at=data.get("created_at", utc_now()),
|
|
created_at=data.get("created_at", utc_now()),
|
|
|
updated_at=data.get("updated_at", utc_now()),
|
|
updated_at=data.get("updated_at", utc_now()),
|
|
|
)
|
|
)
|
|
@@ -364,6 +563,116 @@ class PlannerDecision:
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+@dataclass
|
|
|
|
|
+class BackgroundOperation:
|
|
|
|
|
+ operation_id: str
|
|
|
|
|
+ root_trace_id: str
|
|
|
|
|
+ kind: OperationKind
|
|
|
|
|
+ request: Dict[str, Any] = field(default_factory=dict)
|
|
|
|
|
+ request_fingerprint: str = ""
|
|
|
|
|
+ status: OperationStatus = OperationStatus.PENDING
|
|
|
|
|
+ task_ids: List[str] = field(default_factory=list)
|
|
|
|
|
+ attempt_ids: List[str] = field(default_factory=list)
|
|
|
|
|
+ validation_ids: List[str] = field(default_factory=list)
|
|
|
|
|
+ result_ref: Optional[Dict[str, Any]] = None
|
|
|
|
|
+ execution_epoch: int = 0
|
|
|
|
|
+ deadline_at: Optional[str] = None
|
|
|
|
|
+ error: Optional[str] = None
|
|
|
|
|
+ started_at: Optional[str] = None
|
|
|
|
|
+ completed_at: Optional[str] = None
|
|
|
|
|
+ created_at: str = field(default_factory=utc_now)
|
|
|
|
|
+ updated_at: str = field(default_factory=utc_now)
|
|
|
|
|
+
|
|
|
|
|
+ @classmethod
|
|
|
|
|
+ def from_dict(cls, data: Dict[str, Any]) -> "BackgroundOperation":
|
|
|
|
|
+ return cls(
|
|
|
|
|
+ operation_id=data["operation_id"],
|
|
|
|
|
+ root_trace_id=data["root_trace_id"],
|
|
|
|
|
+ kind=OperationKind(data["kind"]),
|
|
|
|
|
+ request=dict(data.get("request", {})),
|
|
|
|
|
+ request_fingerprint=str(data.get("request_fingerprint", "")),
|
|
|
|
|
+ status=OperationStatus(data.get("status", OperationStatus.PENDING.value)),
|
|
|
|
|
+ task_ids=list(data.get("task_ids", [])),
|
|
|
|
|
+ attempt_ids=list(data.get("attempt_ids", [])),
|
|
|
|
|
+ validation_ids=list(data.get("validation_ids", [])),
|
|
|
|
|
+ result_ref=dict(data["result_ref"]) if data.get("result_ref") is not None else None,
|
|
|
|
|
+ execution_epoch=int(data.get("execution_epoch", 0)),
|
|
|
|
|
+ deadline_at=data.get("deadline_at"),
|
|
|
|
|
+ error=data.get("error"),
|
|
|
|
|
+ started_at=data.get("started_at"),
|
|
|
|
|
+ completed_at=data.get("completed_at"),
|
|
|
|
|
+ created_at=data.get("created_at", utc_now()),
|
|
|
|
|
+ updated_at=data.get("updated_at", utc_now()),
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+@dataclass(frozen=True)
|
|
|
|
|
+class CommandRecord:
|
|
|
|
|
+ command_id: str
|
|
|
|
|
+ operation: str
|
|
|
|
|
+ fingerprint: str
|
|
|
|
|
+ result_ref: Dict[str, Any] = field(default_factory=dict)
|
|
|
|
|
+ committed_revision: int = -1
|
|
|
|
|
+ operation_id: Optional[str] = None
|
|
|
|
|
+ created_at: str = field(default_factory=utc_now)
|
|
|
|
|
+
|
|
|
|
|
+ @classmethod
|
|
|
|
|
+ def from_dict(cls, data: Dict[str, Any]) -> "CommandRecord":
|
|
|
|
|
+ return cls(
|
|
|
|
|
+ command_id=data["command_id"],
|
|
|
|
|
+ operation=str(data.get("operation", "")),
|
|
|
|
|
+ fingerprint=str(data.get("fingerprint", "")),
|
|
|
|
|
+ result_ref=dict(data.get("result_ref", {})),
|
|
|
|
|
+ committed_revision=int(data.get("committed_revision", -1)),
|
|
|
|
|
+ operation_id=data.get("operation_id"),
|
|
|
|
|
+ created_at=data.get("created_at", utc_now()),
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+@dataclass(frozen=True)
|
|
|
|
|
+class EventDraft:
|
|
|
|
|
+ event_type: str
|
|
|
|
|
+ payload: Dict[str, Any] = field(default_factory=dict)
|
|
|
|
|
+ command_id: Optional[str] = None
|
|
|
|
|
+ operation_id: Optional[str] = None
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+@dataclass(frozen=True)
|
|
|
|
|
+class OrchestrationEvent:
|
|
|
|
|
+ schema_version: int
|
|
|
|
|
+ event_id: str
|
|
|
|
|
+ sequence: int
|
|
|
|
|
+ root_trace_id: str
|
|
|
|
|
+ ledger_revision: int
|
|
|
|
|
+ event_type: str
|
|
|
|
|
+ occurred_at: str
|
|
|
|
|
+ payload: Dict[str, Any] = field(default_factory=dict)
|
|
|
|
|
+ command_id: Optional[str] = None
|
|
|
|
|
+ operation_id: Optional[str] = None
|
|
|
|
|
+
|
|
|
|
|
+ @classmethod
|
|
|
|
|
+ def from_dict(cls, data: Dict[str, Any]) -> "OrchestrationEvent":
|
|
|
|
|
+ return cls(
|
|
|
|
|
+ schema_version=int(data.get("schema_version", 1)),
|
|
|
|
|
+ event_id=data["event_id"],
|
|
|
|
|
+ sequence=int(data["sequence"]),
|
|
|
|
|
+ root_trace_id=data["root_trace_id"],
|
|
|
|
|
+ ledger_revision=int(data["ledger_revision"]),
|
|
|
|
|
+ event_type=data["event_type"],
|
|
|
|
|
+ occurred_at=data["occurred_at"],
|
|
|
|
|
+ payload=dict(data.get("payload", {})),
|
|
|
|
|
+ command_id=data.get("command_id"),
|
|
|
|
|
+ operation_id=data.get("operation_id"),
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+@dataclass(frozen=True)
|
|
|
|
|
+class EventPage:
|
|
|
|
|
+ events: List[OrchestrationEvent]
|
|
|
|
|
+ next_cursor: Optional[str]
|
|
|
|
|
+ has_more: bool = False
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
@dataclass
|
|
@dataclass
|
|
|
class TaskLedger:
|
|
class TaskLedger:
|
|
|
root_trace_id: str
|
|
root_trace_id: str
|
|
@@ -373,13 +682,15 @@ class TaskLedger:
|
|
|
attempts: Dict[str, TaskAttempt] = field(default_factory=dict)
|
|
attempts: Dict[str, TaskAttempt] = field(default_factory=dict)
|
|
|
validations: Dict[str, ValidationReport] = field(default_factory=dict)
|
|
validations: Dict[str, ValidationReport] = field(default_factory=dict)
|
|
|
decisions: Dict[str, PlannerDecision] = field(default_factory=dict)
|
|
decisions: Dict[str, PlannerDecision] = field(default_factory=dict)
|
|
|
|
|
+ operations: Dict[str, BackgroundOperation] = field(default_factory=dict)
|
|
|
|
|
+ command_records: Dict[str, CommandRecord] = field(default_factory=dict)
|
|
|
focused_task_id: Optional[str] = None
|
|
focused_task_id: Optional[str] = None
|
|
|
idempotency_results: Dict[str, Dict[str, Any]] = field(default_factory=dict)
|
|
idempotency_results: Dict[str, Dict[str, Any]] = field(default_factory=dict)
|
|
|
created_at: str = field(default_factory=utc_now)
|
|
created_at: str = field(default_factory=utc_now)
|
|
|
updated_at: str = field(default_factory=utc_now)
|
|
updated_at: str = field(default_factory=utc_now)
|
|
|
|
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
|
- return _enum_values(asdict(self))
|
|
|
|
|
|
|
+ return json_values(asdict(self))
|
|
|
|
|
|
|
|
@classmethod
|
|
@classmethod
|
|
|
def from_dict(cls, data: Dict[str, Any]) -> "TaskLedger":
|
|
def from_dict(cls, data: Dict[str, Any]) -> "TaskLedger":
|
|
@@ -391,6 +702,8 @@ class TaskLedger:
|
|
|
attempts={k: TaskAttempt.from_dict(v) for k, v in data.get("attempts", {}).items()},
|
|
attempts={k: TaskAttempt.from_dict(v) for k, v in data.get("attempts", {}).items()},
|
|
|
validations={k: ValidationReport.from_dict(v) for k, v in data.get("validations", {}).items()},
|
|
validations={k: ValidationReport.from_dict(v) for k, v in data.get("validations", {}).items()},
|
|
|
decisions={k: PlannerDecision.from_dict(v) for k, v in data.get("decisions", {}).items()},
|
|
decisions={k: PlannerDecision.from_dict(v) for k, v in data.get("decisions", {}).items()},
|
|
|
|
|
+ operations={k: BackgroundOperation.from_dict(v) for k, v in data.get("operations", {}).items()},
|
|
|
|
|
+ command_records={k: CommandRecord.from_dict(v) for k, v in data.get("command_records", {}).items()},
|
|
|
focused_task_id=data.get("focused_task_id"),
|
|
focused_task_id=data.get("focused_task_id"),
|
|
|
idempotency_results=dict(data.get("idempotency_results", {})),
|
|
idempotency_results=dict(data.get("idempotency_results", {})),
|
|
|
created_at=data.get("created_at", utc_now()),
|
|
created_at=data.get("created_at", utc_now()),
|
|
@@ -408,7 +721,7 @@ class TaskCycleResult:
|
|
|
error: Optional[str] = None
|
|
error: Optional[str] = None
|
|
|
|
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
|
- return _enum_values(asdict(self))
|
|
|
|
|
|
|
+ return json_values(asdict(self))
|
|
|
|
|
|
|
|
@classmethod
|
|
@classmethod
|
|
|
def from_dict(cls, data: Dict[str, Any]) -> "TaskCycleResult":
|
|
def from_dict(cls, data: Dict[str, Any]) -> "TaskCycleResult":
|
|
@@ -423,21 +736,38 @@ class TaskCycleResult:
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
-def _enum_values(value: Any) -> Any:
|
|
|
|
|
|
|
+def _duration_ms(started_at: Optional[str], completed_at: Optional[str]) -> Optional[int]:
|
|
|
|
|
+ if not started_at or not completed_at:
|
|
|
|
|
+ return None
|
|
|
|
|
+ try:
|
|
|
|
|
+ started = datetime.fromisoformat(started_at.replace("Z", "+00:00"))
|
|
|
|
|
+ completed = datetime.fromisoformat(completed_at.replace("Z", "+00:00"))
|
|
|
|
|
+ if started.tzinfo is None:
|
|
|
|
|
+ started = started.replace(tzinfo=timezone.utc)
|
|
|
|
|
+ if completed.tzinfo is None:
|
|
|
|
|
+ completed = completed.replace(tzinfo=timezone.utc)
|
|
|
|
|
+ except (TypeError, ValueError):
|
|
|
|
|
+ return None
|
|
|
|
|
+ return max(0, int((completed - started).total_seconds() * 1000))
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def json_values(value: Any) -> Any:
|
|
|
if isinstance(value, Enum):
|
|
if isinstance(value, Enum):
|
|
|
return value.value
|
|
return value.value
|
|
|
if isinstance(value, dict):
|
|
if isinstance(value, dict):
|
|
|
- return {str(k): _enum_values(v) for k, v in value.items()}
|
|
|
|
|
|
|
+ return {str(k): json_values(v) for k, v in value.items()}
|
|
|
if isinstance(value, (list, tuple)):
|
|
if isinstance(value, (list, tuple)):
|
|
|
- return [_enum_values(v) for v in value]
|
|
|
|
|
|
|
+ return [json_values(v) for v in value]
|
|
|
return value
|
|
return value
|
|
|
|
|
|
|
|
|
|
|
|
|
__all__ = [
|
|
__all__ = [
|
|
|
"CompletionPolicy", "AgentRole", "TaskStatus", "AttemptStatus",
|
|
"CompletionPolicy", "AgentRole", "TaskStatus", "AttemptStatus",
|
|
|
- "ValidationRunStatus", "ValidationVerdict", "DecisionAction",
|
|
|
|
|
|
|
+ "ValidationRunStatus", "ValidationVerdict", "ValidationMode", "ValidationPlan", "DecisionAction",
|
|
|
|
|
+ "OperationKind", "OperationStatus", "FailureCode", "ExecutionStats",
|
|
|
"AcceptanceCriterion", "TaskSpec", "TaskRecord", "TaskAttempt",
|
|
"AcceptanceCriterion", "TaskSpec", "TaskRecord", "TaskAttempt",
|
|
|
"ArtifactRef", "ArtifactSnapshot", "AttemptSubmission", "CriterionResult",
|
|
"ArtifactRef", "ArtifactSnapshot", "AttemptSubmission", "CriterionResult",
|
|
|
- "ValidationReport", "PlannerDecision", "TaskLedger", "TaskCycleResult",
|
|
|
|
|
- "new_id", "utc_now",
|
|
|
|
|
|
|
+ "ValidationReport", "PlannerDecision", "BackgroundOperation", "CommandRecord",
|
|
|
|
|
+ "EventDraft", "OrchestrationEvent", "EventPage", "TaskLedger", "TaskCycleResult",
|
|
|
|
|
+ "json_values", "new_id", "utc_now",
|
|
|
]
|
|
]
|