|
@@ -1,67 +1,116 @@
|
|
|
-"""Classify Agent-visible script-build failures in one application policy."""
|
|
|
|
|
|
|
+"""Exhaustive typed Host failure policy; no string inference."""
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
from collections.abc import Mapping
|
|
from collections.abc import Mapping
|
|
|
|
|
+from dataclasses import dataclass
|
|
|
from typing import Any
|
|
from typing import Any
|
|
|
|
|
|
|
|
from agent import FailureDetail, FailureDisposition
|
|
from agent import FailureDetail, FailureDisposition
|
|
|
|
|
|
|
|
from script_build_host.domain.errors import ScriptBuildError
|
|
from script_build_host.domain.errors import ScriptBuildError
|
|
|
|
|
|
|
|
-_REPLAN_CODES = frozenset(
|
|
|
|
|
- {
|
|
|
|
|
- "INPUT_SCOPE_MISMATCH",
|
|
|
|
|
- "CHILD_DECISION_INVALID",
|
|
|
|
|
- "PHASE_POLICY_VIOLATION",
|
|
|
|
|
- "PHASE_TWO_BOUNDARY_NOT_READY",
|
|
|
|
|
- "PHASE_THREE_BOUNDARY_NOT_READY",
|
|
|
|
|
- "PROTOCOL_VIOLATION",
|
|
|
|
|
- "TASK_CHILD_BLOCKED",
|
|
|
|
|
- "TASK_BUDGET_EXCEEDED",
|
|
|
|
|
- "TASK_KIND_PRESET_MISMATCH",
|
|
|
|
|
- "TASK_STATE_CONFLICT",
|
|
|
|
|
- "ATTEMPT_WORKSPACE_FROZEN",
|
|
|
|
|
- "CONTEXT_HANDLE_UNKNOWN",
|
|
|
|
|
- "WRITE_SCOPE_VIOLATION",
|
|
|
|
|
- "WRITE_SCOPE_CONFLICT",
|
|
|
|
|
- "SUPERSESSION_CYCLE",
|
|
|
|
|
- }
|
|
|
|
|
-)
|
|
|
|
|
-_REPAIR_CODES = frozenset(
|
|
|
|
|
|
|
+
|
|
|
|
|
+@dataclass(frozen=True, slots=True)
|
|
|
|
|
+class FailurePolicyCatalog:
|
|
|
|
|
+ dispositions: Mapping[str, FailureDisposition]
|
|
|
|
|
+ tool_overrides: Mapping[tuple[str, str], FailureDisposition]
|
|
|
|
|
+
|
|
|
|
|
+ def disposition_for(
|
|
|
|
|
+ self, code: str, *, source_tool: str
|
|
|
|
|
+ ) -> FailureDisposition | None:
|
|
|
|
|
+ return self.tool_overrides.get((code, source_tool), self.dispositions.get(code))
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+_REPLAN = {
|
|
|
|
|
+ "ACCEPTED_INPUT_CONFLICT",
|
|
|
|
|
+ "ADOPTION_INVALID",
|
|
|
|
|
+ "ATTEMPT_WORKSPACE_FROZEN",
|
|
|
|
|
+ "CHILD_DECISION_INVALID",
|
|
|
|
|
+ "GOAL_COVERAGE_INCOMPLETE",
|
|
|
|
|
+ "GOAL_COVERAGE_INVALID",
|
|
|
|
|
+ "GOAL_SCOPE_INVALID",
|
|
|
|
|
+ "INPUT_SCOPE_MISMATCH",
|
|
|
|
|
+ "PHASE_POLICY_VIOLATION",
|
|
|
|
|
+ "PHASE_TWO_BOUNDARY_NOT_READY",
|
|
|
|
|
+ "PHASE_THREE_BOUNDARY_NOT_READY",
|
|
|
|
|
+ "SUPERSESSION_CYCLE",
|
|
|
|
|
+ "TASK_BUDGET_EXCEEDED",
|
|
|
|
|
+ "TASK_CAPABILITY_MISMATCH",
|
|
|
|
|
+ "TASK_CHILD_BLOCKED",
|
|
|
|
|
+ "TASK_CONTRACT_DUPLICATE",
|
|
|
|
|
+ "TASK_CONTRACT_INVALID",
|
|
|
|
|
+ "TASK_CONTRACT_NOT_EXECUTABLE",
|
|
|
|
|
+ "TASK_DECISION_CONFLICT",
|
|
|
|
|
+ "TASK_KIND_PRESET_MISMATCH",
|
|
|
|
|
+ "TASK_PARENT_AMBIGUOUS",
|
|
|
|
|
+ "TASK_TARGET_AMBIGUOUS",
|
|
|
|
|
+ "WRITE_SCOPE_CONFLICT",
|
|
|
|
|
+ "WRITE_SCOPE_VIOLATION",
|
|
|
|
|
+ "WORKSPACE_SEED_MISSING",
|
|
|
|
|
+}
|
|
|
|
|
+_REPAIR = {
|
|
|
|
|
+ "ARTIFACT_NOT_FOUND",
|
|
|
|
|
+ "LEGACY_ARTIFACT_NOT_FOUND",
|
|
|
|
|
+ "LEGACY_REFERENCE_INVALID",
|
|
|
|
|
+ "LEGACY_WRITE_INVALID",
|
|
|
|
|
+}
|
|
|
|
|
+_RETRY = {
|
|
|
|
|
+ "CONTEXT_CURSOR_INVALID",
|
|
|
|
|
+ "CONTEXT_CURSOR_OUT_OF_SEQUENCE",
|
|
|
|
|
+ "CONTEXT_CURSOR_SCOPE_MISMATCH",
|
|
|
|
|
+ "CONTEXT_HANDLE_FORMAT_INVALID",
|
|
|
|
|
+ "CONTEXT_HANDLE_INVALID",
|
|
|
|
|
+ "CONTEXT_HANDLE_UNAUTHORIZED",
|
|
|
|
|
+ "INVALID_TOOL_ARGUMENT",
|
|
|
|
|
+ "PUBLICATION_DEADLOCK_RETRYABLE",
|
|
|
|
|
+ "PUBLICATION_LOCK_TIMEOUT",
|
|
|
|
|
+ "RATE_LIMITED",
|
|
|
|
|
+ "RETRIEVAL_RESERVATION_BUSY",
|
|
|
|
|
+ "STALE_BASE_REVISION",
|
|
|
|
|
+ "STALE_WORKBENCH_STATE",
|
|
|
|
|
+ "TRANSIENT_CONNECTION_LOST",
|
|
|
|
|
+ "TRANSIENT_TIMEOUT",
|
|
|
|
|
+ "UPSTREAM_UNAVAILABLE",
|
|
|
|
|
+ "VALIDATION_EVIDENCE_INVALID",
|
|
|
|
|
+ "VALIDATION_EVIDENCE_UNAUTHORIZED",
|
|
|
|
|
+}
|
|
|
|
|
+_ABORT = {
|
|
|
|
|
+ "ARTIFACT_DIGEST_MISMATCH",
|
|
|
|
|
+ "ARTIFACT_OWNERSHIP_MISMATCH",
|
|
|
|
|
+ "BUILD_NOT_FOUND",
|
|
|
|
|
+ "CONTEXT_BUDGET_EXCEEDED",
|
|
|
|
|
+ "HOST_CONTRACT_INVARIANT_BROKEN",
|
|
|
|
|
+ "IDEMPOTENCY_KEY_CONFLICT",
|
|
|
|
|
+ "INPUT_DIGEST_MISMATCH",
|
|
|
|
|
+ "INTERNAL_UNCLASSIFIED_FAILURE",
|
|
|
|
|
+ "INVALID_CONFIGURATION",
|
|
|
|
|
+ "MISSION_ALREADY_OWNED",
|
|
|
|
|
+ "MISSION_FENCING_TOKEN_STALE",
|
|
|
|
|
+ "MISSION_STOP_REQUESTED",
|
|
|
|
|
+ "PROTOCOL_VIOLATION",
|
|
|
|
|
+ "PUBLICATION_READBACK_MISMATCH",
|
|
|
|
|
+ "REQUIRED_EVIDENCE_UNSATISFIED",
|
|
|
|
|
+ "TASK_CONTRACT_NOT_FOUND",
|
|
|
|
|
+ "UNSAFE_OUTBOUND_TARGET",
|
|
|
|
|
+ "UPSTREAM_AUTH_REJECTED",
|
|
|
|
|
+ "VALIDATION_EVIDENCE_CONFLICT",
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+FAILURE_POLICY_CATALOG = FailurePolicyCatalog(
|
|
|
{
|
|
{
|
|
|
- "LEGACY_WRITE_INVALID",
|
|
|
|
|
- "LEGACY_REFERENCE_INVALID",
|
|
|
|
|
- "ARTIFACT_NOT_FOUND",
|
|
|
|
|
- }
|
|
|
|
|
-)
|
|
|
|
|
-_RETRY_CODES = frozenset(
|
|
|
|
|
|
|
+ **{code: FailureDisposition.REPLAN_TASK for code in _REPLAN},
|
|
|
|
|
+ **{code: FailureDisposition.REPAIR_ATTEMPT for code in _REPAIR},
|
|
|
|
|
+ **{code: FailureDisposition.RETRY_CALL for code in _RETRY},
|
|
|
|
|
+ **{code: FailureDisposition.ABORT_RUN for code in _ABORT},
|
|
|
|
|
+ "SCRIPT_CONTENT_INCOMPLETE": FailureDisposition.REPLAN_TASK,
|
|
|
|
|
+ },
|
|
|
{
|
|
{
|
|
|
- "PUBLICATION_LOCK_TIMEOUT",
|
|
|
|
|
- "PUBLICATION_DEADLOCK_RETRYABLE",
|
|
|
|
|
- "STALE_BASE_REVISION",
|
|
|
|
|
- "STALE_WORKBENCH_STATE",
|
|
|
|
|
- "VALIDATION_EVIDENCE_INVALID",
|
|
|
|
|
- "CONTEXT_NOT_EXHAUSTED",
|
|
|
|
|
- "CONTEXT_HANDLE_INVALID",
|
|
|
|
|
- "CONTEXT_HANDLE_FORMAT_INVALID",
|
|
|
|
|
- "CONTEXT_HANDLE_UNAUTHORIZED",
|
|
|
|
|
- "CONTEXT_CURSOR_INVALID",
|
|
|
|
|
- "TRANSIENT_TIMEOUT",
|
|
|
|
|
- "TRANSIENT_CONNECTION_LOST",
|
|
|
|
|
- "RATE_LIMITED",
|
|
|
|
|
- "UPSTREAM_UNAVAILABLE",
|
|
|
|
|
- }
|
|
|
|
|
-)
|
|
|
|
|
-_ABORT_MARKERS = (
|
|
|
|
|
- "STOP",
|
|
|
|
|
- "FENCING",
|
|
|
|
|
- "OWNERSHIP",
|
|
|
|
|
- "ALREADY_OWNED",
|
|
|
|
|
- "DIGEST",
|
|
|
|
|
- "RECOVERY",
|
|
|
|
|
- "MIGRATION",
|
|
|
|
|
- "RECONCILIATION",
|
|
|
|
|
|
|
+ ("SCRIPT_CONTENT_INCOMPLETE", "submit_attempt"):
|
|
|
|
|
+ FailureDisposition.REPAIR_ATTEMPT,
|
|
|
|
|
+ ("SCRIPT_CONTENT_INCOMPLETE", "save_paragraph_candidate"):
|
|
|
|
|
+ FailureDisposition.REPAIR_ATTEMPT,
|
|
|
|
|
+ },
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@@ -71,56 +120,41 @@ def classify_script_failure(
|
|
|
source_tool: str,
|
|
source_tool: str,
|
|
|
context: Mapping[str, Any] | None = None,
|
|
context: Mapping[str, Any] | None = None,
|
|
|
) -> FailureDetail:
|
|
) -> FailureDetail:
|
|
|
- """Return the sole Host classification for an Agent-visible failure."""
|
|
|
|
|
-
|
|
|
|
|
if isinstance(error, ValueError) and not isinstance(error, ScriptBuildError):
|
|
if isinstance(error, ValueError) and not isinstance(error, ScriptBuildError):
|
|
|
return FailureDetail(
|
|
return FailureDetail(
|
|
|
- code="INVALID_TOOL_ARGUMENT",
|
|
|
|
|
- message=str(error),
|
|
|
|
|
- disposition=FailureDisposition.RETRY_CALL,
|
|
|
|
|
|
|
+ "INVALID_TOOL_ARGUMENT",
|
|
|
|
|
+ str(error),
|
|
|
|
|
+ FailureDisposition.RETRY_CALL,
|
|
|
source_tool=source_tool,
|
|
source_tool=source_tool,
|
|
|
- details=_failure_details(error, context),
|
|
|
|
|
|
|
+ details=_details(error, context),
|
|
|
)
|
|
)
|
|
|
-
|
|
|
|
|
code = error.code
|
|
code = error.code
|
|
|
- if code == "SCRIPT_CONTENT_INCOMPLETE":
|
|
|
|
|
- disposition = (
|
|
|
|
|
- FailureDisposition.REPAIR_ATTEMPT
|
|
|
|
|
- if source_tool in {"submit_attempt", "save_paragraph_candidate"}
|
|
|
|
|
- else FailureDisposition.REPLAN_TASK
|
|
|
|
|
- )
|
|
|
|
|
- elif (
|
|
|
|
|
- code in _REPLAN_CODES
|
|
|
|
|
- or code.startswith("GOAL_")
|
|
|
|
|
- or code.startswith("TASK_CAPABILITY_")
|
|
|
|
|
- or code.startswith("TASK_CONTRACT_")
|
|
|
|
|
- or code.startswith("ADOPTION_")
|
|
|
|
|
- ):
|
|
|
|
|
- disposition = FailureDisposition.REPLAN_TASK
|
|
|
|
|
- elif code in _REPAIR_CODES:
|
|
|
|
|
- disposition = FailureDisposition.REPAIR_ATTEMPT
|
|
|
|
|
- elif code in _RETRY_CODES or code.startswith("CONTEXT_CURSOR_"):
|
|
|
|
|
- disposition = FailureDisposition.RETRY_CALL
|
|
|
|
|
- elif any(marker in code for marker in _ABORT_MARKERS):
|
|
|
|
|
- disposition = FailureDisposition.ABORT_RUN
|
|
|
|
|
- else:
|
|
|
|
|
|
|
+ disposition = FAILURE_POLICY_CATALOG.disposition_for(
|
|
|
|
|
+ code, source_tool=source_tool
|
|
|
|
|
+ )
|
|
|
|
|
+ details = _details(error, context)
|
|
|
|
|
+ message = error.summary
|
|
|
|
|
+ if disposition is None:
|
|
|
|
|
+ details["observed_code"] = code
|
|
|
|
|
+ code = "INTERNAL_UNCLASSIFIED_FAILURE"
|
|
|
|
|
+ message = "an unclassified internal failure terminated the script build"
|
|
|
disposition = FailureDisposition.ABORT_RUN
|
|
disposition = FailureDisposition.ABORT_RUN
|
|
|
return FailureDetail(
|
|
return FailureDetail(
|
|
|
- code=code,
|
|
|
|
|
- message=error.summary,
|
|
|
|
|
- disposition=disposition,
|
|
|
|
|
|
|
+ code,
|
|
|
|
|
+ message,
|
|
|
|
|
+ disposition,
|
|
|
source_tool=source_tool,
|
|
source_tool=source_tool,
|
|
|
- details=_failure_details(error, context),
|
|
|
|
|
|
|
+ details=details,
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
-def _failure_details(
|
|
|
|
|
|
|
+def _details(
|
|
|
error: ScriptBuildError | ValueError,
|
|
error: ScriptBuildError | ValueError,
|
|
|
context: Mapping[str, Any] | None,
|
|
context: Mapping[str, Any] | None,
|
|
|
) -> dict[str, Any]:
|
|
) -> dict[str, Any]:
|
|
|
raw = getattr(error, "details", None)
|
|
raw = getattr(error, "details", None)
|
|
|
details = dict(raw) if isinstance(raw, Mapping) else {}
|
|
details = dict(raw) if isinstance(raw, Mapping) else {}
|
|
|
- if isinstance(context, Mapping):
|
|
|
|
|
|
|
+ if context:
|
|
|
details.update(
|
|
details.update(
|
|
|
{
|
|
{
|
|
|
key: str(context[key])
|
|
key: str(context[key])
|
|
@@ -131,4 +165,8 @@ def _failure_details(
|
|
|
return details
|
|
return details
|
|
|
|
|
|
|
|
|
|
|
|
|
-__all__ = ["classify_script_failure"]
|
|
|
|
|
|
|
+__all__ = [
|
|
|
|
|
+ "FAILURE_POLICY_CATALOG",
|
|
|
|
|
+ "FailurePolicyCatalog",
|
|
|
|
|
+ "classify_script_failure",
|
|
|
|
|
+]
|