1248 lines
77 KiB
Python
1248 lines
77 KiB
Python
"""Action selection, candidate, review, and completion command handlers."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from hashlib import sha256
|
|
import json
|
|
import secrets
|
|
from typing import Any
|
|
|
|
from app.cad_agent.application.llm_contracts import (
|
|
CandidateReview,
|
|
FinalReview,
|
|
GeometryConclusion,
|
|
NextAction,
|
|
RollbackCheckpoint,
|
|
)
|
|
from app.cad_agent.application.results import Accepted, Rejected
|
|
from app.cad_agent.domain.errors import ErrorCode, WorkflowError
|
|
from app.cad_agent.domain.operation_contract import canonical_hash, validate_fragment
|
|
from app.cad_agent.domain.state import PendingAction, TaskPhase, TaskState, reject_stale_head, transition
|
|
from app.cad_agent.ports import ArtifactStore, CadRuntime, TaskRepository, VerifierExecutor
|
|
|
|
|
|
class ActionCommandHandler:
|
|
def __init__(self, repository: TaskRepository, artifacts: ArtifactStore, runtime: CadRuntime, verifiers: VerifierExecutor) -> None:
|
|
self.repository = repository
|
|
self.artifacts = artifacts
|
|
self.runtime = runtime
|
|
self.verifiers = verifiers
|
|
|
|
def available_atomic_ids(self, task_id: str, state: TaskState | None = None) -> tuple[str, ...]:
|
|
"""Return operations whose mandatory snapshot inputs exist now."""
|
|
state = state or self.repository.get_state(task_id)
|
|
topology = self.artifacts.read_topology(task_id, state.active_revision) if state is not None else None
|
|
selector_tokens = self.runtime.selector_tokens(topology)
|
|
active_cdsl = self.artifacts.read_active_cdsl(task_id, state.active_revision) if state is not None else None
|
|
reference_tokens = self.runtime.reference_tokens(active_cdsl)
|
|
has_active_solid = isinstance(active_cdsl, dict) and bool(active_cdsl.get("features"))
|
|
available: list[str] = []
|
|
for atomic_id in self.runtime.supported_atomic_ids():
|
|
contract = self.runtime.operation_contract(atomic_id)
|
|
shape = contract.get("fragment_shape") if isinstance(contract.get("fragment_shape"), dict) else {}
|
|
if str(shape.get("selector_tokens") or "forbidden") == "required":
|
|
policy = contract.get("selector_policy") if isinstance(contract.get("selector_policy"), dict) else {}
|
|
required_kind = str(policy.get("token_kind") or "")
|
|
if not any(token.get("kind") == required_kind for token in selector_tokens.values() if isinstance(token, dict)):
|
|
continue
|
|
reference_policy = contract.get("reference_policy") if isinstance(contract.get("reference_policy"), dict) else {}
|
|
if reference_policy.get("mode") == "snapshot_bound":
|
|
minimum = int(reference_policy.get("min_items") or 1)
|
|
if len(reference_tokens) < minimum:
|
|
continue
|
|
if "requires_active_solid" in (contract.get("semantic_preflight") or ()) and not has_active_solid:
|
|
continue
|
|
available.append(atomic_id)
|
|
return tuple(available)
|
|
|
|
def propose_next_action(self, task_id: str, proposal: NextAction, *, invocation_id: str) -> Accepted | Rejected:
|
|
state = self.repository.get_state(task_id)
|
|
if state is None or state.phase != TaskPhase.AWAITING_ACTION or state.pending_action is not None:
|
|
return Rejected(WorkflowError(ErrorCode.AUTHOR_DECISION_REJECTED, "A next action is not allowed in the current workflow state."))
|
|
if not self.repair_action_ready(task_id, state):
|
|
return Rejected(WorkflowError(ErrorCode.AUTHOR_DECISION_REJECTED, "Record a current geometry conclusion before selecting a repair action."))
|
|
stale = reject_stale_head(state, proposal.working_head)
|
|
if stale:
|
|
return Rejected(stale)
|
|
contract = self._requirements_contract(task_id, state)
|
|
if not isinstance(contract, dict):
|
|
return Rejected(WorkflowError(ErrorCode.AUTHOR_DECISION_REJECTED, "Requirements contract is not frozen."))
|
|
requirement_ids = {str(item.get("requirement_id") or "") for item in contract.get("requirements") or () if isinstance(item, dict)}
|
|
if not set(proposal.requirement_ids).issubset(requirement_ids):
|
|
return Rejected(WorkflowError(ErrorCode.AUTHOR_FORMAT_INVALID, "Action references a requirement outside the frozen contract."))
|
|
if proposal.atomic_id not in self.runtime.supported_atomic_ids():
|
|
return Rejected(WorkflowError(ErrorCode.RUNTIME_CONTRACT_INVALID, "Action selects an operation absent from the verified runtime registry."))
|
|
if proposal.atomic_id not in self.available_atomic_ids(task_id, state):
|
|
return Rejected(WorkflowError(
|
|
ErrorCode.AUTHOR_DECISION_REJECTED,
|
|
"Action requires selector or feature-reference facts unavailable from the current geometry snapshot.",
|
|
))
|
|
operation = self.runtime.operation_contract(proposal.atomic_id)
|
|
key = self._key(task_id, "next_action", state.working_head, proposal.model_dump(mode="json"))
|
|
invocation = self.repository.begin_invocation(task_id, invocation_id, key)
|
|
if invocation.status == "finished" and invocation.result is not None:
|
|
return Accepted(invocation.result)
|
|
action = PendingAction(
|
|
action_id=f"act_{state.version + 1:03d}", working_head=state.working_head, intent=proposal.intent,
|
|
requirement_ids=tuple(proposal.requirement_ids), atomic_id=proposal.atomic_id, expected_change=proposal.expected_change,
|
|
contract_hash=str(operation["contract_hash"]), idempotency_key=key,
|
|
)
|
|
next_state = transition(state, "action_proposed", pending_action=action)
|
|
payload = {"action_id": action.action_id, "working_head": action.working_head, "atomic_id": action.atomic_id, "contract_hash": action.contract_hash}
|
|
if not self._commit_invocation(next_state, [{"event": "proposed", **payload, "intent": action.intent, "requirement_ids": list(action.requirement_ids), "expected_change": action.expected_change}], invocation, payload):
|
|
return Rejected(self._stale())
|
|
return Accepted(payload)
|
|
|
|
def diagnostic_evidence_refs(self, task_id: str, state: TaskState | None = None) -> tuple[str, ...]:
|
|
"""Return the server-generated evidence references for a repair turn.
|
|
|
|
These are opaque names for current facts and committed audit evidence,
|
|
never model-provided paths or feature IDs. The command handler checks
|
|
them again so a schema from an earlier turn cannot authorize a write.
|
|
"""
|
|
state = state or self.repository.get_state(task_id)
|
|
if state is None:
|
|
return ()
|
|
refs = ["evidence_current_state"]
|
|
if state.active_revision:
|
|
refs.extend(["evidence_current_model", "evidence_current_claims"])
|
|
for event in self.repository.ledger_events(task_id)[-16:]:
|
|
if event.get("event") in {
|
|
"candidate_rejected", "candidate_build_failed", "candidate_recovery_failed",
|
|
"candidate_recovered_rejected", "candidate_operation_skipped", "final_review_repair",
|
|
} or (event.get("event") == "accepted" and event.get("repair_required")):
|
|
sequence = event.get("sequence")
|
|
if isinstance(sequence, int):
|
|
refs.append(f"evidence_ledger_{sequence}")
|
|
return tuple(dict.fromkeys(refs))
|
|
|
|
def repair_diagnostics(self, task_id: str, state: TaskState | None = None) -> list[dict[str, Any]]:
|
|
"""Return bounded, server-measured evidence from failed candidates.
|
|
|
|
A repair turn runs against the last accepted checkpoint, while its
|
|
useful facts often live in an immutable rejected candidate stage. An
|
|
opaque ledger reference alone forces the author to rediscover those
|
|
facts or repeat a failed construction. This projection deliberately
|
|
exposes only compact build/verification outcomes, never the rejected
|
|
fragment or a server-selected next operation.
|
|
"""
|
|
state = state or self.repository.get_state(task_id)
|
|
if state is None or not state.repair_required:
|
|
return []
|
|
relevant = {
|
|
"candidate_rejected",
|
|
"candidate_build_failed",
|
|
"candidate_recovery_failed",
|
|
"candidate_recovered_rejected",
|
|
"candidate_operation_skipped",
|
|
"runtime_precondition_rejected",
|
|
"final_review_repair",
|
|
"accepted",
|
|
}
|
|
diagnostics: list[dict[str, Any]] = []
|
|
for event in reversed(self.repository.ledger_events(task_id)):
|
|
if event.get("event") not in relevant:
|
|
continue
|
|
sequence = event.get("sequence")
|
|
item: dict[str, Any] = {
|
|
"event": str(event.get("event") or ""),
|
|
"evidence_ref": f"evidence_ledger_{sequence}" if isinstance(sequence, int) else "evidence_current_state",
|
|
}
|
|
for field in ("candidate_id", "action_id"):
|
|
value = event.get(field)
|
|
if isinstance(value, str) and value:
|
|
item[field] = value
|
|
message = event.get("message")
|
|
if isinstance(message, str) and message:
|
|
item["message"] = message[:500]
|
|
for field in ("issues", "failed_checklist_items"):
|
|
values = event.get(field)
|
|
if isinstance(values, list):
|
|
item[field] = [str(value)[:500] for value in values[:8] if isinstance(value, str)]
|
|
failures = event.get("operation_failures")
|
|
if isinstance(failures, list):
|
|
item["operation_failures"] = [
|
|
{key: str(value)[:500] for key, value in failure.items() if key in {"feature_id", "message"}}
|
|
for failure in failures[:8]
|
|
if isinstance(failure, dict)
|
|
]
|
|
fragment_hash = event.get("fragment_hash")
|
|
if isinstance(fragment_hash, str) and fragment_hash:
|
|
item["fragment_hash"] = fragment_hash
|
|
stage_id = event.get("stage_id")
|
|
candidate = None
|
|
if isinstance(stage_id, str) and stage_id:
|
|
try:
|
|
candidate = self.artifacts.read_stage_json(task_id, stage_id, "candidate.json")
|
|
except (OSError, ValueError, json.JSONDecodeError):
|
|
# The ledger remains authoritative if an abandoned stage
|
|
# is unavailable; diagnostic projection must not turn an
|
|
# existing repair into a new service failure.
|
|
candidate = None
|
|
if isinstance(candidate, dict):
|
|
atomic_id = candidate.get("actual_atomic_id")
|
|
if isinstance(atomic_id, str) and atomic_id:
|
|
item["atomic_id"] = atomic_id
|
|
health = candidate.get("health") if isinstance(candidate.get("health"), dict) else {}
|
|
bbox = health.get("bbox_mm") if isinstance(health.get("bbox_mm"), dict) else {}
|
|
measured = {
|
|
key: health[key]
|
|
for key in ("solid_count", "feature_count", "volume_mm3")
|
|
if isinstance(health.get(key), (int, float))
|
|
}
|
|
if isinstance(bbox.get("dimensions"), list):
|
|
measured["bbox_dimensions_mm"] = bbox["dimensions"][:3]
|
|
if measured:
|
|
item["measured"] = measured
|
|
claim_results = candidate.get("claim_results")
|
|
if isinstance(claim_results, list):
|
|
failures: list[dict[str, Any]] = []
|
|
for claim in claim_results:
|
|
if not isinstance(claim, dict) or claim.get("status") not in {"fail", "unavailable"}:
|
|
continue
|
|
evidence = claim.get("evidence") if isinstance(claim.get("evidence"), dict) else {}
|
|
failures.append({
|
|
"claim_id": str(claim.get("claim_id") or ""),
|
|
"claim_kind": str(claim.get("claim_kind") or ""),
|
|
"status": str(claim.get("status") or ""),
|
|
"evidence": evidence,
|
|
})
|
|
if failures:
|
|
item["failed_claims"] = failures[:8]
|
|
operation_results = candidate.get("operation_verifier_results")
|
|
if isinstance(operation_results, list):
|
|
operation_blockers: list[dict[str, Any]] = []
|
|
for result in operation_results:
|
|
if not isinstance(result, dict) or result.get("status") == "pass":
|
|
continue
|
|
evidence = result.get("evidence") if isinstance(result.get("evidence"), dict) else {}
|
|
operation_blockers.append({
|
|
"claim_id": str(result.get("claim_id") or ""),
|
|
"claim_kind": str(result.get("claim_kind") or ""),
|
|
"status": str(result.get("status") or ""),
|
|
"evidence": evidence,
|
|
})
|
|
if operation_blockers:
|
|
# An operation may be rejected because it produced no
|
|
# net change. That result is often pending relative
|
|
# to the full requirements contract, but it is still
|
|
# a definite blocker for this specific action.
|
|
item["operation_blockers"] = operation_blockers[:8]
|
|
review = None
|
|
if isinstance(stage_id, str) and stage_id:
|
|
try:
|
|
review = self.artifacts.read_stage_json(task_id, stage_id, "candidate-review.json")
|
|
except (OSError, ValueError, json.JSONDecodeError):
|
|
review = None
|
|
if isinstance(review, dict):
|
|
evidence = review.get("evidence")
|
|
issues = review.get("issues")
|
|
if isinstance(evidence, list):
|
|
item["review_evidence"] = [str(value)[:500] for value in evidence[:8] if isinstance(value, str)]
|
|
if isinstance(issues, list):
|
|
item["review_issues"] = [str(value)[:500] for value in issues[:8] if isinstance(value, str)]
|
|
diagnostics.append(item)
|
|
if len(diagnostics) == 3:
|
|
break
|
|
return diagnostics
|
|
|
|
def checkpoint_tokens(self, task_id: str, state: TaskState | None = None) -> dict[str, str]:
|
|
"""Map opaque rollback tokens to the current linear checkpoint lineage."""
|
|
state = state or self.repository.get_state(task_id)
|
|
if state is None:
|
|
return {}
|
|
accepted = [
|
|
event for event in self.repository.ledger_events(task_id)
|
|
if event.get("event") == "accepted" and isinstance(event.get("revision_id"), str)
|
|
]
|
|
by_revision = {str(event["revision_id"]): event for event in accepted}
|
|
if state.active_revision and state.active_revision not in by_revision:
|
|
return {}
|
|
lineage: list[str] = []
|
|
cursor = state.active_revision
|
|
while cursor:
|
|
event = by_revision.get(cursor)
|
|
if event is None or cursor in lineage:
|
|
return {}
|
|
lineage.append(cursor)
|
|
parent = event.get("parent_revision")
|
|
if isinstance(parent, str):
|
|
cursor = parent
|
|
continue
|
|
# v3 events written before parent_revision existed were linear.
|
|
position = accepted.index(event)
|
|
cursor = str(accepted[position - 1]["revision_id"]) if position else ""
|
|
lineage.reverse()
|
|
return {"checkpoint_root": "", **{f"checkpoint_{revision}": revision for revision in lineage}}
|
|
|
|
def rollback_available(self, task_id: str, state: TaskState | None = None) -> bool:
|
|
state = state or self.repository.get_state(task_id)
|
|
if state is None or state.phase != TaskPhase.AWAITING_ACTION or state.pending_action is not None:
|
|
return False
|
|
has_earlier_checkpoint = any(
|
|
revision != state.active_revision
|
|
for revision in self.checkpoint_tokens(task_id, state).values()
|
|
)
|
|
return has_earlier_checkpoint and any(
|
|
event.get("event") == "geometry_conclusion"
|
|
and event.get("decision") == "rollback"
|
|
and event.get("working_head") == state.working_head
|
|
for event in reversed(self.repository.ledger_events(task_id))
|
|
)
|
|
|
|
def repair_action_ready(self, task_id: str, state: TaskState | None = None) -> bool:
|
|
"""Require a structured diagnosis before repairing failed geometry.
|
|
|
|
A rollback is itself a resolved repair decision. For a new feature,
|
|
the author must instead record a return-to-action-selection conclusion
|
|
after the latest failed candidate. The ordering matters: a diagnosis
|
|
used to select a prior repair action cannot authorize retries after
|
|
that new action fails on the same checkpoint.
|
|
"""
|
|
state = state or self.repository.get_state(task_id)
|
|
if state is None or not state.repair_required:
|
|
return True
|
|
repair_triggers = {
|
|
"candidate_rejected",
|
|
"candidate_build_failed",
|
|
"candidate_recovery_failed",
|
|
"candidate_recovered_rejected",
|
|
"final_review_repair",
|
|
}
|
|
for event in reversed(self.repository.ledger_events(task_id)):
|
|
event_name = event.get("event")
|
|
if event_name == "rollback":
|
|
return True
|
|
if event_name == "geometry_conclusion" and event.get("decision") == "return_to_action_selection":
|
|
return True
|
|
if event_name in repair_triggers:
|
|
return False
|
|
return False
|
|
|
|
def record_geometry_conclusion(self, task_id: str, conclusion: GeometryConclusion, *, invocation_id: str) -> Accepted | Rejected:
|
|
state = self.repository.get_state(task_id)
|
|
if state is None or state.phase not in {TaskPhase.ACTION_PENDING, TaskPhase.AWAITING_ACTION}:
|
|
return Rejected(WorkflowError(ErrorCode.AUTHOR_DECISION_REJECTED, "Geometry diagnosis is only available while repairing or selecting an action."))
|
|
stale = reject_stale_head(state, conclusion.working_head)
|
|
if stale:
|
|
return Rejected(stale)
|
|
available = set(self.diagnostic_evidence_refs(task_id, state))
|
|
if not set(conclusion.evidence_refs).issubset(available):
|
|
return Rejected(WorkflowError(ErrorCode.AUTHOR_FORMAT_INVALID, "Geometry conclusion references evidence outside the current server snapshot."))
|
|
if conclusion.decision == "rollback" and state.phase != TaskPhase.AWAITING_ACTION:
|
|
return Rejected(WorkflowError(ErrorCode.AUTHOR_DECISION_REJECTED, "Return to action selection before requesting a checkpoint rollback."))
|
|
key = self._key(task_id, "geometry_conclusion", state.working_head, conclusion.model_dump(mode="json"))
|
|
invocation = self.repository.begin_invocation(task_id, invocation_id, key)
|
|
if invocation.status == "finished" and invocation.result is not None:
|
|
return Accepted(invocation.result)
|
|
if conclusion.decision == "return_to_action_selection" and state.phase == TaskPhase.ACTION_PENDING:
|
|
next_state = transition(state, "diagnosis_return_to_action_selection", repair_required=True)
|
|
else:
|
|
next_state = transition(state, "diagnosis_recorded", repair_required=True)
|
|
event = {
|
|
"event": "geometry_conclusion",
|
|
"working_head_before": state.working_head,
|
|
"working_head": next_state.working_head,
|
|
"evidence_refs": list(conclusion.evidence_refs),
|
|
"root_cause": conclusion.root_cause,
|
|
"decision": conclusion.decision,
|
|
"corrective_intent": conclusion.corrective_intent or "",
|
|
}
|
|
result = {"decision": conclusion.decision, "phase": next_state.phase.value, "working_head": next_state.working_head}
|
|
if not self._commit_invocation(next_state, [event], invocation, result):
|
|
return Rejected(self._stale())
|
|
return Accepted(result)
|
|
|
|
def rollback_checkpoint(self, task_id: str, rollback: RollbackCheckpoint, *, invocation_id: str) -> Accepted | Rejected:
|
|
state = self.repository.get_state(task_id)
|
|
if state is None or state.phase != TaskPhase.AWAITING_ACTION or state.pending_action is not None:
|
|
return Rejected(WorkflowError(ErrorCode.AUTHOR_DECISION_REJECTED, "Checkpoint rollback requires an idle action-selection state."))
|
|
stale = reject_stale_head(state, rollback.working_head)
|
|
if stale:
|
|
return Rejected(stale)
|
|
if not self.rollback_available(task_id, state):
|
|
return Rejected(WorkflowError(ErrorCode.AUTHOR_DECISION_REJECTED, "Record a current geometry conclusion with decision=rollback before rolling back."))
|
|
target = self.checkpoint_tokens(task_id, state).get(rollback.checkpoint_token)
|
|
if target is None:
|
|
return Rejected(WorkflowError(ErrorCode.AUTHOR_FORMAT_INVALID, "Rollback checkpoint token is not in the active lineage."))
|
|
if target == state.active_revision:
|
|
return Rejected(WorkflowError(ErrorCode.AUTHOR_DECISION_REJECTED, "Rollback must select an earlier checkpoint."))
|
|
key = self._key(task_id, "rollback", state.working_head, rollback.model_dump(mode="json"))
|
|
invocation = self.repository.begin_invocation(task_id, invocation_id, key)
|
|
if invocation.status == "finished" and invocation.result is not None:
|
|
return Accepted(invocation.result)
|
|
next_state = transition(state, "rollback", active_revision=target, repair_required=True)
|
|
event = {
|
|
"event": "rollback",
|
|
"working_head_before": state.working_head,
|
|
"working_head": next_state.working_head,
|
|
"checkpoint_token": rollback.checkpoint_token,
|
|
"rollback_from_revision": state.active_revision,
|
|
"rollback_to_revision": target,
|
|
"reason": rollback.reason,
|
|
}
|
|
result = {"status": "rolled_back", "active_revision": target, "working_head": next_state.working_head}
|
|
if not self._commit_invocation(next_state, [event], invocation, result):
|
|
return Rejected(self._stale())
|
|
return Accepted(result)
|
|
|
|
def submit_cdsl_fragment(self, task_id: str, fragment: dict[str, Any], *, invocation_id: str) -> Accepted | Rejected:
|
|
state = self.repository.get_state(task_id)
|
|
action = state.pending_action if state else None
|
|
if state is None or state.phase != TaskPhase.ACTION_PENDING or action is None:
|
|
return Rejected(WorkflowError(ErrorCode.AUTHOR_DECISION_REJECTED, "A CDSL fragment requires one pending action."))
|
|
contract = self.runtime.operation_contract(action.atomic_id)
|
|
if contract["contract_hash"] != action.contract_hash:
|
|
return Rejected(WorkflowError(ErrorCode.STALE_WORKING_HEAD, "The pending operation contract changed; read the current contract again."))
|
|
base = self.artifacts.read_active_cdsl(task_id, state.active_revision)
|
|
topology = self.artifacts.read_topology(task_id, state.active_revision)
|
|
selectors = self.runtime.selector_tokens(topology)
|
|
references = self.runtime.reference_tokens(base)
|
|
selector_policy = contract.get("selector_policy") if isinstance(contract.get("selector_policy"), dict) else {}
|
|
expected_selector_kind = str(selector_policy.get("token_kind") or "")
|
|
allowed_selector_tokens = (
|
|
[token for token, value in selectors.items() if isinstance(value, dict) and value.get("kind") == expected_selector_kind]
|
|
if str((contract.get("fragment_shape") or {}).get("selector_tokens") or "forbidden") == "required"
|
|
else []
|
|
)
|
|
errors = validate_fragment(
|
|
contract,
|
|
fragment,
|
|
selector_tokens=allowed_selector_tokens,
|
|
reference_tokens=list(references),
|
|
root_xy_datum=not bool(state.active_revision),
|
|
)
|
|
if errors:
|
|
return Rejected(WorkflowError(ErrorCode.AUTHOR_FORMAT_INVALID, "CDSL fragment violates the active operation schema.", field_errors=tuple(errors)))
|
|
fragment_hash = canonical_hash(fragment)
|
|
exact_fingerprint = canonical_hash({
|
|
"active_revision": state.active_revision,
|
|
"atomic_id": action.atomic_id,
|
|
"fragment_hash": fragment_hash,
|
|
})
|
|
prior_exact = next((
|
|
event for event in reversed(self.repository.ledger_events(task_id))
|
|
if event.get("failure_exact_fingerprint") == exact_fingerprint
|
|
), None)
|
|
if prior_exact is not None:
|
|
return Rejected(WorkflowError(
|
|
ErrorCode.RUNTIME_PRECONDITION_FAILED,
|
|
"This exact CDSL fragment was already proven to fail at the current checkpoint; choose a different fragment or operation path.",
|
|
details={
|
|
"duplicate_fragment": True,
|
|
"active_revision": state.active_revision,
|
|
"atomic_id": action.atomic_id,
|
|
"fragment_hash": fragment_hash,
|
|
"failure_exact_fingerprint": exact_fingerprint,
|
|
"normalized_error_code": str(prior_exact.get("normalized_error_code") or ErrorCode.RUNTIME_PRECONDITION_FAILED.value),
|
|
},
|
|
))
|
|
key = self._key(task_id, "fragment", action.working_head, {"action_id": action.action_id, "fragment": fragment})
|
|
try:
|
|
require_through = self._action_requires_through(task_id, action.requirement_ids)
|
|
cdsl, audit = self.runtime.materialize_fragment(base, fragment, contract, selectors, references, require_through=require_through)
|
|
except Exception as error:
|
|
runtime_error = self._runtime_error(error)
|
|
return Rejected(WorkflowError(
|
|
runtime_error.code,
|
|
runtime_error.message,
|
|
field_errors=runtime_error.field_errors,
|
|
retryable=runtime_error.retryable,
|
|
details={
|
|
**runtime_error.details,
|
|
"active_revision": state.active_revision,
|
|
"atomic_id": action.atomic_id,
|
|
"fragment_hash": fragment_hash,
|
|
"failure_exact_fingerprint": exact_fingerprint,
|
|
"normalized_error_code": runtime_error.code.value,
|
|
},
|
|
))
|
|
invocation = self.repository.begin_invocation(task_id, invocation_id, key)
|
|
if invocation.status == "finished" and invocation.result is not None:
|
|
return Accepted(invocation.result)
|
|
candidate_id = "candidate_" + sha256(key.encode("utf-8")).hexdigest()[:16]
|
|
try:
|
|
stage = self.artifacts.start_candidate_stage(task_id, key, {"schema_version": "cad.v3.candidate-input.v1", "idempotency_key": key, "candidate_id": candidate_id, "action_id": action.action_id, "working_head": action.working_head, "contract_hash": action.contract_hash, "fragment": fragment, "fragment_audit": audit})
|
|
except OSError as error:
|
|
return self._park_for_storage_retry(
|
|
state,
|
|
event="candidate_stage_storage_failure",
|
|
message=str(error),
|
|
)
|
|
building = transition(state, "candidate_started", candidate_id=candidate_id, candidate_stage_id=stage.stage_id)
|
|
if not self.repository.compare_and_swap(building, events=[{"event": "candidate_building", "candidate_id": candidate_id, "stage_id": stage.stage_id, "action_id": action.action_id, "fragment_hash": audit["fragment_hash"]}]):
|
|
return Rejected(self._stale())
|
|
try:
|
|
rebuilt, operation_failures = self.runtime.rebuild_best_effort(cdsl, stage.output_dir, task_id, candidate_id)
|
|
attempted_feature_ids = {str(value) for value in audit.get("assigned_feature_ids") or () if isinstance(value, str)}
|
|
executed_feature_ids = {str(value) for value in rebuilt.get("executed_feature_ids") or () if isinstance(value, str)}
|
|
if attempted_feature_ids and not attempted_feature_ids.intersection(executed_feature_ids):
|
|
next_state = transition(building, "candidate_rejected", candidate_id="", candidate_stage_id="", repair_required=True, error=ErrorCode.CANDIDATE_BUILD_FAILED)
|
|
result = {"candidate_id": candidate_id, "status": "skipped", "code": ErrorCode.CANDIDATE_BUILD_FAILED.value, "operation_failures": operation_failures}
|
|
if not self._commit_invocation(next_state, [{
|
|
"event": "candidate_operation_skipped",
|
|
"candidate_id": candidate_id,
|
|
"stage_id": stage.stage_id,
|
|
"action_id": action.action_id,
|
|
"working_head": action.working_head,
|
|
"checkpoint_revision": state.active_revision,
|
|
"atomic_id": action.atomic_id,
|
|
"fragment_hash": fragment_hash,
|
|
"operation_failures": operation_failures,
|
|
"message": "The submitted feature did not execute; earlier executable features were retained.",
|
|
}], invocation, result):
|
|
return Rejected(self._stale())
|
|
return Rejected(WorkflowError(
|
|
ErrorCode.CANDIDATE_BUILD_FAILED,
|
|
"The submitted feature could not execute; the previous executable checkpoint was retained.",
|
|
details={"operation_failures": operation_failures},
|
|
))
|
|
try:
|
|
claim_results = self._evaluate_claims(task_id, rebuilt)
|
|
except Exception as error:
|
|
failed_state = transition(building, "failed", error=ErrorCode.REQUIREMENTS_SPEC_INVALID)
|
|
result = {"candidate_id": candidate_id, "status": "failed", "code": ErrorCode.REQUIREMENTS_SPEC_INVALID.value}
|
|
if not self._commit_invocation(failed_state, [{
|
|
"event": "requirements_contract_execution_failed",
|
|
"candidate_id": candidate_id,
|
|
"stage_id": stage.stage_id,
|
|
"action_id": action.action_id,
|
|
"message": str(error)[:1000],
|
|
}], invocation, result):
|
|
return Rejected(self._stale())
|
|
return Rejected(WorkflowError(
|
|
ErrorCode.REQUIREMENTS_SPEC_INVALID,
|
|
"The frozen requirements contract could not be evaluated; no CAD repair was attempted.",
|
|
details={"diagnostic": str(error)[:1000]},
|
|
))
|
|
operation_results = self._operation_candidate_results(
|
|
action,
|
|
contract,
|
|
cdsl,
|
|
rebuilt,
|
|
parent_facts=self._facts(task_id, state.active_revision),
|
|
require_through=require_through,
|
|
)
|
|
blockers = [
|
|
*self._candidate_blockers(task_id, action.requirement_ids, claim_results),
|
|
*[item for item in operation_results if item.get("status") != "pass"],
|
|
]
|
|
candidate = {"schema_version": "cad.v3.candidate.v1", "candidate_id": candidate_id, "stage_id": stage.stage_id, "action_id": action.action_id, "working_head": action.working_head, "actual_atomic_id": action.atomic_id, "fragment_hash": audit["fragment_hash"], "selector_snapshot_id": audit["selector_snapshot_id"], "claim_results": claim_results, "operation_verifier_results": operation_results, "blockers": blockers, "operation_failures": operation_failures, "executed_feature_ids": rebuilt.get("executed_feature_ids") or [], "health": rebuilt["health"], "render_manifest": rebuilt.get("render_manifest") or {}, "paths": rebuilt["paths"]}
|
|
self.artifacts.write_stage_json(task_id, stage.stage_id, "candidate.json", candidate)
|
|
review = transition(building, "candidate_built", candidate_id=candidate_id, candidate_stage_id=stage.stage_id)
|
|
result = {"candidate_id": candidate_id, "stage_id": stage.stage_id, "status": "awaiting_review", "claim_results": claim_results, "operation_failures": operation_failures}
|
|
if not self._commit_invocation(review, [{"event": "candidate_built", "candidate_id": candidate_id, "action_id": action.action_id, "claim_results": claim_results, "operation_failures": operation_failures}], invocation, result):
|
|
return Rejected(self._stale())
|
|
return Accepted(result)
|
|
except OSError as error:
|
|
return self._park_retry(
|
|
building,
|
|
ErrorCode.STORAGE_FAILURE,
|
|
event="candidate_build_storage_failure",
|
|
message=str(error),
|
|
details={"candidate_id": candidate_id, "stage_id": stage.stage_id},
|
|
)
|
|
except Exception as error:
|
|
if "RENDER_SERVICE_UNAVAILABLE" in str(error):
|
|
return self._park_retry(
|
|
building,
|
|
ErrorCode.RENDER_SERVICE_UNAVAILABLE,
|
|
event="candidate_render_failure",
|
|
message=str(error),
|
|
details={"candidate_id": candidate_id, "stage_id": stage.stage_id},
|
|
)
|
|
if "RUNTIME_EXECUTION_FAILURE" in str(error):
|
|
return self._park_retry(
|
|
building,
|
|
ErrorCode.RUNTIME_EXECUTION_FAILURE,
|
|
event="candidate_runtime_execution_failure",
|
|
message=str(error),
|
|
details={"candidate_id": candidate_id, "stage_id": stage.stage_id, "checkpoint_revision": state.active_revision},
|
|
)
|
|
failed_state = transition(building, "candidate_rejected", candidate_id="", candidate_stage_id="", repair_required=True, error=ErrorCode.CANDIDATE_BUILD_FAILED)
|
|
result = {"candidate_id": candidate_id, "status": "failed", "code": ErrorCode.CANDIDATE_BUILD_FAILED.value}
|
|
if not self._commit_invocation(failed_state, [{
|
|
"event": "candidate_build_failed",
|
|
"candidate_id": candidate_id,
|
|
"stage_id": stage.stage_id,
|
|
"action_id": action.action_id,
|
|
"working_head": action.working_head,
|
|
"checkpoint_revision": state.active_revision,
|
|
"atomic_id": action.atomic_id,
|
|
"fragment_hash": fragment_hash,
|
|
"normalized_error_code": ErrorCode.CANDIDATE_BUILD_FAILED.value,
|
|
"failure_exact_fingerprint": exact_fingerprint,
|
|
"message": str(error)[:1000],
|
|
}], invocation, result):
|
|
return Rejected(self._stale())
|
|
return Rejected(WorkflowError(ErrorCode.CANDIDATE_BUILD_FAILED, "Candidate build failed; the checkpoint remains unchanged.", details={"diagnostic": str(error)[:1000]}))
|
|
|
|
def recover_candidate_build(self, task_id: str) -> Accepted | Rejected:
|
|
"""Resume one persisted candidate build without inventing another action.
|
|
|
|
A complete staging directory is converted to the next state directly.
|
|
If a process stopped mid-build, the same stage/idempotency key is used
|
|
and an already-written CDSL document is rebuilt in place.
|
|
"""
|
|
state = self.repository.get_state(task_id)
|
|
action = state.pending_action if state else None
|
|
if state is None or state.phase != TaskPhase.CANDIDATE_BUILDING or action is None or not state.candidate_stage_id:
|
|
return Rejected(WorkflowError(ErrorCode.AUTHOR_DECISION_REJECTED, "There is no recoverable candidate build."))
|
|
try:
|
|
candidate = self.artifacts.read_stage_json(task_id, state.candidate_stage_id, "candidate.json")
|
|
if isinstance(candidate, dict):
|
|
return self._advance_recovered_candidate(task_id, state, candidate)
|
|
source = self.artifacts.read_stage_json(task_id, state.candidate_stage_id, "input.json")
|
|
if not isinstance(source, dict) or source.get("candidate_id") != state.candidate_id:
|
|
return self._park_for_storage_retry(
|
|
state,
|
|
event="candidate_recovery_storage_failure",
|
|
message="Candidate recovery input is unavailable.",
|
|
)
|
|
report = self.artifacts.read_stage_json(task_id, state.candidate_stage_id, "rebuild-report.json")
|
|
topology = self.artifacts.read_stage_json(task_id, state.candidate_stage_id, "model.topology.json")
|
|
cdsl = self.artifacts.read_stage_json(task_id, state.candidate_stage_id, "model.cdsl.json")
|
|
if isinstance(report, dict) and isinstance(topology, dict) and isinstance(cdsl, dict):
|
|
rebuilt = {
|
|
"health": report.get("health") or {}, "topology": topology, "report": report,
|
|
"render_manifest": report.get("render_manifest") or {},
|
|
"paths": {"cdsl": "model.cdsl.json", "step": "model.step", "glb": "model.glb", "topology": "model.topology.json", "report": "rebuild-report.json", "render_manifest": "renders/render-manifest.json"},
|
|
}
|
|
else:
|
|
base = self.artifacts.read_active_cdsl(task_id, state.active_revision)
|
|
fragment = source.get("fragment")
|
|
contract = self.runtime.operation_contract(action.atomic_id)
|
|
if not isinstance(fragment, dict) or contract.get("contract_hash") != action.contract_hash:
|
|
raise RuntimeError("RUNTIME_PRECONDITION_FAILED: candidate recovery contract is stale")
|
|
selectors = self.runtime.selector_tokens(self.artifacts.read_topology(task_id, state.active_revision))
|
|
references = self.runtime.reference_tokens(base)
|
|
cdsl, _audit = self.runtime.materialize_fragment(base, fragment, contract, selectors, references, require_through=self._action_requires_through(task_id, action.requirement_ids))
|
|
rebuilt, operation_failures = self.runtime.rebuild_best_effort(cdsl, self.artifacts.stage_output_dir(task_id, state.candidate_stage_id), task_id, state.candidate_id)
|
|
if "operation_failures" not in locals():
|
|
operation_failures = []
|
|
claim_results = self._evaluate_claims(task_id, rebuilt)
|
|
audit = source.get("fragment_audit") if isinstance(source.get("fragment_audit"), dict) else {}
|
|
contract = self.runtime.operation_contract(action.atomic_id)
|
|
operation_results = self._operation_candidate_results(
|
|
action,
|
|
contract,
|
|
cdsl,
|
|
rebuilt,
|
|
parent_facts=self._facts(task_id, state.active_revision),
|
|
require_through=self._action_requires_through(task_id, action.requirement_ids),
|
|
)
|
|
candidate = {
|
|
"schema_version": "cad.v3.candidate.v1", "candidate_id": state.candidate_id,
|
|
"stage_id": state.candidate_stage_id, "action_id": action.action_id,
|
|
"working_head": action.working_head, "actual_atomic_id": action.atomic_id,
|
|
"fragment_hash": audit.get("fragment_hash") or canonical_hash(source.get("fragment") or {}),
|
|
"selector_snapshot_id": audit.get("selector_snapshot_id") or "", "claim_results": claim_results,
|
|
"operation_verifier_results": operation_results,
|
|
"operation_failures": operation_failures,
|
|
"blockers": [
|
|
*self._candidate_blockers(task_id, action.requirement_ids, claim_results),
|
|
*[item for item in operation_results if item.get("status") != "pass"],
|
|
],
|
|
"health": rebuilt["health"], "render_manifest": rebuilt.get("render_manifest") or {}, "paths": rebuilt["paths"],
|
|
}
|
|
self.artifacts.write_stage_json(task_id, state.candidate_stage_id, "candidate.json", candidate)
|
|
return self._advance_recovered_candidate(task_id, state, candidate)
|
|
except OSError as error:
|
|
return self._park_for_storage_retry(state, event="candidate_recovery_storage_failure", message=str(error))
|
|
except Exception as error:
|
|
if "RENDER_SERVICE_UNAVAILABLE" in str(error):
|
|
return self._park_retry(
|
|
state,
|
|
ErrorCode.RENDER_SERVICE_UNAVAILABLE,
|
|
event="candidate_recovery_render_failure",
|
|
message=str(error),
|
|
details={"candidate_id": state.candidate_id, "stage_id": state.candidate_stage_id},
|
|
)
|
|
if "RUNTIME_EXECUTION_FAILURE" in str(error):
|
|
return self._park_retry(
|
|
state,
|
|
ErrorCode.RUNTIME_EXECUTION_FAILURE,
|
|
event="candidate_recovery_runtime_execution_failure",
|
|
message=str(error),
|
|
details={"candidate_id": state.candidate_id, "stage_id": state.candidate_stage_id, "checkpoint_revision": state.active_revision},
|
|
)
|
|
failed = transition(state, "candidate_rejected", candidate_id="", candidate_stage_id="", repair_required=True, error=ErrorCode.CANDIDATE_BUILD_FAILED)
|
|
self.repository.compare_and_swap(failed, events=[{
|
|
"event": "candidate_recovery_failed",
|
|
"candidate_id": state.candidate_id,
|
|
"stage_id": state.candidate_stage_id,
|
|
"action_id": action.action_id,
|
|
"working_head": action.working_head,
|
|
"message": str(error)[:1000],
|
|
}])
|
|
return Accepted({"candidate_id": state.candidate_id, "status": "failed", "code": ErrorCode.CANDIDATE_BUILD_FAILED.value, "diagnostic": str(error)[:1000]})
|
|
|
|
def record_candidate_review(self, task_id: str, review: CandidateReview, *, invocation_id: str) -> Accepted | Rejected:
|
|
state = self.repository.get_state(task_id)
|
|
action = state.pending_action if state else None
|
|
if state is None or state.phase != TaskPhase.CANDIDATE_REVIEW or action is None:
|
|
return Rejected(WorkflowError(ErrorCode.AUTHOR_DECISION_REJECTED, "Candidate review is not expected in the current workflow phase."))
|
|
if review.candidate_id != state.candidate_id or review.working_head != action.working_head:
|
|
return Rejected(WorkflowError(ErrorCode.STALE_WORKING_HEAD, "Candidate review references a stale candidate or head."))
|
|
candidate = self.artifacts.read_stage_json(task_id, state.candidate_stage_id, "candidate.json")
|
|
published = None if isinstance(candidate, dict) else self.artifacts.find_published_candidate(task_id, state.candidate_stage_id)
|
|
published_revision = published[0] if published else ""
|
|
if published:
|
|
candidate = published[1]
|
|
if not isinstance(candidate, dict):
|
|
return Rejected(WorkflowError(ErrorCode.STORAGE_FAILURE, "Candidate review evidence is unavailable.", retryable=True))
|
|
actual = candidate.get("claim_results") if isinstance(candidate.get("claim_results"), list) else []
|
|
expected_ids = {str(item.get("claim_id") or "") for item in actual if isinstance(item, dict)}
|
|
submitted = {item.claim_id: item.status for item in review.claim_coverage}
|
|
submitted_ids = set(submitted)
|
|
if submitted_ids != expected_ids or len(submitted_ids) != len(review.claim_coverage):
|
|
return Rejected(WorkflowError(ErrorCode.AUTHOR_FORMAT_INVALID, "Candidate review must cover exactly the current claim set."))
|
|
mismatched_deterministic = [
|
|
str(item.get("claim_id") or "")
|
|
for item in actual
|
|
if isinstance(item, dict)
|
|
and item.get("deterministic")
|
|
and submitted.get(str(item.get("claim_id") or "")) != item.get("status")
|
|
]
|
|
if mismatched_deterministic:
|
|
return Rejected(WorkflowError(
|
|
ErrorCode.AUTHOR_FORMAT_INVALID,
|
|
"Candidate review must report the server's deterministic claim status exactly.",
|
|
details={"claim_ids": mismatched_deterministic},
|
|
))
|
|
deterministic_fail = [item for item in actual if isinstance(item, dict) and item.get("deterministic") and item.get("status") in {"fail", "unavailable"}]
|
|
key = self._key(task_id, "candidate_review", action.working_head, review.model_dump(mode="json"))
|
|
invocation = self.repository.begin_invocation(task_id, invocation_id, key)
|
|
if invocation.status == "finished" and invocation.result is not None:
|
|
return Accepted(invocation.result)
|
|
review_payload = review.model_dump(mode="json")
|
|
try:
|
|
if published_revision:
|
|
self.artifacts.write_json_once(task_id, f"revisions/{published_revision}/candidate-review.json", review_payload)
|
|
else:
|
|
self.artifacts.write_stage_json(task_id, state.candidate_stage_id, "candidate-review.json", review_payload)
|
|
except OSError as error:
|
|
return self._park_for_storage_retry(
|
|
state,
|
|
event="candidate_review_storage_failure",
|
|
message=str(error),
|
|
)
|
|
operation_failures = candidate.get("operation_failures") if isinstance(candidate.get("operation_failures"), list) else []
|
|
candidate_blockers = candidate.get("blockers") if isinstance(candidate.get("blockers"), list) else []
|
|
needs_repair = review.verdict != "accept" or bool(deterministic_fail) or bool(operation_failures) or bool(candidate_blockers)
|
|
revision_id = self._next_revision(task_id)
|
|
if published_revision and published_revision != revision_id:
|
|
return Rejected(WorkflowError(ErrorCode.STORAGE_FAILURE, "Published candidate revision does not match the current checkpoint lineage.", retryable=True))
|
|
try:
|
|
paths = self.artifacts.publish_candidate(task_id, state.candidate_stage_id, revision_id) if not published_revision else {}
|
|
except OSError as error:
|
|
return self._park_for_storage_retry(
|
|
state,
|
|
event="candidate_publish_storage_failure",
|
|
message=str(error),
|
|
)
|
|
next_state = transition(state, "candidate_accepted", active_revision=revision_id, repair_required=needs_repair, error=ErrorCode.CLAIM_VERIFICATION_FAILED if needs_repair else None)
|
|
event = {"event": "accepted", "action_id": action.action_id, "working_head_before": action.working_head, "parent_revision": state.active_revision, "revision_id": revision_id, "actual_atomic_id": action.atomic_id, "fragment_hash": candidate.get("fragment_hash"), "selector_snapshot_id": candidate.get("selector_snapshot_id"), "candidate_id": state.candidate_id, "review_path": f"revisions/{revision_id}/candidate-review.json", "coverage": actual, "issues": list(review.issues), "operation_failures": operation_failures, "repair_required": needs_repair}
|
|
result = {"candidate_id": state.candidate_id, "revision_id": revision_id, "paths": paths, "status": "accepted_with_issues" if needs_repair else "accepted"}
|
|
if not self._commit_invocation(next_state, [event], invocation, result):
|
|
return Rejected(self._stale())
|
|
return Accepted(result)
|
|
|
|
def recover_candidate_review(self, task_id: str) -> Accepted | Rejected | None:
|
|
state = self.repository.get_state(task_id)
|
|
if state is None or state.phase != TaskPhase.CANDIDATE_REVIEW or not state.candidate_stage_id:
|
|
return None
|
|
raw = self.artifacts.read_stage_json(task_id, state.candidate_stage_id, "candidate-review.json")
|
|
if raw is None:
|
|
published = self.artifacts.find_published_candidate(task_id, state.candidate_stage_id)
|
|
raw = self.artifacts.read_json(task_id, f"revisions/{published[0]}/candidate-review.json") if published else None
|
|
if raw is None:
|
|
return None
|
|
try:
|
|
review = CandidateReview.model_validate(raw)
|
|
except ValueError as error:
|
|
return Rejected(WorkflowError(ErrorCode.STORAGE_FAILURE, "Persisted candidate review is invalid.", details={"diagnostic": str(error)[:1000]}))
|
|
return self.record_candidate_review(task_id, review, invocation_id=f"{task_id}_recover_candidate_{secrets.token_hex(8)}")
|
|
|
|
def complete_task(self, task_id: str, *, invocation_id: str) -> Accepted | Rejected:
|
|
state = self.repository.get_state(task_id)
|
|
if state is None:
|
|
return Rejected(WorkflowError(ErrorCode.AUTHOR_DECISION_REJECTED, "Task completion is not available for an unknown task."))
|
|
# Completion idempotency is revision-scoped. A duplicate request after
|
|
# final validation or publication replays the existing result and can
|
|
# never start a second final rebuild/review.
|
|
key = self._key(task_id, "complete", state.active_revision, {})
|
|
if state.phase == TaskPhase.FINAL_VALIDATION and state.active_revision:
|
|
invocation = self.repository.begin_invocation(task_id, invocation_id, key)
|
|
if invocation.status == "finished" and invocation.result is not None:
|
|
return Accepted(invocation.result)
|
|
return Rejected(WorkflowError(ErrorCode.AUTHOR_DECISION_REJECTED, "Final validation is already in progress for this revision."))
|
|
if state.phase != TaskPhase.AWAITING_ACTION or state.pending_action is not None or state.repair_required:
|
|
return Rejected(WorkflowError(ErrorCode.AUTHOR_DECISION_REJECTED, "Task completion is not available until there is no pending action or repair."))
|
|
claim_results = self._evaluate_claims(task_id, self._facts(task_id, state.active_revision))
|
|
deterministic = [item for item in claim_results if item.get("deterministic")]
|
|
if not state.active_revision or any(item.get("status") != "pass" for item in deterministic):
|
|
return Rejected(WorkflowError(ErrorCode.CLAIM_VERIFICATION_FAILED, "All deterministic claims must pass before final review.", details={"claim_results": claim_results}))
|
|
invocation = self.repository.begin_invocation(task_id, invocation_id, key)
|
|
if invocation.status == "finished" and invocation.result is not None:
|
|
return Accepted(invocation.result)
|
|
next_state = transition(state, "final_requested")
|
|
result = {"revision_id": state.active_revision, "working_head": next_state.working_head, "claim_results": claim_results}
|
|
if not self._commit_invocation(next_state, [{"event": "final_validation_requested", "revision_id": state.active_revision, "claim_results": claim_results}], invocation, result):
|
|
return Rejected(self._stale())
|
|
return Accepted(result)
|
|
|
|
def recover_final_review(self, task_id: str) -> Accepted | Rejected | None:
|
|
state = self.repository.get_state(task_id)
|
|
if state is None or state.phase != TaskPhase.FINAL_VALIDATION or not state.active_revision:
|
|
return None
|
|
raw = self.artifacts.read_json(task_id, self._final_review_path(state.active_revision))
|
|
if raw is None:
|
|
return None
|
|
try:
|
|
review = FinalReview.model_validate(raw)
|
|
except ValueError as error:
|
|
return Rejected(WorkflowError(ErrorCode.STORAGE_FAILURE, "Persisted final review is invalid.", details={"diagnostic": str(error)[:1000]}))
|
|
return self.record_final_review(task_id, review, invocation_id=f"{task_id}_recover_final_{secrets.token_hex(8)}")
|
|
|
|
def _advance_recovered_candidate(self, task_id: str, state: TaskState, candidate: dict[str, Any]) -> Accepted | Rejected:
|
|
action = state.pending_action
|
|
if action is None or candidate.get("candidate_id") != state.candidate_id or candidate.get("action_id") != action.action_id:
|
|
return Rejected(WorkflowError(ErrorCode.STORAGE_FAILURE, "Candidate recovery evidence does not match the pending action.", retryable=True))
|
|
claim_results = candidate.get("claim_results") if isinstance(candidate.get("claim_results"), list) else []
|
|
operation_results = candidate.get("operation_verifier_results") if isinstance(candidate.get("operation_verifier_results"), list) else []
|
|
blockers = [
|
|
*self._candidate_blockers(task_id, action.requirement_ids, claim_results),
|
|
*[item for item in operation_results if isinstance(item, dict) and item.get("status") != "pass"],
|
|
]
|
|
key = str((self.artifacts.read_stage_json(task_id, state.candidate_stage_id, "input.json") or {}).get("idempotency_key") or "")
|
|
invocation = self.repository.begin_invocation(task_id, f"{task_id}_recover_build_{secrets.token_hex(8)}", key) if key else None
|
|
if invocation is not None and invocation.status == "finished" and invocation.result is not None:
|
|
return Accepted(invocation.result)
|
|
review = transition(state, "candidate_built", candidate_id=state.candidate_id, candidate_stage_id=state.candidate_stage_id)
|
|
result = {"candidate_id": state.candidate_id, "stage_id": state.candidate_stage_id, "status": "awaiting_review", "claim_results": claim_results}
|
|
committed = self._commit_invocation(review, [{"event": "candidate_recovered", "candidate_id": state.candidate_id, "action_id": action.action_id, "claim_results": claim_results}], invocation, result) if invocation is not None else self.repository.compare_and_swap(review, events=[{"event": "candidate_recovered", "candidate_id": state.candidate_id, "action_id": action.action_id, "claim_results": claim_results}])
|
|
if not committed:
|
|
return Rejected(self._stale())
|
|
return Accepted(result)
|
|
|
|
def finalize_best_effort(self, task_id: str, *, reason: ErrorCode, invocation_id: str) -> Accepted | Rejected:
|
|
"""Publish the last executable checkpoint when further repair is bounded.
|
|
|
|
This is deliberately separate from strict final validation. It does
|
|
not claim that unmet acceptance targets passed; it makes the usable
|
|
model and its measured gaps durable instead of converting a planning
|
|
dead-end into a failed task with no deliverable.
|
|
"""
|
|
state = self.repository.get_state(task_id)
|
|
if state is None or not state.active_revision:
|
|
return Rejected(WorkflowError(ErrorCode.AUTHOR_DECISION_REJECTED, "Best-effort completion requires an executable checkpoint."))
|
|
if state.phase == TaskPhase.COMPLETED:
|
|
return Accepted({"status": "completed", "revision_id": state.active_revision})
|
|
key = self._key(task_id, "best_effort_complete", state.active_revision, {"reason": reason.value})
|
|
invocation = self.repository.begin_invocation(task_id, invocation_id, key)
|
|
if invocation.status == "finished" and invocation.result is not None:
|
|
return Accepted(invocation.result)
|
|
claim_results = self._evaluate_claims(task_id, self._facts(task_id, state.active_revision))
|
|
issues = [
|
|
f"{item.get('claim_kind')}: {item.get('status')}"
|
|
for item in claim_results
|
|
if item.get("status") != "pass"
|
|
]
|
|
next_state = transition(state, "best_effort_completed", error=ErrorCode.BEST_EFFORT_COMPLETED, repair_required=False)
|
|
result = {"status": "completed_with_warnings", "revision_id": state.active_revision, "claim_results": claim_results, "issues": issues}
|
|
if not self._commit_invocation(next_state, [{
|
|
"event": "completed_best_effort",
|
|
"revision_id": state.active_revision,
|
|
"termination_code": reason.value,
|
|
"message": "Further CAD repair was bounded; the last executable checkpoint was published.",
|
|
"issues": issues,
|
|
"claim_results": claim_results,
|
|
"completion_result_path": "completion-result.md",
|
|
}], invocation, result):
|
|
return Rejected(self._stale())
|
|
return Accepted(result)
|
|
|
|
def record_final_review(self, task_id: str, review: FinalReview, *, invocation_id: str) -> Accepted | Rejected:
|
|
state = self.repository.get_state(task_id)
|
|
if state is None or state.phase != TaskPhase.FINAL_VALIDATION:
|
|
return Rejected(WorkflowError(ErrorCode.AUTHOR_DECISION_REJECTED, "Final review is not expected in the current workflow phase."))
|
|
stale = reject_stale_head(state, review.working_head)
|
|
if stale:
|
|
return Rejected(stale)
|
|
claim_results = self._evaluate_claims(task_id, self._facts(task_id, state.active_revision))
|
|
expected_ids = {str(item.get("claim_id") or "") for item in claim_results}
|
|
submitted = {item.claim_id: item.status for item in review.claim_coverage}
|
|
submitted_ids = set(submitted)
|
|
if submitted_ids != expected_ids or len(submitted_ids) != len(review.claim_coverage):
|
|
return Rejected(WorkflowError(ErrorCode.AUTHOR_FORMAT_INVALID, "Final review must cover exactly the final claim set."))
|
|
mismatched_deterministic = [
|
|
str(item.get("claim_id") or "")
|
|
for item in claim_results
|
|
if item.get("deterministic")
|
|
and submitted.get(str(item.get("claim_id") or "")) != item.get("status")
|
|
]
|
|
if mismatched_deterministic:
|
|
return Rejected(WorkflowError(
|
|
ErrorCode.AUTHOR_FORMAT_INVALID,
|
|
"Final review must report the server's deterministic claim status exactly.",
|
|
details={"claim_ids": mismatched_deterministic},
|
|
))
|
|
deterministic_fail = [item for item in claim_results if item.get("deterministic") and item.get("status") != "pass"]
|
|
review_coverage = submitted
|
|
visual_not_passed = [
|
|
item
|
|
for item in claim_results
|
|
if not item.get("deterministic") and review_coverage.get(str(item.get("claim_id") or "")) != "pass"
|
|
]
|
|
key = self._key(task_id, "final_review", state.working_head, review.model_dump(mode="json"))
|
|
invocation = self.repository.begin_invocation(task_id, invocation_id, key)
|
|
if invocation.status == "finished" and invocation.result is not None:
|
|
return Accepted(invocation.result)
|
|
final_review_path = self._final_review_path(state.active_revision)
|
|
try:
|
|
self.artifacts.write_json_once(task_id, final_review_path, review.model_dump(mode="json"))
|
|
except OSError as error:
|
|
return self._park_for_storage_retry(
|
|
state,
|
|
event="final_review_storage_failure",
|
|
message=str(error),
|
|
)
|
|
if review.verdict != "pass" or deterministic_fail or visual_not_passed:
|
|
accepted_fragment = next((
|
|
event.get("fragment_hash") for event in reversed(self.repository.ledger_events(task_id))
|
|
if event.get("event") == "accepted" and event.get("revision_id") == state.active_revision
|
|
), "")
|
|
atomic_id = next((
|
|
str(event.get("actual_atomic_id") or "") for event in reversed(self.repository.ledger_events(task_id))
|
|
if event.get("event") == "accepted" and event.get("revision_id") == state.active_revision
|
|
), "")
|
|
exact_fingerprint = canonical_hash({
|
|
"active_revision": state.active_revision,
|
|
"atomic_id": atomic_id,
|
|
"fragment_hash": accepted_fragment,
|
|
}) if accepted_fragment and atomic_id else ""
|
|
next_state = transition(state, "final_repair", repair_required=True, error=ErrorCode.CLAIM_VERIFICATION_FAILED if deterministic_fail else ErrorCode.CANDIDATE_REVIEW_REJECTED)
|
|
result = {"status": "repair", "revision_id": state.active_revision}
|
|
if not self._commit_invocation(next_state, [{
|
|
"event": "final_review_repair",
|
|
"revision_id": state.active_revision,
|
|
"claim_results": claim_results,
|
|
"review_claim_coverage": [item.model_dump(mode="json") for item in review.claim_coverage],
|
|
"visual_not_passed": [str(item.get("claim_id") or "") for item in visual_not_passed],
|
|
"issues": list(review.issues),
|
|
"evidence": list(review.evidence),
|
|
"failed_checklist_items": [
|
|
str(requirement.get("statement") or "")
|
|
for requirement in (self._requirements_contract(task_id) or {}).get("requirements") or ()
|
|
if isinstance(requirement, dict)
|
|
and any(str(claim.get("claim_id") or "") in {str(item.get("claim_id") or "") for item in deterministic_fail + visual_not_passed} for claim in requirement.get("acceptance_claims") or () if isinstance(claim, dict))
|
|
],
|
|
"atomic_id": atomic_id,
|
|
"fragment_hash": accepted_fragment,
|
|
"failure_exact_fingerprint": exact_fingerprint,
|
|
"normalized_error_code": ErrorCode.CLAIM_VERIFICATION_FAILED.value if deterministic_fail else ErrorCode.CANDIDATE_REVIEW_REJECTED.value,
|
|
}], invocation, result):
|
|
return Rejected(self._stale())
|
|
return Accepted(result)
|
|
next_state = transition(state, "final_accepted")
|
|
result = {"status": "completed", "revision_id": state.active_revision}
|
|
if not self._commit_invocation(next_state, [{"event": "completed", "revision_id": state.active_revision, "final_review_path": final_review_path, "completion_result_path": "completion-result.md", "claim_results": claim_results}], invocation, result):
|
|
return Rejected(self._stale())
|
|
return Accepted(result)
|
|
|
|
def _evaluate_claims(self, task_id: str, facts: dict[str, Any]) -> list[dict[str, Any]]:
|
|
contract = self._requirements_contract(task_id) or {}
|
|
claims = [claim for requirement in contract.get("requirements") or () if isinstance(requirement, dict) for claim in requirement.get("acceptance_claims") or () if isinstance(claim, dict)]
|
|
return self.verifiers.evaluate(claims, facts)
|
|
|
|
def claim_summary(self, task_id: str, state: TaskState) -> list[dict[str, Any]]:
|
|
"""Return bounded current claim facts for author context assembly."""
|
|
return [
|
|
{
|
|
"claim_id": str(item.get("claim_id") or ""),
|
|
"claim_kind": str(item.get("claim_kind") or ""),
|
|
"deterministic": bool(item.get("deterministic")),
|
|
"status": str(item.get("status") or "pending"),
|
|
"evidence": item.get("evidence") if isinstance(item.get("evidence"), dict) else {},
|
|
}
|
|
for item in self._evaluate_claims(task_id, self._facts(task_id, state.active_revision))
|
|
if isinstance(item, dict)
|
|
]
|
|
|
|
def model_summary(self, task_id: str, state: TaskState) -> dict[str, Any]:
|
|
"""Expose measurements, not raw topology, in recurring author turns."""
|
|
if not state.active_revision:
|
|
return {"revision_id": "", "available": False}
|
|
facts = self._facts(task_id, state.active_revision)
|
|
health = facts.get("health") if isinstance(facts.get("health"), dict) else {}
|
|
topology = facts.get("topology") if isinstance(facts.get("topology"), dict) else {}
|
|
counts: dict[str, int] = {}
|
|
inner_cylindrical_bores: list[dict[str, Any]] = []
|
|
for record in topology.get("records") or ():
|
|
if isinstance(record, dict) and isinstance(record.get("kind"), str):
|
|
kind = record["kind"]
|
|
counts[kind] = counts.get(kind, 0) + 1
|
|
geometry = record.get("geometry") if isinstance(record.get("geometry"), dict) else {}
|
|
radius = geometry.get("radius_mm")
|
|
axis_origin = geometry.get("axis_origin_mm")
|
|
if (
|
|
geometry.get("surface_type") == "cylinder"
|
|
and geometry.get("cylinder_role") == "inner"
|
|
and isinstance(radius, (int, float))
|
|
and isinstance(axis_origin, list)
|
|
and len(axis_origin) == 3
|
|
and all(isinstance(value, (int, float)) for value in axis_origin)
|
|
):
|
|
inner_cylindrical_bores.append({
|
|
"diameter_mm": round(float(radius) * 2, 6),
|
|
"axis_origin_mm": [round(float(value), 6) for value in axis_origin],
|
|
"through": bool(geometry.get("through")),
|
|
})
|
|
bbox = health.get("bbox_mm") if isinstance(health.get("bbox_mm"), dict) else {}
|
|
return {
|
|
"revision_id": state.active_revision,
|
|
"available": bool(health or topology),
|
|
"solid_count": health.get("solid_count"),
|
|
"bbox_dimensions_mm": bbox.get("dimensions"),
|
|
"topology_snapshot_id": str(topology.get("snapshot_id") or ""),
|
|
"topology_counts": counts,
|
|
# These are compact measured facts, not author-controlled CDSL.
|
|
# They make duplicate or misplaced hole repairs observable without
|
|
# injecting the full topology snapshot into every author turn.
|
|
"inner_cylindrical_bores": inner_cylindrical_bores[:16],
|
|
}
|
|
|
|
def _facts(self, task_id: str, revision_id: str) -> dict[str, Any]:
|
|
if not revision_id:
|
|
return {}
|
|
report = self.artifacts.read_json(task_id, f"revisions/{revision_id}/rebuild-report.json") or {}
|
|
return {"health": report.get("health") or {}, "topology": self.artifacts.read_topology(task_id, revision_id) or {}, "report": report}
|
|
|
|
def _action_requires_through(self, task_id: str, requirement_ids: tuple[str, ...]) -> bool:
|
|
contract = self._requirements_contract(task_id) or {}
|
|
return any(claim.get("claim_kind") == "through_cylindrical_bore" for requirement in contract.get("requirements") or () if isinstance(requirement, dict) and requirement.get("requirement_id") in requirement_ids for claim in requirement.get("acceptance_claims") or () if isinstance(claim, dict))
|
|
|
|
def _candidate_blockers(self, task_id: str, action_requirements: tuple[str, ...], results: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
# Global connected-body failure always rejects; action-linked claims
|
|
# must not already be false, while future-work claims may remain pending.
|
|
return [item for item in results if item.get("status") in {"fail", "unavailable"} and (item.get("claim_kind") in {"solid_count_equals", "single_connected_body"} or item.get("claim_id", "") in self._claim_ids_for_requirements(task_id, action_requirements))]
|
|
|
|
def _operation_candidate_results(
|
|
self,
|
|
action: PendingAction,
|
|
contract: dict[str, Any],
|
|
cdsl: dict[str, Any],
|
|
rebuilt: dict[str, Any],
|
|
*,
|
|
parent_facts: dict[str, Any],
|
|
require_through: bool,
|
|
) -> list[dict[str, Any]]:
|
|
"""Run the verified operation-level acceptance checks after rebuild.
|
|
|
|
Requirement claims prove the user contract. These checks separately
|
|
prove that an operation declared in the runtime registry actually made
|
|
the kind of change it advertises, even when a user did not include a
|
|
matching claim. A blind-hole action does not require the through-bore
|
|
verifier unless its linked requirement explicitly requests through
|
|
topology.
|
|
"""
|
|
features = cdsl.get("features") if isinstance(cdsl.get("features"), list) else []
|
|
feature = next(
|
|
(item for item in reversed(features) if isinstance(item, dict) and item.get("atomic_id") == action.atomic_id),
|
|
{},
|
|
)
|
|
params = feature.get("params") if isinstance(feature, dict) and isinstance(feature.get("params"), dict) else {}
|
|
claims: list[dict[str, Any]] = []
|
|
for claim_kind in contract.get("candidate_verifiers") or ():
|
|
if claim_kind == "through_cylindrical_bore" and not require_through:
|
|
continue
|
|
expected: dict[str, Any]
|
|
if claim_kind in {"cylindrical_bore", "through_cylindrical_bore"}:
|
|
diameter = params.get("diameter_mm")
|
|
positions = params.get("positions")
|
|
if not isinstance(diameter, (int, float)):
|
|
return [{"claim_id": f"operation_{action.action_id}_{claim_kind}", "claim_kind": claim_kind, "deterministic": True, "status": "unavailable", "evidence": {"reason": "operation has no measurable bore diameter"}}]
|
|
increment = len(positions) if isinstance(positions, list) else 1
|
|
prior_count = self._existing_bore_count(claim_kind, float(diameter), parent_facts)
|
|
expected = {
|
|
"diameter_mm": float(diameter),
|
|
# Candidate topology represents the full model, not only
|
|
# the feature just submitted. Therefore the operation
|
|
# proof must compare against the parent checkpoint plus
|
|
# this action's declared number of positions.
|
|
"count": prior_count + increment,
|
|
"tolerance_mm": 0.01,
|
|
}
|
|
elif claim_kind in {"single_connected_body", "volume_decreased"}:
|
|
expected = {}
|
|
else:
|
|
return [{"claim_id": f"operation_{action.action_id}_{claim_kind}", "claim_kind": str(claim_kind), "deterministic": True, "status": "unavailable", "evidence": {"reason": "operation verifier has no runtime expected-value binding"}}]
|
|
claims.append({"claim_id": f"operation_{action.action_id}_{claim_kind}", "claim_kind": claim_kind, "expected": expected})
|
|
facts = {
|
|
"health": rebuilt.get("health") or {},
|
|
"parent_health": parent_facts.get("health") or {},
|
|
"topology": rebuilt.get("topology") or {},
|
|
"report": rebuilt.get("report") or {},
|
|
}
|
|
results = self.verifiers.evaluate(claims, facts)
|
|
for result, claim in zip(results, claims, strict=True):
|
|
expected = claim.get("expected") if isinstance(claim.get("expected"), dict) else {}
|
|
if claim.get("claim_kind") not in {"cylindrical_bore", "through_cylindrical_bore"}:
|
|
continue
|
|
evidence = result.get("evidence") if isinstance(result.get("evidence"), dict) else {}
|
|
evidence = dict(evidence)
|
|
requested_total = expected.get("count")
|
|
if isinstance(requested_total, int):
|
|
positions = params.get("positions")
|
|
increment = len(positions) if isinstance(positions, list) else 1
|
|
evidence.update({
|
|
"parent_matching_count": requested_total - increment,
|
|
"expected_increment": increment,
|
|
"expected_total_count": requested_total,
|
|
})
|
|
result["evidence"] = evidence
|
|
return results
|
|
|
|
def _existing_bore_count(self, claim_kind: str, diameter_mm: float, parent_facts: dict[str, Any]) -> int:
|
|
"""Measure same-diameter bores in the parent with registry semantics.
|
|
|
|
The registry owns topology coalescing, including periodic faces from a
|
|
single analytic circle. Asking it for one instance provides an exact
|
|
count for populated parent geometry without duplicating B-rep logic in
|
|
the workflow layer.
|
|
"""
|
|
topology = parent_facts.get("topology") if isinstance(parent_facts.get("topology"), dict) else {}
|
|
if not isinstance(topology.get("records"), list) or not topology["records"]:
|
|
return 0
|
|
result = self.verifiers.evaluate([{
|
|
"claim_id": "operation_parent_bore_count",
|
|
"claim_kind": claim_kind,
|
|
"expected": {"diameter_mm": diameter_mm, "count": 1, "tolerance_mm": 0.01},
|
|
}], parent_facts)[0]
|
|
evidence = result.get("evidence") if isinstance(result.get("evidence"), dict) else {}
|
|
actual_count = evidence.get("actual_count")
|
|
if isinstance(actual_count, int):
|
|
return actual_count
|
|
matched = evidence.get("matched_cylindrical_faces")
|
|
if isinstance(matched, list):
|
|
return len(matched)
|
|
through = evidence.get("through_bores")
|
|
if isinstance(through, list):
|
|
return len(through)
|
|
return 0
|
|
|
|
def _claim_ids_for_requirements(self, task_id: str, requirement_ids: tuple[str, ...]) -> set[str]:
|
|
contract = self._requirements_contract(task_id) or {}
|
|
return {
|
|
str(claim.get("claim_id") or "")
|
|
for requirement in contract.get("requirements") or ()
|
|
if isinstance(requirement, dict) and requirement.get("requirement_id") in requirement_ids
|
|
for claim in requirement.get("acceptance_claims") or ()
|
|
if isinstance(claim, dict)
|
|
}
|
|
|
|
def _next_revision(self, task_id: str) -> str:
|
|
"""Allocate a monotonically increasing immutable revision ID.
|
|
|
|
Rollback changes the active lineage but never reuses an artifact
|
|
directory. The ledger is the authority for already allocated IDs,
|
|
including a directory published just before an interrupted CAS.
|
|
"""
|
|
values = [
|
|
int(str(event.get("revision_id") or "").removeprefix("rev_"))
|
|
for event in self.repository.ledger_events(task_id)
|
|
if event.get("event") == "accepted"
|
|
and str(event.get("revision_id") or "").startswith("rev_")
|
|
and str(event.get("revision_id") or "").removeprefix("rev_").isdigit()
|
|
]
|
|
return f"rev_{max(values, default=0) + 1:03d}"
|
|
|
|
def _requirements_contract(self, task_id: str, state: TaskState | None = None) -> dict[str, Any] | None:
|
|
state = state or self.repository.get_state(task_id)
|
|
if state is None or not state.requirements_contract_path:
|
|
return None
|
|
return self.artifacts.read_requirements_contract(task_id, state.requirements_contract_path)
|
|
|
|
@staticmethod
|
|
def _final_review_path(revision_id: str) -> str:
|
|
# Revisions are manifest-sealed when a candidate becomes a checkpoint.
|
|
# Final review evidence therefore has its own immutable namespace.
|
|
return f"reviews/final/{revision_id}/final-review.json"
|
|
|
|
def _commit_invocation(self, state: TaskState, events: list[dict[str, Any]], invocation: Any, result: dict[str, Any]) -> bool:
|
|
"""Commit state/outbox and its idempotent result in one SQLite transaction."""
|
|
return self.repository.compare_and_swap(
|
|
state,
|
|
events=events,
|
|
invocation_id=invocation.invocation_id,
|
|
invocation_result=result,
|
|
)
|
|
|
|
@staticmethod
|
|
def _key(task_id: str, kind: str, head: str, value: dict[str, Any]) -> str:
|
|
encoded = json.dumps(value, ensure_ascii=True, sort_keys=True, separators=(",", ":"))
|
|
return sha256(f"{task_id}|{kind}|{head}|{encoded}".encode("utf-8")).hexdigest()
|
|
|
|
@staticmethod
|
|
def _failure_class_fingerprint(active_revision: str, atomic_id: str, code: ErrorCode) -> str:
|
|
return sha256(f"{active_revision}|{atomic_id}|{code.value}".encode("utf-8")).hexdigest()
|
|
|
|
def _failure_class_events(self, task_id: str, fingerprint: str) -> list[dict[str, Any]]:
|
|
return [
|
|
event for event in self.repository.ledger_events(task_id)
|
|
if event.get("failure_class_fingerprint") == fingerprint
|
|
]
|
|
|
|
@staticmethod
|
|
def _stale() -> WorkflowError:
|
|
return WorkflowError(ErrorCode.STALE_WORKING_HEAD, "Task state changed before this command could commit.")
|
|
|
|
@staticmethod
|
|
def _runtime_error(error: Exception) -> WorkflowError:
|
|
message = str(error)
|
|
code = (
|
|
ErrorCode.RUNTIME_PRECONDITION_FAILED
|
|
if "RUNTIME_PRECONDITION_FAILED" in message
|
|
else ErrorCode.RUNTIME_CONTRACT_INVALID
|
|
if "RUNTIME_CONTRACT_INVALID" in message or "CDSL schema violation" in message
|
|
else ErrorCode.RUNTIME_EXECUTION_FAILURE
|
|
if "RUNTIME_EXECUTION_FAILURE" in message
|
|
else ErrorCode.AUTHOR_FORMAT_INVALID
|
|
)
|
|
return WorkflowError(code, message[:1000])
|
|
|
|
def _park_for_storage_retry(self, state: TaskState, *, event: str, message: str) -> Rejected:
|
|
"""Persist a recoverable artifact failure without changing CAD evidence."""
|
|
return self._park_retry(state, ErrorCode.STORAGE_FAILURE, event=event, message=message)
|
|
|
|
def _park_retry(
|
|
self,
|
|
state: TaskState,
|
|
code: ErrorCode,
|
|
*,
|
|
event: str,
|
|
message: str,
|
|
details: dict[str, Any] | None = None,
|
|
) -> Rejected:
|
|
waiting = transition(state, "waiting_retry", error=code)
|
|
payload = {
|
|
"event": event,
|
|
"code": code.value,
|
|
"message": message[:1000],
|
|
**(details or {}),
|
|
}
|
|
if not self.repository.compare_and_swap(waiting, events=[{
|
|
**payload,
|
|
}]):
|
|
return Rejected(self._stale())
|
|
return Rejected(WorkflowError(
|
|
code,
|
|
"Candidate render evidence is temporarily unavailable; the checkpoint is preserved."
|
|
if code == ErrorCode.RENDER_SERVICE_UNAVAILABLE
|
|
else "The CAD runtime failed after validation; retry will resume from the preserved checkpoint."
|
|
if code == ErrorCode.RUNTIME_EXECUTION_FAILURE
|
|
else "Candidate artifact storage is temporarily unavailable; the checkpoint is preserved.",
|
|
retryable=True,
|
|
))
|