Files
cdsl-cad/backend/app/cad_agent/application/workflow.py
T
2026-09-01 16:37:27 +08:00

1537 lines
88 KiB
Python

"""LLM turn coordinator for protocol v3.
It selects only which schema is visible from the persisted state. It never
derives a CAD operation from requirements; that choice is always an author
tool call handled by ``ActionCommandHandler``.
"""
from __future__ import annotations
import json
import secrets
from hashlib import sha256
from dataclasses import dataclass
from typing import Any, AsyncIterator, TypeVar
from pydantic import BaseModel
from app.cad_agent.application.action_handlers import ActionCommandHandler
from app.cad_agent.application.llm_contracts import (
CandidateReview, EmptyCommand, FinalReview, GeometryConclusion, ImageObservation, NextAction,
RequirementsAuthorOutput, RollbackCheckpoint, StatelessCandidateReview,
StatelessFinalReview, StatelessGeometryConclusion, StatelessNextAction, StatelessRollbackCheckpoint,
StatelessTopologyRequest, TopologyRequest,
canonical_json_object, canonical_validate, canonical_validate_schema,
stateless_final_review_schema,
stateless_next_action_schema, stateless_rollback_checkpoint_schema,
raw_arguments_hash, validate_one_tool_call,
)
from app.cad_agent.application.requirements import RequirementsCommandHandler
from app.cad_agent.application.results import Accepted, Rejected, Waiting
from app.cad_agent.domain.errors import ErrorCode, WorkflowError
from app.cad_agent.domain.operation_contract import fragment_schema
from app.cad_agent.domain.state import TaskPhase, TaskState, retry_resume_event, transition
from app.cad_agent.ports import AdapterUnavailable, ArtifactStore, CadRuntime, ModelGateway, ReviewGateway, TaskRepository
T = TypeVar("T", bound=BaseModel)
@dataclass(frozen=True, slots=True)
class ModelIdentity:
provider_id: str
model_id: str
@dataclass(frozen=True, slots=True)
class WorkflowConfig:
max_turns: int
format_error_limit: int
author_fallbacks: tuple[ModelIdentity, ...] = ()
max_author_turns: int | None = None
max_reviewer_turns: int | None = None
max_model_calls: int | None = None
@dataclass(slots=True)
class _ModelCallBudget:
"""Task-scoped model-call caps, including calls from a prior resume."""
max_author_turns: int | None
max_reviewer_turns: int | None
max_model_calls: int | None
author_calls: int = 0
reviewer_calls: int = 0
@classmethod
def from_usage(cls, config: WorkflowConfig, records: list[dict[str, Any]]) -> "_ModelCallBudget":
reviewer_calls = sum(1 for record in records if record.get("role") == "reviewer")
return cls(
max_author_turns=config.max_author_turns,
max_reviewer_turns=config.max_reviewer_turns,
max_model_calls=config.max_model_calls,
author_calls=len(records) - reviewer_calls,
reviewer_calls=reviewer_calls,
)
@property
def total_calls(self) -> int:
return self.author_calls + self.reviewer_calls
def exhausted(self, actor: str) -> bool:
return (
(actor == "author" and self.max_author_turns is not None and self.author_calls >= self.max_author_turns)
or (actor == "reviewer" and self.max_reviewer_turns is not None and self.reviewer_calls >= self.max_reviewer_turns)
or (self.max_model_calls is not None and self.total_calls >= self.max_model_calls)
)
def record_attempt(self, actor: str) -> None:
if actor == "reviewer":
self.reviewer_calls += 1
else:
self.author_calls += 1
def payload(self) -> dict[str, int | None]:
return {
"author_calls": self.author_calls,
"reviewer_calls": self.reviewer_calls,
"total_calls": self.total_calls,
"max_author_turns": self.max_author_turns,
"max_reviewer_turns": self.max_reviewer_turns,
"max_model_calls": self.max_model_calls,
}
class WorkflowCoordinator:
def __init__(
self,
config: WorkflowConfig,
repository: TaskRepository,
artifacts: ArtifactStore,
runtime: CadRuntime,
model_gateway: ModelGateway,
review_gateway: ReviewGateway,
requirements: RequirementsCommandHandler,
actions: ActionCommandHandler,
) -> None:
self.config = config
self.repository = repository
self.artifacts = artifacts
self.runtime = runtime
self.model_gateway = model_gateway
self.review_gateway = review_gateway
self.requirements = requirements
self.actions = actions
def create_task(
self,
task_id: str,
request: str,
*,
source_blocks: list[dict[str, Any]] | None = None,
image_inputs: list[dict[str, str]] | None = None,
) -> TaskState:
# The immutable source artifact is safe to create before SQLite state:
# an interrupted creation leaves only an unreferenced directory, never
# a runnable task without its source index.
self.artifacts.initialize_task(task_id, request, source_blocks=source_blocks, image_inputs=image_inputs)
return self.repository.create_task(task_id, request)
def resume(self, task_id: str) -> bool:
state = self.repository.get_state(task_id)
if state is None or state.phase != TaskPhase.WAITING_RETRY:
return False
event = retry_resume_event(state)
if not event:
return False
next_state = transition(state, event)
return self.repository.compare_and_swap(next_state, events=[
{
"event": "workflow_resumed",
"from_phase": state.phase.value,
"retry_from_phase": state.retry_from_phase.value if state.retry_from_phase else "",
"to_phase": next_state.phase.value,
},
])
def resume_with_user_clarification(self, task_id: str, clarification: str, *, message_id: str) -> bool:
"""Resume a requirements pause on the same task with durable input."""
state = self.repository.get_state(task_id)
if state is None or state.phase != TaskPhase.WAITING_FOR_USER:
return False
clarification_request = self.artifacts.read_json(task_id, state.clarification_path) if state.clarification_path else None
if not isinstance(clarification_request, dict) or not str(clarification_request.get("question") or "").strip():
return False
text = clarification.strip()
if not text:
return False
digest = sha256(f"{message_id}:{text}".encode("utf-8")).hexdigest()[:16]
clarification_path = f"documents/user-clarification-{digest}.json"
try:
self.artifacts.write_json_once(task_id, clarification_path, {
"schema_version": "cad.user-clarification.v1",
"task_id": task_id,
"message_id": message_id,
"text": text,
})
except OSError:
return False
resumed = transition(state, "requirements_clarified", clarification_path="")
return self.repository.compare_and_swap(resumed, events=[{
"event": "user_clarification_received",
"message_id": message_id,
"clarification_path": clarification_path,
"response_sha256": sha256(text.encode("utf-8")).hexdigest(),
}])
async def run(self, *, task_id: str, author: ModelIdentity, reviewer: ModelIdentity) -> AsyncIterator[tuple[str, dict[str, Any]]]:
feedback: list[dict[str, Any]] = []
format_errors: dict[str, int] = {}
transport_attempted: set[str] = set()
# Observations are request-scoped, read-only facts. A restarted run
# deliberately has to fetch them again before authoring a selector-
# bound feature.
action_observations: dict[str, set[str]] = {}
active_author = author
max_turns = self.config.max_turns
call_budget = _ModelCallBudget.from_usage(
self.config,
self.repository.usage_summary(task_id).get("records", []),
)
try:
initial = self.repository.get_state(task_id)
if initial is not None:
referenced = {initial.candidate_stage_id} if initial.candidate_stage_id else set()
# Candidate rejection/build-failure evidence is itself an
# immutable repair input. Once the state transition clears
# ``candidate_stage_id``, retain that stage through restart
# based on its committed ledger reference.
referenced.update(
str(event["stage_id"])
for event in self.repository.ledger_events(task_id)
if event.get("event") in {
"candidate_rejected", "candidate_build_failed", "candidate_recovery_failed", "candidate_recovered_rejected",
}
and isinstance(event.get("stage_id"), str)
and event["stage_id"]
)
try:
self.artifacts.recover_staged_candidates(task_id, referenced)
except Exception as error:
yield self._storage_failure(task_id, str(error))
return
for _turn in range(max_turns):
try:
self._sync_action_ledger(task_id)
except Exception as error:
yield self._storage_failure(task_id, str(error))
return
state = self.repository.get_state(task_id)
if state is None:
yield "task_terminal", {"taskId": task_id, "lifecycle": "failed", "code": ErrorCode.STORAGE_FAILURE.value, "message": "V3 task state is unavailable."}
return
if state.requirements_contract_path and state.phase not in {TaskPhase.COMPLETED, TaskPhase.FAILED, TaskPhase.CANCELLED}:
try:
self.requirements.ensure_rendered_contract_views(task_id, state)
except Exception as error:
yield self._service_failure(
task_id,
state,
WorkflowError(ErrorCode.STORAGE_FAILURE, str(error)[:1000], retryable=True),
)
return
if state.phase == TaskPhase.COMPLETED:
yield "task_terminal", self._projected_terminal(task_id, state)
return
if state.phase == TaskPhase.CANCELLED:
yield "task_terminal", {
"taskId": task_id,
"lifecycle": "cancelled",
"revisionId": state.active_revision,
"code": ErrorCode.CANCELLED.value,
}
return
if state.phase == TaskPhase.WAITING_FOR_USER:
yield "task_terminal", self.waiting_for_user_terminal(task_id, state)
return
if state.phase in {TaskPhase.FAILED, TaskPhase.WAITING_RETRY}:
yield "task_terminal", self._projected_terminal(task_id, state)
return
if state.phase == TaskPhase.CANDIDATE_BUILDING:
recovered = self.actions.recover_candidate_build(task_id)
yield "candidate_result", {"taskId": task_id, "status": "success" if isinstance(recovered, Accepted) else "error", "result": self._result_payload(recovered), "recovered": True}
if isinstance(recovered, Rejected):
yield self._service_failure(task_id, state, recovered.error)
return
continue
if state.phase == TaskPhase.DRAFTING_REQUIREMENTS:
image_paths = self.artifacts.source_image_paths(task_id)
if image_paths and self.artifacts.read_json(task_id, "documents/image-observation.json") is None:
terminal = self._call_budget_terminal(task_id, state, call_budget, actor="reviewer")
if terminal:
yield terminal
return
call_budget.record_attempt("reviewer")
observation = await self._observe_images(task_id, reviewer, image_paths)
if isinstance(observation, WorkflowError):
if observation.code == ErrorCode.AUTHOR_FORMAT_INVALID:
observation = WorkflowError(
ErrorCode.REVIEW_SERVICE_UNAVAILABLE,
"Image observation did not return the required structured format.",
field_errors=observation.field_errors,
retryable=True,
)
yield self._service_failure(task_id, state, observation)
return
try:
self.artifacts.write_json_once(task_id, "documents/image-observation.json", {
"schema_version": "cad.image-observation.v3",
**observation.model_dump(mode="json"),
})
except OSError as error:
yield self._storage_failure(task_id, str(error))
return
observed_state = transition(state, "image_observed")
self.repository.compare_and_swap(observed_state, events=[{
"event": "image_observation_ready",
"path": "documents/image-observation.json",
"image_count": len(image_paths),
}])
yield "image_observation", {"taskId": task_id, "status": "success", "path": "documents/image-observation.json"}
continue
spec_schema = self.requirements.spec_schema()
tools = [self._tool("submit_requirements_spec", spec_schema)]
terminal = self._call_budget_terminal(task_id, state, call_budget, actor="author")
if terminal:
yield terminal
return
call_budget.record_attempt("author")
result = await self._author_turn(task_id, active_author, tools, feedback)
if isinstance(result, WorkflowError):
if result.code == ErrorCode.AUTHOR_FORMAT_INVALID:
terminal = self._format_failure(task_id, state, "author_turn", result, format_errors, feedback)
yield "tool_call", {"taskId": task_id, "tool": "author_turn", "status": "error", "result": result.payload()}
if terminal:
yield terminal
return
continue
active_author, terminal = self._transport_or_failure(task_id, state, result, active_author, transport_attempted)
if terminal:
yield terminal
return
continue
name, raw, usage = result
validation = canonical_validate(raw, RequirementsAuthorOutput)
dynamic_error = canonical_validate_schema(raw, spec_schema) if not isinstance(validation, WorkflowError) else None
if dynamic_error is not None:
validation = dynamic_error
if isinstance(validation, WorkflowError):
terminal = self._requirements_format_failure(task_id, state, validation, format_errors, feedback)
yield "tool_call", self._event(task_id, name, validation.payload(), "error", usage)
if terminal:
yield terminal
return
continue
invocation_id = self._invocation_id(task_id)
command = self.requirements.submit_spec(task_id, validation, invocation_id=invocation_id)
if isinstance(command, Rejected):
terminal = self._requirements_rejection(task_id, state, command.error, format_errors, feedback)
yield "tool_call", self._event(task_id, name, command.error.payload(), "error", usage)
if terminal:
yield terminal
return
continue
yield "requirements_ready", self._event(task_id, name, self._result_payload(command), "waiting" if isinstance(command, Waiting) else "success", usage)
feedback = []
continue
if state.phase == TaskPhase.AWAITING_ACTION:
contract = self._requirements_contract(task_id, state) or {}
requirement_ids = [str(item.get("requirement_id") or "") for item in contract.get("requirements") or () if isinstance(item, dict) and item.get("requirement_id")]
action_schema = stateless_next_action_schema(list(self.actions.available_atomic_ids(task_id, state)))
tools = self._recovery_tools(task_id, state)
if not tools:
if self._can_complete(task_id, state):
tools = [self._tool("complete_task", EmptyCommand)]
elif self.actions.repair_action_ready(task_id, state):
tools = [self._tool("propose_next_action", action_schema)]
terminal = self._call_budget_terminal(task_id, state, call_budget, actor="author")
if terminal:
yield terminal
return
call_budget.record_attempt("author")
result = await self._author_turn(task_id, active_author, tools, feedback)
if isinstance(result, WorkflowError):
if result.code == ErrorCode.AUTHOR_FORMAT_INVALID:
terminal = self._format_failure(task_id, state, "author_turn", result, format_errors, feedback)
yield "tool_call", {"taskId": task_id, "tool": "author_turn", "status": "error", "result": result.payload()}
if terminal:
yield terminal
return
continue
active_author, terminal = self._transport_or_failure(task_id, state, result, active_author, transport_attempted)
if terminal:
yield terminal
return
continue
name, raw, usage = result
if name == "complete_task":
validation = canonical_validate(raw, EmptyCommand)
if isinstance(validation, WorkflowError):
terminal = self._format_failure(task_id, state, name, validation, format_errors, feedback)
yield "tool_call", self._event(task_id, name, validation.payload(), "error", usage)
if terminal:
yield terminal
return
continue
command = self.actions.complete_task(task_id, invocation_id=self._invocation_id(task_id))
elif name == "record_geometry_conclusion":
validation = canonical_validate(raw, StatelessGeometryConclusion)
if isinstance(validation, WorkflowError):
terminal = self._format_failure(task_id, state, name, validation, format_errors, feedback)
yield "tool_call", self._event(task_id, name, validation.payload(), "error", usage)
if terminal:
yield terminal
return
continue
validation = GeometryConclusion(
working_head=state.working_head,
evidence_refs=list(self.actions.diagnostic_evidence_refs(task_id, state)),
root_cause=validation.root_cause,
decision=validation.decision,
corrective_intent=validation.corrective_intent,
)
command = self.actions.record_geometry_conclusion(task_id, validation, invocation_id=self._invocation_id(task_id))
elif name == "rollback_checkpoint":
rollback_schema = stateless_rollback_checkpoint_schema(list(self.actions.checkpoint_tokens(task_id, state)))
validation = canonical_validate(raw, StatelessRollbackCheckpoint)
dynamic_error = canonical_validate_schema(raw, rollback_schema) if not isinstance(validation, WorkflowError) else None
if dynamic_error is not None:
validation = dynamic_error
if isinstance(validation, WorkflowError):
terminal = self._format_failure(task_id, state, name, validation, format_errors, feedback)
yield "tool_call", self._event(task_id, name, validation.payload(), "error", usage)
if terminal:
yield terminal
return
continue
validation = RollbackCheckpoint(working_head=state.working_head, checkpoint_token=validation.checkpoint_token, reason=validation.reason)
command = self.actions.rollback_checkpoint(task_id, validation, invocation_id=self._invocation_id(task_id))
else:
validation = canonical_validate(raw, StatelessNextAction)
dynamic_error = canonical_validate_schema(raw, action_schema) if not isinstance(validation, WorkflowError) else None
if dynamic_error is not None:
validation = dynamic_error
if isinstance(validation, WorkflowError):
terminal = self._format_failure(task_id, state, name, validation, format_errors, feedback)
yield "tool_call", self._event(task_id, name, validation.payload(), "error", usage)
if terminal:
yield terminal
return
continue
validation = NextAction(
working_head=state.working_head,
intent=validation.intent,
requirement_ids=requirement_ids,
atomic_id=validation.operation,
expected_change=validation.expected_change,
)
command = self.actions.propose_next_action(task_id, validation, invocation_id=self._invocation_id(task_id))
if isinstance(command, Rejected):
terminal = self._model_rejection_or_service_failure(task_id, state, name, command.error, format_errors, feedback)
yield "action_selection", self._event(task_id, name, command.error.payload(), "error", usage)
if terminal:
yield terminal
return
continue
yield "action_selection", self._event(task_id, name, self._result_payload(command), "success", usage)
feedback = []
continue
if state.phase == TaskPhase.ACTION_PENDING:
observed = action_observations.setdefault(state.working_head, set())
tools = self._recovery_tools(task_id, state)
if not tools and self.actions.repair_action_ready(task_id, state):
tools = self._action_tools(task_id, state, observed)
terminal = self._call_budget_terminal(task_id, state, call_budget, actor="author")
if terminal:
yield terminal
return
call_budget.record_attempt("author")
result = await self._author_turn(task_id, active_author, tools, feedback)
if isinstance(result, WorkflowError):
if result.code == ErrorCode.AUTHOR_FORMAT_INVALID:
terminal = self._format_failure(task_id, state, "author_turn", result, format_errors, feedback)
yield "tool_call", {"taskId": task_id, "tool": "author_turn", "status": "error", "result": result.payload()}
if terminal:
yield terminal
return
continue
active_author, terminal = self._transport_or_failure(task_id, state, result, active_author, transport_attempted)
if terminal:
yield terminal
return
continue
name, raw, usage = result
if name == "record_geometry_conclusion":
diagnostic_schema = geometry_conclusion_schema(state.working_head, list(self.actions.diagnostic_evidence_refs(task_id, state)))
validation = canonical_validate(raw, GeometryConclusion)
dynamic_error = canonical_validate_schema(raw, diagnostic_schema) if not isinstance(validation, WorkflowError) else None
if dynamic_error is not None:
validation = dynamic_error
if isinstance(validation, WorkflowError):
terminal = self._format_failure(task_id, state, name, validation, format_errors, feedback)
yield "tool_call", self._event(task_id, name, validation.payload(), "error", usage)
if terminal:
yield terminal
return
continue
command = self.actions.record_geometry_conclusion(task_id, validation, invocation_id=self._invocation_id(task_id))
if isinstance(command, Rejected):
terminal = self._model_rejection_or_service_failure(task_id, state, name, command.error, format_errors, feedback)
yield "tool_call", self._event(task_id, name, command.error.payload(), "error", usage)
if terminal:
yield terminal
return
continue
yield "tool_call", self._event(task_id, name, self._result_payload(command), "success", usage)
feedback = []
continue
if name == "inspect_topology":
validation = canonical_validate(raw, StatelessTopologyRequest)
if isinstance(validation, WorkflowError):
terminal = self._format_failure(task_id, state, name, validation, format_errors, feedback)
yield "tool_call", self._event(task_id, name, validation.payload(), "error", usage)
if terminal:
yield terminal
return
continue
payload = self._topology_payload(task_id, state, validation.kind, validation.limit)
observed.add("topology")
yield "tool_call", self._event(task_id, name, payload, "success", usage)
feedback = [*feedback, {"role": "tool", "content": json.dumps({"tool": name, "result": payload}, ensure_ascii=False)}][-2:]
continue
fragment = canonical_json_object(raw)
if isinstance(fragment, WorkflowError):
validation_error = fragment
terminal = self._format_failure(task_id, state, name, validation_error, format_errors, feedback)
yield "tool_call", self._event(task_id, name, validation_error.payload(), "error", usage)
if terminal:
yield terminal
return
continue
command = self.actions.submit_cdsl_fragment(task_id, fragment, invocation_id=self._invocation_id(task_id))
if isinstance(command, Rejected):
# A candidate build may reject and advance back to
# ACTION_PENDING. That is repair work, not a no-side-
# effect schema/state retry.
after = self.repository.get_state(task_id)
if after is not None and after.version != state.version:
feedback = [self._feedback(command.error)]
yield "candidate_result", self._event(task_id, name, command.error.payload(), "error", usage)
continue
terminal = self._model_rejection_or_service_failure(task_id, state, name, command.error, format_errors, feedback)
yield "candidate_result", self._event(task_id, name, command.error.payload(), "error", usage)
if terminal:
yield terminal
return
continue
yield "candidate_result", self._event(task_id, name, self._result_payload(command), "success", usage)
feedback = []
continue
if state.phase == TaskPhase.CANDIDATE_REVIEW:
recovered = self.actions.recover_candidate_review(task_id)
if recovered is not None:
yield "candidate_review", {"taskId": task_id, "status": "success" if isinstance(recovered, Accepted) else "error", "result": self._result_payload(recovered), "recovered": True}
if isinstance(recovered, Rejected):
yield self._service_failure(task_id, state, recovered.error)
return
continue
terminal = self._call_budget_terminal(task_id, state, call_budget, actor="reviewer")
if terminal:
yield terminal
return
call_budget.record_attempt("reviewer")
review = await self._review_candidate(task_id, reviewer, state, feedback)
if isinstance(review, WorkflowError):
if review.code == ErrorCode.AUTHOR_FORMAT_INVALID:
terminal = self._format_failure(task_id, state, "review_candidate", review, format_errors, feedback, actor="reviewer")
yield "candidate_review", {"taskId": task_id, "status": "error", "result": review.payload()}
if terminal:
yield terminal
return
continue
yield self._service_failure(task_id, state, review)
return
candidate = self.artifacts.read_stage_json(task_id, state.candidate_stage_id, "candidate.json") or {}
action = state.pending_action
if action is None:
yield self._storage_failure(task_id, "Candidate action is unavailable during review.")
return
coverage = []
for item in candidate.get("claim_results") or ():
if not isinstance(item, dict):
continue
status = str(item.get("status") or "pending")
coverage.append({
"claim_id": str(item.get("claim_id") or ""),
"status": status if status in {"pass", "pending", "fail", "not_applicable"} else "fail",
"evidence_refs": [],
})
review = CandidateReview(
candidate_id=state.candidate_id,
working_head=action.working_head,
verdict=review.verdict,
claim_coverage=coverage,
evidence=review.evidence,
issues=review.issues,
)
command = self.actions.record_candidate_review(task_id, review, invocation_id=self._invocation_id(task_id))
if isinstance(command, Rejected):
terminal = self._model_rejection_or_service_failure(task_id, state, "review_candidate", command.error, format_errors, feedback, actor="reviewer")
yield "candidate_review", {"taskId": task_id, "status": "error", "result": command.error.payload()}
if terminal:
yield terminal
return
continue
yield "candidate_review", {"taskId": task_id, "status": "success", "result": self._result_payload(command)}
continue
if state.phase == TaskPhase.FINAL_VALIDATION:
recovered = self.actions.recover_final_review(task_id)
if recovered is not None:
if isinstance(recovered, Accepted) and recovered.payload.get("status") == "completed":
completed_state = self.repository.get_state(task_id)
if completed_state is not None:
try:
self._ensure_recovered_completion_result(task_id, completed_state)
except OSError as error:
yield self._storage_failure(task_id, str(error))
return
yield "completion_result_ready", {"taskId": task_id, "status": "success", "path": "completion-result.md"}
yield "final_review", {"taskId": task_id, "status": "success" if isinstance(recovered, Accepted) else "error", "result": self._result_payload(recovered), "recovered": True}
if isinstance(recovered, Rejected):
yield self._service_failure(task_id, state, recovered.error)
return
continue
terminal = self._call_budget_terminal(task_id, state, call_budget, actor="reviewer")
if terminal:
yield terminal
return
call_budget.record_attempt("reviewer")
review = await self._review_final(task_id, reviewer, state, feedback)
if isinstance(review, WorkflowError):
if review.code == ErrorCode.AUTHOR_FORMAT_INVALID:
terminal = self._format_failure(task_id, state, "review_final", review, format_errors, feedback, actor="reviewer")
yield "final_review", {"taskId": task_id, "status": "error", "result": review.payload()}
if terminal:
yield terminal
return
continue
yield self._service_failure(task_id, state, review)
return
facts = self.actions._facts(task_id, state.active_revision)
claim_results = self.actions._evaluate_claims(task_id, facts)
visual_decisions = iter(review.visual_claims)
coverage = []
for item in claim_results:
if item.get("deterministic"):
status = str(item.get("status") or "fail")
status = status if status in {"pass", "pending", "fail", "not_applicable"} else "fail"
else:
status = next(visual_decisions).status
coverage.append({"claim_id": str(item.get("claim_id") or ""), "status": status, "evidence_refs": []})
stateless_review = review
review = FinalReview(
working_head=state.working_head,
verdict=review.verdict,
claim_coverage=coverage,
evidence=review.evidence,
issues=review.issues,
)
will_complete = (
stateless_review.verdict == "pass"
and all(item.get("status") == "pass" for item in claim_results if item.get("deterministic"))
and all(item.status == "pass" for item in stateless_review.visual_claims)
)
if will_complete:
try:
self.requirements.write_completion_result(
task_id,
state,
claim_results=claim_results,
review=stateless_review.model_dump(mode="json"),
)
except OSError as error:
yield self._storage_failure(task_id, str(error))
return
command = self.actions.record_final_review(task_id, review, invocation_id=self._invocation_id(task_id))
if isinstance(command, Rejected):
terminal = self._model_rejection_or_service_failure(task_id, state, "review_final", command.error, format_errors, feedback, actor="reviewer")
yield "final_review", {"taskId": task_id, "status": "error", "result": command.error.payload()}
if terminal:
yield terminal
return
continue
if isinstance(command, Accepted) and command.payload.get("status") == "completed":
yield "completion_result_ready", {"taskId": task_id, "status": "success", "path": "completion-result.md"}
yield "final_review", {"taskId": task_id, "status": "success", "result": self._result_payload(command)}
continue
state = self.repository.get_state(task_id)
if state:
failed = transition(state, "failed", error=ErrorCode.FAILED_INTERNAL)
self.repository.compare_and_swap(failed, events=[{"event": "failed_internal", "message": "Workflow exceeded its finite turn limit."}])
yield "task_terminal", {"taskId": task_id, "lifecycle": "failed", "code": ErrorCode.FAILED_INTERNAL.value, "message": "Workflow exceeded its finite turn limit."}
except OSError as error:
yield self._storage_failure(task_id, str(error))
except Exception as error:
state = self.repository.get_state(task_id)
if state and state.phase not in {TaskPhase.COMPLETED, TaskPhase.FAILED, TaskPhase.CANCELLED}:
failed = transition(state, "failed", error=ErrorCode.FAILED_INTERNAL)
self.repository.compare_and_swap(failed, events=[{"event": "failed_internal", "message": str(error)[:1000]}])
yield "task_terminal", {"taskId": task_id, "lifecycle": "failed", "code": ErrorCode.FAILED_INTERNAL.value, "message": str(error)[:1000]}
finally:
# The database ledger is authoritative. A failed mirror write is
# retried by the next active workflow pass; do not replace a
# completed/cancelled durable result with a filesystem exception.
try:
self._sync_action_ledger(task_id)
except Exception:
pass
def _sync_action_ledger(self, task_id: str) -> None:
self.artifacts.sync_action_ledger(task_id, self.repository.ledger_events(task_id))
def _call_budget_terminal(
self,
task_id: str,
state: TaskState,
budget: _ModelCallBudget,
*,
actor: str,
) -> tuple[str, dict[str, Any]] | None:
if not budget.exhausted(actor):
return None
details = budget.payload()
details["next_actor"] = actor
failed = transition(state, "failed", error=ErrorCode.CALL_BUDGET_EXHAUSTED)
self.repository.compare_and_swap(failed, events=[{
"event": "call_budget_exhausted",
"code": ErrorCode.CALL_BUDGET_EXHAUSTED.value,
"message": "Configured model-call budget is exhausted before the workflow converged.",
**details,
}])
return "task_terminal", {
"taskId": task_id,
"lifecycle": "failed",
"code": ErrorCode.CALL_BUDGET_EXHAUSTED.value,
"message": "Configured model-call budget is exhausted before the workflow converged.",
"budget": details,
"blockerType": "call_budget_exhausted",
"userActionRequired": False,
}
def _storage_failure(self, task_id: str, message: str) -> tuple[str, dict[str, Any]]:
"""Park a nonterminal task when a durable artifact operation fails."""
state = self.repository.get_state(task_id)
if state is not None and state.phase not in {TaskPhase.COMPLETED, TaskPhase.FAILED, TaskPhase.CANCELLED, TaskPhase.WAITING_RETRY, TaskPhase.WAITING_FOR_USER}:
waiting = transition(state, "waiting_retry", error=ErrorCode.STORAGE_FAILURE)
self.repository.compare_and_swap(waiting, events=[{
"event": "waiting_retry",
"code": ErrorCode.STORAGE_FAILURE.value,
"message": message[:1000],
}])
return "task_terminal", {
"taskId": task_id,
"lifecycle": "waiting_retry",
"code": ErrorCode.STORAGE_FAILURE.value,
"message": message[:1000],
}
lifecycle = state.phase.value.lower() if state is not None else "failed"
return "task_terminal", {
"taskId": task_id,
"lifecycle": lifecycle,
"code": ErrorCode.STORAGE_FAILURE.value,
"message": message[:1000],
}
async def _author_turn(self, task_id: str, author: ModelIdentity, tools: list[dict[str, Any]], feedback: list[dict[str, Any]]) -> tuple[str, str, dict[str, int]] | WorkflowError:
if len(tools) != 1:
raise RuntimeError("Workflow state must expose exactly one author tool.")
tool = tools[0]
name = str((tool.get("function") or {}).get("name") or "")
if not name:
raise RuntimeError("Workflow exposed an unnamed author tool.")
messages = self._author_context(task_id, feedback)
try:
response = await self.model_gateway.call_tool(
messages=messages, tool=tool, provider_id=author.provider_id,
model_id=author.model_id, required_tool_name=name,
)
except AdapterUnavailable as error:
self.repository.record_usage(task_id, {
"prompt_tokens": 0,
"completion_tokens": 0,
"total_tokens": 0,
"usage_available": False,
"context_chars": len(json.dumps(messages, ensure_ascii=False)),
"tool": name,
"provider_id": author.provider_id,
"model_id": author.model_id,
"retry_reason": "provider_unavailable",
"cache_hit": False,
})
return WorkflowError(ErrorCode.AUTHOR_TRANSPORT_UNAVAILABLE, str(error)[:1000], retryable=True)
call = validate_one_tool_call(response["tool_calls"], name)
if isinstance(call, WorkflowError):
self._record_rejected_tool_calls(
task_id,
actor="author",
expected_tool=name,
tool_calls=response["tool_calls"],
schema=(tool.get("function") or {}).get("parameters"),
)
return call
_name, raw = call
self._record_tool_audit(
task_id,
actor="author",
tool=name,
raw_arguments=raw,
schema=(tool.get("function") or {}).get("parameters"),
)
usage = {
**response["usage"],
"context_chars": len(json.dumps(messages, ensure_ascii=False)),
"tool": name,
"provider_id": author.provider_id,
"model_id": author.model_id,
"raw_arguments_hash": raw_arguments_hash(raw),
"retry_reason": "",
"cache_hit": False,
}
self.repository.record_usage(task_id, usage)
return name, raw, usage
async def _review_candidate(self, task_id: str, reviewer: ModelIdentity, state: TaskState, feedback: list[dict[str, Any]] | None = None) -> StatelessCandidateReview | WorkflowError:
candidate = self.artifacts.read_stage_json(task_id, state.candidate_stage_id, "candidate.json")
action = state.pending_action
if not isinstance(candidate, dict) or action is None:
return WorkflowError(ErrorCode.STORAGE_FAILURE, "Candidate review facts are unavailable.", retryable=True)
return await self._review_tool(task_id, reviewer, "review_candidate", StatelessCandidateReview, {
"requirements": self._public_requirements(self._requirements_contract(task_id, state)),
"action": {"intent": action.intent, "expected_change": action.expected_change, "operation": action.atomic_id},
"candidate_facts": self._public_candidate(candidate),
"render_manifest": candidate.get("render_manifest") or {},
"instruction": "Review only whether the current checkpoint correctly performs the stated action. Deterministic facts are authoritative. Do not return task, action, candidate, requirement, claim, revision, head, or evidence identifiers.",
}, feedback=feedback)
async def _observe_images(self, task_id: str, reviewer: ModelIdentity, image_paths: list[str]) -> ImageObservation | WorkflowError:
return await self._review_tool(task_id, reviewer, "observe_images", ImageObservation, {
"source_requirements": self.artifacts.read_source_requirements(task_id),
"reference_image_paths": image_paths,
"instruction": (
"Inspect every supplied reference image once. Describe visible part geometry, view directions, readable dimensions, holes and profiles, confidence, assumptions, and uncertainties. "
"Do not create CAD operations and do not return attachment or runtime identifiers."
),
})
async def _review_final(self, task_id: str, reviewer: ModelIdentity, state: TaskState, feedback: list[dict[str, Any]] | None = None) -> StatelessFinalReview | WorkflowError:
facts = self.actions._facts(task_id, state.active_revision)
results = self.actions._evaluate_claims(task_id, facts)
visual_claims = [item for item in results if not item.get("deterministic")]
return await self._review_tool(task_id, reviewer, "review_final", StatelessFinalReview, {
"source_requirements": self.artifacts.read_source_requirements(task_id),
"requirements": self._public_requirements(self._requirements_contract(task_id, state)),
"deterministic_results": [self._public_claim_result(item) for item in results if item.get("deterministic")],
"visual_claims": [self._public_claim_result(item) for item in visual_claims],
"render_manifest": (facts.get("report") or {}).get("render_manifest") or {},
"reference_image_paths": self.artifacts.source_image_paths(task_id),
"instruction": "Review the final CAD renders against the original reference images and the ordered visual claims. Return exactly one visual_claims decision for each supplied visual claim, in the same order. Deterministic results are final. Do not return any runtime identifiers.",
}, schema=stateless_final_review_schema(len(visual_claims)), feedback=feedback)
async def _review_tool(self, task_id: str, reviewer: ModelIdentity, name: str, model_type: type[T], payload: dict[str, Any], *, schema: dict[str, Any] | None = None, feedback: list[dict[str, Any]] | None = None) -> T | WorkflowError:
if feedback:
payload = {**payload, "previous_schema_or_state_error": str(feedback[-1].get("content") or "")[:2_000]}
try:
tool = self._tool(name, schema or model_type)
kind = "image_observation" if name == "observe_images" else "candidate" if name == "review_candidate" else "final"
response = await self.review_gateway.review(kind=kind, payload=payload, tool=tool, provider_id=reviewer.provider_id, model_id=reviewer.model_id)
except AdapterUnavailable as error:
error_code = (
ErrorCode.RENDER_SERVICE_UNAVAILABLE
if str(error).startswith("RENDER_SERVICE_UNAVAILABLE:")
else ErrorCode.REVIEW_SERVICE_UNAVAILABLE
)
self.repository.record_usage(task_id, {
"prompt_tokens": 0,
"completion_tokens": 0,
"total_tokens": 0,
"usage_available": False,
"context_chars": len(json.dumps(payload, ensure_ascii=False)),
"tool": name,
"provider_id": reviewer.provider_id,
"model_id": reviewer.model_id,
"role": "reviewer",
"retry_reason": error_code.value.lower(),
"cache_hit": False,
})
return WorkflowError(error_code, str(error)[:1000], retryable=True)
check = validate_one_tool_call(response["tool_calls"], name)
if isinstance(check, WorkflowError):
self._record_rejected_tool_calls(
task_id,
actor="reviewer",
expected_tool=name,
tool_calls=response["tool_calls"],
schema=(tool.get("function") or {}).get("parameters"),
)
return check
_tool_name, raw = check
self._record_tool_audit(
task_id,
actor="reviewer",
tool=name,
raw_arguments=raw,
schema=(tool.get("function") or {}).get("parameters"),
)
self.repository.record_usage(task_id, {
**response["usage"],
"context_chars": len(json.dumps(payload, ensure_ascii=False)),
"tool": name,
"provider_id": reviewer.provider_id,
"model_id": reviewer.model_id,
"role": "reviewer",
"raw_arguments_hash": raw_arguments_hash(raw),
"retry_reason": "",
"cache_hit": False,
})
validated = canonical_validate(raw, model_type)
if isinstance(validated, WorkflowError):
return validated
dynamic_error = canonical_validate_schema(raw, schema) if schema is not None else None
return dynamic_error or validated # type: ignore[return-value]
def _record_tool_audit(
self,
task_id: str,
*,
actor: str,
tool: str,
raw_arguments: str,
schema: Any,
returned_tool: str | None = None,
single_allowed_call: bool = True,
) -> None:
"""Persist the raw-output validation and current state-binding facts.
Provider parsers are not a trust boundary. This stores a redacted,
diagnostic copy locally while release reports expose only the hash and
validation/binding summary.
"""
state = self.repository.get_state(task_id)
schema_error = canonical_validate_schema(raw_arguments, schema) if isinstance(schema, dict) else WorkflowError(
ErrorCode.RUNTIME_CONTRACT_INVALID,
"The exposed tool has no JSON Schema.",
)
parsed = canonical_json_object(raw_arguments)
supplied_head = parsed.get("working_head") if isinstance(parsed, dict) else None
topology = self.artifacts.read_topology(task_id, state.active_revision) if state and state.active_revision else None
pending = state.pending_action if state else None
contract_current = True
if pending is not None:
try:
contract_current = self.runtime.operation_contract(pending.atomic_id).get("contract_hash") == pending.contract_hash
except Exception:
contract_current = False
schema_head = self._schema_working_head(schema)
expected_head = schema_head or (state.working_head if state is not None else "")
# Candidate reviews are scoped to the action checkpoint that was used
# to build the candidate. The state has already advanced by the time
# the review runs, so current-state equality would reject a valid,
# schema-bound response. The dynamic schema is server-generated and
# canonical-validated above, making its const the authority here.
working_head_matches = not isinstance(supplied_head, str) or (bool(expected_head) and supplied_head == expected_head)
binding_valid = bool(state) and single_allowed_call and schema_error is None and working_head_matches and contract_current
self.repository.record_tool_audit(task_id, {
"schema_version": "cad.v3.tool-audit.v2",
"actor": actor,
"tool": tool,
"raw_arguments_hash": raw_arguments_hash(raw_arguments),
"redacted_arguments": self._redact_tool_arguments(parsed) if isinstance(parsed, dict) else None,
"canonical_schema_valid": schema_error is None,
"field_errors": list(schema_error.field_errors) if isinstance(schema_error, WorkflowError) else [],
"state_binding": {
"phase": state.phase.value if state else "",
"working_head": state.working_head if state else "",
"expected_working_head": expected_head,
"working_head_binding_source": "schema_const" if schema_head else "current_state",
"supplied_working_head": str(supplied_head or ""),
"working_head_matches": working_head_matches,
"active_revision": state.active_revision if state else "",
"contract_hash": pending.contract_hash if pending else "",
"contract_hash_current": contract_current,
"selector_snapshot_id": str((topology or {}).get("snapshot_id") or ""),
"binding_valid": binding_valid,
},
"returned_tool": returned_tool if returned_tool is not None else tool,
"single_allowed_call": single_allowed_call,
})
@staticmethod
def _schema_working_head(schema: Any) -> str | None:
"""Return a server-bound head from a dynamic top-level tool schema."""
properties = schema.get("properties") if isinstance(schema, dict) else None
working_head = properties.get("working_head") if isinstance(properties, dict) else None
value = working_head.get("const") if isinstance(working_head, dict) else None
return value if isinstance(value, str) else None
def _record_rejected_tool_calls(
self,
task_id: str,
*,
actor: str,
expected_tool: str,
tool_calls: list[dict[str, Any]],
schema: Any,
) -> None:
"""Audit every returned call even when the one-call gate rejects it."""
if not tool_calls:
self._record_tool_audit(
task_id,
actor=actor,
tool=expected_tool,
raw_arguments="",
schema=schema,
returned_tool="",
single_allowed_call=False,
)
return
for call in tool_calls:
function = call.get("function") if isinstance(call, dict) and isinstance(call.get("function"), dict) else {}
name = str(function.get("name") or "")
arguments = function.get("arguments")
raw = arguments if isinstance(arguments, str) else ""
self._record_tool_audit(
task_id,
actor=actor,
tool=expected_tool,
raw_arguments=raw,
schema=schema,
returned_tool=name,
single_allowed_call=False,
)
@staticmethod
def _redact_tool_arguments(value: Any) -> Any:
if isinstance(value, list):
return [WorkflowCoordinator._redact_tool_arguments(item) for item in value]
if not isinstance(value, dict):
return value
sensitive = {"api_key", "authorization", "credential", "credentials", "secret", "token", "password"}
return {
str(key): "[REDACTED]" if str(key).casefold() in sensitive else WorkflowCoordinator._redact_tool_arguments(item)
for key, item in value.items()
}
def _action_tools(self, task_id: str, state: TaskState, observed: set[str] | None = None) -> list[dict[str, Any]]:
action = state.pending_action
if action is None:
return []
contract = self.runtime.operation_contract(action.atomic_id)
selector_shape = str((contract.get("fragment_shape") or {}).get("selector_tokens") or "forbidden")
seen = observed or set()
topology = self.artifacts.read_topology(task_id, state.active_revision)
tokens = self.runtime.selector_tokens(topology)
eligible_tokens = self._selector_tokens_for_contract(contract, tokens)
if selector_shape == "required" and len(eligible_tokens) > 16 and "topology" not in seen:
return [self._tool("inspect_topology", StatelessTopologyRequest)]
references = self.runtime.reference_tokens(self.artifacts.read_active_cdsl(task_id, state.active_revision))
description = "Submit exactly one CDSL feature for the pending action."
if action.atomic_id.startswith("hole_"):
description += (
" Every feature.params.positions[].mm value is an absolute world-space mm point on the selected host face. "
"It is not a host-face-local offset; the server converts the world point to the selected face frame."
)
if selector_shape == "required":
description += (
" The root payload shape is {\"feature\":{\"atomic_id\":\"...\","
"\"selector_tokens\":[\"opaque topology token\"],\"params\":{...}}}. "
"feature.selector_tokens is required author input: copy one of the opaque tokens from "
"inspect_topology; the server resolves it to the host after validation."
)
fragment = {"type": "function", "function": {"name": "submit_cdsl_fragment", "description": description, "parameters": fragment_schema(contract, selector_tokens=eligible_tokens, reference_tokens=list(references))}}
return [fragment]
def _recovery_tools(self, task_id: str, state: TaskState) -> list[dict[str, Any]]:
if not state.repair_required:
return []
if self.actions.rollback_available(task_id, state):
return [self._tool("rollback_checkpoint", stateless_rollback_checkpoint_schema(list(self.actions.checkpoint_tokens(task_id, state))))]
if self.actions.repair_action_ready(task_id, state):
return []
evidence_refs = list(self.actions.diagnostic_evidence_refs(task_id, state))
if not evidence_refs:
return []
return [self._tool("record_geometry_conclusion", StatelessGeometryConclusion)]
def _author_context(self, task_id: str, feedback: list[dict[str, Any]]) -> list[dict[str, Any]]:
state = self.repository.get_state(task_id)
if state is None:
return []
if state.phase == TaskPhase.DRAFTING_REQUIREMENTS:
content = {
"protocol": "cad.v3.spec.v1",
"source_requirements": self.artifacts.read_source_requirements(task_id),
"image_observation": self.artifacts.read_json(task_id, "documents/image-observation.json"),
"user_clarifications": self._user_clarifications(task_id),
"instruction": (
"Return one complete bounded requirements specification. Use deterministic claims only when the supplied registry can execute them; otherwise use a scoped visual claim. "
"Unspecified design choices are assumptions and must not block generation. Return clarification only when two quoted source statements cannot both be followed and the user must choose; ask exactly one question. "
"Never return source, task, draft, requirement, claim, revision, candidate, action, head, or evidence identifiers. Do not solve semantic conflicts by changing a user value."
),
}
else:
contract = self._requirements_contract(task_id, state) or {}
compact = [{
"statement": item.get("statement"),
"acceptance_claims": [
{
"claim_kind": claim.get("claim_kind"),
"expected": claim.get("expected"),
}
for claim in item.get("acceptance_claims") or ()
if isinstance(claim, dict)
],
} for item in contract.get("requirements") or () if isinstance(item, dict)]
action = state.pending_action
operation = self.runtime.operation_contract(action.atomic_id) if action is not None else None
selector_shape = str((operation or {}).get("fragment_shape", {}).get("selector_tokens") or "forbidden")
instruction = "Choose only the next atomically verifiable action or complete when every requirement is proven. The service will not choose CAD operations for you."
if state.repair_required:
instruction = "A prior candidate or final review requires repair. Use the current server evidence to record a geometry conclusion, then either choose a new action or, after a rollback conclusion, request an earlier checkpoint. The service will not choose CAD operations for you."
operation_payload = self._operation_payload(task_id, state) if action is not None else None
selector_summary = []
if action is not None and selector_shape == "required":
tokens = self.runtime.selector_tokens(self.artifacts.read_topology(task_id, state.active_revision))
allowed = set(self._selector_tokens_for_contract(operation or {}, tokens))
selector_summary = [
{"token": token, "kind": value.get("kind"), "geometry": {key: value.get("geometry", {}).get(key) for key in ("center_mm", "normal", "bbox_mm", "surface_type") if key in value.get("geometry", {})}}
for token, value in tokens.items() if token in allowed
][:16]
instruction = "The exact operation contract and eligible selector summary are attached. Submit one fragment; call inspect_topology only when the selector summary is marked truncated."
content = {"protocol": "cad.v3", "phase": state.phase.value, "requirements": compact, "verification_warnings": contract.get("verification_warnings") or [], "claim_coverage": [self._public_claim_result(item) for item in self.actions.claim_summary(task_id, state)], "model_summary": self.actions.model_summary(task_id, state), "pending_action": self._public_pending_context(state), "operation_contract": self._public_operation_payload(operation_payload), "selector_summary": selector_summary, "selector_summary_truncated": bool(action is not None and selector_shape == "required" and len(self._selector_tokens_for_contract(operation or {}, self.runtime.selector_tokens(self.artifacts.read_topology(task_id, state.active_revision)))) > len(selector_summary)), "recent_failures": self._recent_failure_constraints(task_id, state), "repair_diagnostics": self.actions.repair_diagnostics(task_id, state), "rollback_checkpoints": list(self.actions.checkpoint_tokens(task_id, state)) if self.actions.rollback_available(task_id, state) else [], "instruction": instruction}
messages: list[dict[str, Any]] = [{"role": "system", "content": "You are the autonomous CAD author. Use exactly one offered structured tool call. Never emit Markdown plans or free-form JSON."}, {"role": "user", "content": json.dumps(content, ensure_ascii=False)}]
return [*messages, *feedback[-2:]]
def _user_clarifications(self, task_id: str) -> list[dict[str, str]]:
clarifications: list[dict[str, str]] = []
for event in self.repository.ledger_events(task_id):
if event.get("event") != "user_clarification_received":
continue
path = str(event.get("clarification_path") or "")
payload = self.artifacts.read_json(task_id, path) if path else None
text = str((payload or {}).get("text") or "").strip()
if text:
clarifications.append({"message_id": str((payload or {}).get("message_id") or ""), "text": text})
return clarifications
def waiting_for_user_terminal(self, task_id: str, state: TaskState) -> dict[str, Any]:
"""Expose the persisted requirement question when a task is parked.
The clarification artifact is the durable source of the single human
decision needed to continue this task.
"""
clarification = self.artifacts.read_json(task_id, state.clarification_path) if state.clarification_path else None
question = str((clarification or {}).get("question") or "").strip()
questions = [question] if question else []
if not questions:
raise RuntimeError("WAITING_FOR_USER requires at least one answerable requirements question")
message = f"Requirements need a user decision. {questions[0]}"
payload: dict[str, Any] = {
"taskId": task_id,
"lifecycle": TaskPhase.WAITING_FOR_USER.value.lower(),
"revisionId": state.active_revision,
"code": state.last_error.value if state.last_error else ErrorCode.WAITING_FOR_USER.value,
"message": message,
"questions": questions,
"clarificationPath": state.clarification_path,
"blockerType": "requirements_ambiguity",
"userActionRequired": True,
}
return payload
def _requirements_contract(self, task_id: str, state: TaskState | None) -> dict[str, Any] | None:
if state is None or not state.requirements_contract_path:
return None
return self.artifacts.read_requirements_contract(task_id, state.requirements_contract_path)
def _ensure_recovered_completion_result(self, task_id: str, state: TaskState) -> None:
result_path = self.artifacts.artifact_path(task_id, "completion-result.md")
if result_path.is_file():
return
facts = self.actions._facts(task_id, state.active_revision)
claim_results = self.actions._evaluate_claims(task_id, facts)
raw_review = self.artifacts.read_json(task_id, f"reviews/final/{state.active_revision}/final-review.json") or {}
coverage = {
str(item.get("claim_id") or ""): item
for item in raw_review.get("claim_coverage") or ()
if isinstance(item, dict)
}
visual_claims = [
{
"status": str(coverage.get(str(item.get("claim_id") or ""), {}).get("status") or "fail"),
"evidence": "; ".join(str(value) for value in raw_review.get("evidence") or ()) or "Recovered final review decision.",
}
for item in claim_results
if not item.get("deterministic")
]
self.requirements.write_completion_result(
task_id,
state,
claim_results=claim_results,
review={"visual_claims": visual_claims},
)
@staticmethod
def _public_requirements(contract: dict[str, Any] | None) -> list[dict[str, Any]]:
return [
{
"statement": str(item.get("statement") or ""),
"assumptions": list(item.get("assumptions") or []),
"acceptance_claims": [
{
"claim_kind": str(claim.get("claim_kind") or ""),
"expected": claim.get("expected") or {},
"verification_mode": str(claim.get("verification_mode") or ""),
}
for claim in item.get("acceptance_claims") or ()
if isinstance(claim, dict)
],
}
for item in (contract or {}).get("requirements") or ()
if isinstance(item, dict)
]
@staticmethod
def _public_claim_result(item: dict[str, Any]) -> dict[str, Any]:
return {
key: value
for key, value in item.items()
if key not in {"claim_id", "requirement_id", "evidence_refs"}
}
@classmethod
def _public_candidate(cls, candidate: dict[str, Any]) -> dict[str, Any]:
return {
"claim_results": [cls._public_claim_result(item) for item in candidate.get("claim_results") or () if isinstance(item, dict)],
"operation_verifier_results": [cls._public_claim_result(item) for item in candidate.get("operation_verifier_results") or () if isinstance(item, dict)],
"health": candidate.get("health") or {},
"model_summary": candidate.get("model_summary") or {},
}
@staticmethod
def _public_pending_context(state: TaskState) -> dict[str, Any] | None:
action = state.pending_action
return {"intent": action.intent, "operation": action.atomic_id, "expected_change": action.expected_change} if action else None
@staticmethod
def _public_operation_payload(payload: dict[str, Any] | None) -> dict[str, Any] | None:
if not isinstance(payload, dict):
return None
return {
"operation": payload.get("atomic_id"),
"contract": payload.get("contract"),
"fragment_schema": payload.get("fragment_schema"),
}
def _projected_terminal(self, task_id: str, state: TaskState) -> dict[str, Any]:
projection = self.repository.get_task_projection(task_id) or {}
return {
"taskId": task_id,
"lifecycle": str(projection.get("lifecycle") or state.phase.value.lower()),
"revisionId": state.active_revision,
"code": state.last_error.value if state.last_error else "",
"message": str(projection.get("message") or ("CAD generation completed." if state.phase == TaskPhase.COMPLETED else "CAD generation stopped.")),
"questions": projection.get("questions") or [],
"issues": projection.get("issues") or [],
"blockerType": str(projection.get("blocker_type") or ""),
"userActionRequired": bool(projection.get("user_action_required")),
"verificationStatus": str(projection.get("verification_status") or "verified"),
"verificationWarnings": projection.get("verification_warnings") or [],
}
def _operation_payload(self, task_id: str, state: TaskState) -> dict[str, Any]:
action = state.pending_action
assert action is not None
contract = self.runtime.operation_contract(action.atomic_id)
topology = self.artifacts.read_topology(task_id, state.active_revision)
tokens = self.runtime.selector_tokens(topology)
references = self.runtime.reference_tokens(self.artifacts.read_active_cdsl(task_id, state.active_revision))
return {"working_head": state.working_head, "atomic_id": action.atomic_id, "contract_hash": contract["contract_hash"], "contract": contract, "fragment_schema": fragment_schema(contract, selector_tokens=self._selector_tokens_for_contract(contract, tokens), reference_tokens=list(references))}
def _recent_failure_constraints(self, task_id: str, state: TaskState) -> list[dict[str, Any]]:
constraints: list[dict[str, Any]] = []
for event in reversed(self.repository.ledger_events(task_id)):
if event.get("checkpoint_revision") != state.active_revision:
continue
if not event.get("normalized_error_code"):
continue
constraints.append({
"atomic_id": str(event.get("atomic_id") or ""),
"normalized_error_code": str(event.get("normalized_error_code") or ""),
"fragment_hash": str(event.get("fragment_hash") or ""),
"prohibited_exact_fingerprint": str(event.get("failure_exact_fingerprint") or ""),
"attempt": int(event.get("attempt") or 1),
"message": str(event.get("message") or event.get("reason") or "")[:360],
})
if len(constraints) == 4:
break
return constraints
@staticmethod
def _selector_tokens_for_contract(contract: dict[str, Any], tokens: dict[str, dict[str, Any]]) -> list[str]:
"""Narrow dynamic selector enums to the current contract's kind."""
if str((contract.get("fragment_shape") or {}).get("selector_tokens") or "forbidden") != "required":
return []
policy = contract.get("selector_policy") if isinstance(contract.get("selector_policy"), dict) else {}
kind = str(policy.get("token_kind") or "")
return [token for token, value in tokens.items() if isinstance(value, dict) and value.get("kind") == kind]
def _topology_payload(self, task_id: str, state: TaskState, kind: str | None, limit: int) -> dict[str, Any]:
tokens = self.runtime.selector_tokens(self.artifacts.read_topology(task_id, state.active_revision))
values = [{"token": token, "kind": item["kind"], "geometry": {key: item["geometry"].get(key) for key in ("center_mm", "normal", "plane_normal", "bbox_mm", "radius_mm", "surface_type") if key in item["geometry"]}} for token, item in tokens.items() if not kind or item["kind"] == kind]
return {
"working_head": state.working_head,
"coordinate_system": "world_mm",
"tokens": values[:limit],
}
def _can_complete(self, task_id: str, state: TaskState) -> bool:
if not state.active_revision or state.repair_required:
return False
results = self.actions._evaluate_claims(task_id, self.actions._facts(task_id, state.active_revision))
return bool(results) and all(item.get("status") == "pass" for item in results if item.get("deterministic"))
def _transport_or_failure(self, task_id: str, state: TaskState, error: WorkflowError, author: ModelIdentity, attempted: set[str]) -> tuple[ModelIdentity, tuple[str, dict[str, Any]] | None]:
if error.code != ErrorCode.AUTHOR_TRANSPORT_UNAVAILABLE:
failed = transition(state, "failed", error=ErrorCode.FAILED_INTERNAL)
self.repository.compare_and_swap(failed, events=[{"event": "failed_internal", "message": error.message}])
return author, ("task_terminal", {"taskId": task_id, "lifecycle": "failed", "code": ErrorCode.FAILED_INTERNAL.value, "message": error.message})
attempted.add(author.provider_id)
# The adapter already performs bounded exponential retries for the
# active provider. The workflow permits one, and only one, provider
# failover before parking the durable task for explicit recovery.
fallback = next((candidate for candidate in self.config.author_fallbacks if candidate.provider_id not in attempted), None) if len(attempted) == 1 else None
if fallback is not None:
self.repository.append_outbox(task_id, {"event": "author_provider_failover", "from_provider": author.provider_id, "to_provider": fallback.provider_id})
return fallback, None
waiting = transition(state, "waiting_retry", error=ErrorCode.AUTHOR_TRANSPORT_UNAVAILABLE)
self.repository.compare_and_swap(waiting, events=[{"event": "waiting_retry", "code": ErrorCode.AUTHOR_TRANSPORT_UNAVAILABLE.value, "message": error.message}])
return author, ("task_terminal", {"taskId": task_id, "lifecycle": "waiting_retry", "code": ErrorCode.AUTHOR_TRANSPORT_UNAVAILABLE.value, "message": error.message})
def _service_failure(self, task_id: str, state: TaskState, error: WorkflowError) -> tuple[str, dict[str, Any]]:
waiting = transition(state, "waiting_retry", error=error.code)
self.repository.compare_and_swap(waiting, events=[{"event": "waiting_retry", "code": error.code.value, "message": error.message}])
return "task_terminal", {"taskId": task_id, "lifecycle": "waiting_retry", "code": error.code.value, "message": error.message}
def _model_rejection_or_service_failure(self, task_id: str, state: TaskState, name: str, error: WorkflowError, counters: dict[str, int], feedback: list[dict[str, Any]], *, actor: str = "author") -> tuple[str, dict[str, Any]] | None:
"""Bound no-side-effect model rejections; park real service failures."""
if error.code == ErrorCode.RUNTIME_CONTRACT_INVALID:
# Registry integrity is a deployment defect. Retrying a model with
# the same broken contract cannot repair it and must never consume
# the author-format budget.
failed = transition(state, "failed", error=error.code)
self.repository.compare_and_swap(failed, events=[{
"event": "runtime_contract_invalid",
"tool": name,
"message": error.message,
}])
return "task_terminal", {
"taskId": task_id,
"lifecycle": "failed",
"code": error.code.value,
"message": error.message,
}
if error.code == ErrorCode.RUNTIME_PRECONDITION_FAILED:
# A schema-valid fragment can still be impossible on the current
# geometry. This is not an author-format failure: preserve the
# accepted checkpoint, discard only the pending action and let
# the author make a fresh, evidence-backed choice.
if state.phase == TaskPhase.ACTION_PENDING:
action = state.pending_action
atomic_id = str(error.details.get("atomic_id") or (action.atomic_id if action else ""))
checkpoint_revision = str(error.details.get("active_revision") or state.active_revision)
normalized_code = str(error.details.get("normalized_error_code") or error.code.value)
failure_class_fingerprint = sha256(
f"{checkpoint_revision}|{atomic_id}|{normalized_code}".encode("utf-8")
).hexdigest()
prior = [
event for event in self.repository.ledger_events(task_id)
if event.get("failure_class_fingerprint") == failure_class_fingerprint
]
if len(prior) >= 2:
failed = transition(state, "failed", error=ErrorCode.NO_PROGRESS_LIMIT)
self.repository.compare_and_swap(failed, events=[{
"event": "no_progress_limit",
"tool": name,
"code": ErrorCode.NO_PROGRESS_LIMIT.value,
"message": error.message,
"checkpoint_revision": checkpoint_revision,
"atomic_id": atomic_id,
"normalized_error_code": normalized_code,
"failure_class_fingerprint": failure_class_fingerprint,
}])
return "task_terminal", {
"taskId": task_id,
"lifecycle": "failed",
"code": ErrorCode.NO_PROGRESS_LIMIT.value,
"message": "The same operation failure class made no progress after three attempts; the last checkpoint was preserved.",
"blockerType": "no_progress_limit",
"userActionRequired": False,
"revisionId": state.active_revision,
}
next_state = transition(
state,
"runtime_precondition_rejected",
pending_action=None,
error=ErrorCode.RUNTIME_PRECONDITION_FAILED,
)
self.repository.compare_and_swap(next_state, events=[{
"event": "runtime_precondition_rejected",
"tool": name,
"code": error.code.value,
"message": error.message,
"checkpoint_revision": checkpoint_revision,
"atomic_id": atomic_id,
"fragment_hash": error.details.get("fragment_hash"),
"failure_exact_fingerprint": error.details.get("failure_exact_fingerprint"),
"normalized_error_code": normalized_code,
"failure_class_fingerprint": failure_class_fingerprint,
"attempt": len(prior) + 1,
}])
feedback_error = error
if prior:
feedback_error = WorkflowError(
error.code,
error.message + " A different fragment or operation path is required; do not repeat the proven failure class.",
field_errors=error.field_errors,
details={**error.details, "prohibited_failure_class": failure_class_fingerprint},
)
feedback[:] = [self._feedback(feedback_error)]
return None
# A precondition result outside fragment submission is an invalid
# workflow implementation state, not a provider/service outage.
failed = transition(state, "failed", error=error.code)
self.repository.compare_and_swap(failed, events=[{
"event": "runtime_precondition_rejected",
"tool": name,
"code": error.code.value,
"message": error.message,
}])
return "task_terminal", {
"taskId": task_id,
"lifecycle": "failed",
"code": error.code.value,
"message": error.message,
}
model_rejection_codes = {
ErrorCode.AUTHOR_FORMAT_INVALID,
ErrorCode.AUTHOR_DECISION_REJECTED,
ErrorCode.STALE_WORKING_HEAD,
}
if error.code in model_rejection_codes:
return self._format_failure(task_id, state, name, error, counters, feedback, actor=actor)
return self._service_failure(task_id, state, error)
def _format_failure(self, task_id: str, state: TaskState, name: str, error: WorkflowError, counters: dict[str, int], feedback: list[dict[str, Any]], *, actor: str = "author") -> tuple[str, dict[str, Any]] | None:
counters[name] = counters.get(name, 0) + 1
feedback[:] = [self._feedback(error)]
if counters[name] < self.config.format_error_limit:
return None
failed = transition(state, "failed", error=ErrorCode.FAILED_AUTHOR_FORMAT)
self.repository.compare_and_swap(failed, events=[{"event": "failed_author_format", "tool": name, "field_errors": list(error.field_errors)}])
return "task_terminal", {
"taskId": task_id,
"lifecycle": "failed",
"code": ErrorCode.FAILED_AUTHOR_FORMAT.value,
"message": f"{actor.capitalize()} repeatedly failed the same canonical schema or state contract.",
"tool": name,
"field_errors": list(error.field_errors),
}
def _requirements_format_failure(
self,
task_id: str,
state: TaskState,
error: WorkflowError,
counters: dict[str, int],
feedback: list[dict[str, Any]],
) -> tuple[str, dict[str, Any]] | None:
key = "requirements_spec"
counters[key] = counters.get(key, 0) + 1
feedback[:] = [self._feedback(error)]
if counters[key] < 2:
return None
failed = transition(state, "failed", error=ErrorCode.REQUIREMENTS_SPEC_INVALID)
self.repository.compare_and_swap(failed, events=[{
"event": "requirements_spec_invalid",
"code": ErrorCode.REQUIREMENTS_SPEC_INVALID.value,
"message": error.message,
"field_errors": list(error.field_errors),
}])
return "task_terminal", {
"taskId": task_id,
"lifecycle": "failed",
"code": ErrorCode.REQUIREMENTS_SPEC_INVALID.value,
"message": "Requirements specification remained unreadable after one field-level correction.",
"tool": "submit_requirements_spec",
"field_errors": list(error.field_errors),
"userActionRequired": False,
}
def _requirements_rejection(
self,
task_id: str,
state: TaskState,
error: WorkflowError,
counters: dict[str, int],
feedback: list[dict[str, Any]],
) -> tuple[str, dict[str, Any]] | None:
if error.retryable or error.code == ErrorCode.STORAGE_FAILURE:
return self._service_failure(task_id, state, error)
return self._requirements_format_failure(task_id, state, error, counters, feedback)
@staticmethod
def _tool(name: str, model: type[BaseModel] | dict[str, Any]) -> dict[str, Any]:
parameters = model if isinstance(model, dict) else model.model_json_schema()
return {"type": "function", "function": {"name": name, "description": name.replace("_", " "), "parameters": parameters}}
@staticmethod
def _result_payload(result: Accepted | Rejected | Waiting) -> dict[str, Any]:
if isinstance(result, Accepted):
return result.payload
return result.error.payload()
@staticmethod
def _feedback(error: WorkflowError) -> dict[str, Any]:
return {"role": "user", "content": json.dumps({"schema_or_state_error": error.payload(), "instruction": "Correct only the reported fields and return one allowed tool call."}, ensure_ascii=False)}
@staticmethod
def _pending_context(state: TaskState) -> dict[str, Any] | None:
action = state.pending_action
return {"action_id": action.action_id, "working_head": action.working_head, "intent": action.intent, "requirement_ids": list(action.requirement_ids), "atomic_id": action.atomic_id, "expected_change": action.expected_change, "contract_hash": action.contract_hash} if action else None
@staticmethod
def _tool_feedback(content: str) -> dict[str, Any] | None:
try:
value = json.loads(content)
except json.JSONDecodeError:
return None
return value if isinstance(value, dict) else None
@staticmethod
def _event(task_id: str, tool: str, result: dict[str, Any], status: str, usage: dict[str, Any]) -> dict[str, Any]:
return {"taskId": task_id, "tool": tool, "status": status, "result": result, "usage": usage}
@staticmethod
def _invocation_id(task_id: str) -> str:
return f"{task_id}_inv_{secrets.token_hex(8)}"