"""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, CompiledRequirementsSpec, EmptyCommand, FinalReview, GeometryConclusion, ImageObservation, MarkdownDocument, NextAction, RollbackCheckpoint, StatelessCandidateReview, StatelessFinalReview, StatelessGeometryConclusion, StatelessNextAction, StatelessRollbackCheckpoint, StatelessTopologyRequest, TopologyRequest, canonical_json_object, canonical_validate, canonical_validate_schema, sanitize_compiled_requirements_arguments, 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.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_DOCUMENT: 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 document_schema = self.requirements.document_schema() tools = [self._tool("write_requirements_document", document_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, MarkdownDocument) dynamic_error = canonical_validate_schema(raw, document_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_requirements_document(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 event_payload = self._event(task_id, name, self._result_payload(command), "success", usage) event_payload["markdown"] = validation.markdown yield "requirements_document_ready", event_payload feedback = [] continue if state.phase in {TaskPhase.DRAFTING_COMPLETION_TARGET, TaskPhase.DRAFTING_MODELING_PLAN}: document_schema = self.requirements.document_schema() tool_name = "write_completion_target" if state.phase == TaskPhase.DRAFTING_COMPLETION_TARGET else "write_modeling_plan" tools = [self._tool(tool_name, document_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, MarkdownDocument) dynamic_error = canonical_validate_schema(raw, document_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, tool=tool_name) yield "tool_call", self._event(task_id, name, validation.payload(), "error", usage) if terminal: yield terminal return continue command = self.requirements.submit_completion_target(task_id, validation, invocation_id=self._invocation_id(task_id)) if state.phase == TaskPhase.DRAFTING_COMPLETION_TARGET else self.requirements.submit_modeling_plan(task_id, validation, invocation_id=self._invocation_id(task_id)) if isinstance(command, Rejected): terminal = self._requirements_rejection(task_id, state, command.error, format_errors, feedback, tool=tool_name) yield "tool_call", self._event(task_id, name, command.error.payload(), "error", usage) if terminal: yield terminal return continue event_payload = self._event(task_id, name, self._result_payload(command), "success", usage) event_payload["markdown"] = validation.markdown yield ("completion_target_ready" if state.phase == TaskPhase.DRAFTING_COMPLETION_TARGET else "modeling_plan_ready"), event_payload feedback = [] continue if state.phase == TaskPhase.COMPILING_REQUIREMENTS: compiler_schema = self.requirements.compiler_schema(task_id) tools = [self._tool("compile_requirements_spec", compiler_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 sanitized_raw = sanitize_compiled_requirements_arguments(raw) if isinstance(sanitized_raw, WorkflowError): validation: CompiledRequirementsSpec | WorkflowError = sanitized_raw else: validation = canonical_validate(sanitized_raw, CompiledRequirementsSpec) dynamic_error = canonical_validate_schema(sanitized_raw, compiler_schema) if not isinstance(validation, WorkflowError) and isinstance(sanitized_raw, str) 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, tool="compile_requirements_spec") yield "tool_call", self._event(task_id, name, validation.payload(), "error", usage) if terminal: yield terminal return continue command = self.requirements.submit_compiled_spec(task_id, validation, invocation_id=self._invocation_id(task_id)) if isinstance(command, Rejected): terminal = self._requirements_rejection(task_id, state, command.error, format_errors, feedback, tool="compile_requirements_spec") yield "tool_call", self._event(task_id, name, command.error.payload(), "error", usage) if terminal: yield terminal return continue yield "requirements_compiled", self._event(task_id, name, self._result_payload(command), "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": 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)) 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) terminal = self._best_effort_terminal(task_id, state, ErrorCode.NO_PROGRESS_LIMIT, "The workflow reached its bounded turn limit.") if state else None if terminal: yield "completion_result_ready", {"taskId": task_id, "status": "success", "path": "completion-result.md"} yield terminal return 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) terminal = self._best_effort_terminal(task_id, state, ErrorCode.FAILED_INTERNAL, str(error)[:1000]) if state else None if terminal: yield "completion_result_ready", {"taskId": task_id, "status": "success", "path": "completion-result.md"} yield terminal return 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 terminal = self._best_effort_terminal( task_id, state, ErrorCode.CALL_BUDGET_EXHAUSTED, "Configured model-call budget is exhausted; publishing the last executable checkpoint.", budget=details, ) if terminal: return terminal 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 _best_effort_terminal( self, task_id: str, state: TaskState, reason: ErrorCode, message: str, *, budget: dict[str, Any] | None = None, ) -> tuple[str, dict[str, Any]] | None: if not state.active_revision or state.phase in {TaskPhase.COMPLETED, TaskPhase.FAILED, TaskPhase.CANCELLED}: return None claim_results = self.actions._evaluate_claims(task_id, self.actions._facts(task_id, state.active_revision)) visual_claims = [ {"status": "not_reviewed", "evidence": "Final review was not reached before bounded completion."} for item in claim_results if not item.get("deterministic") ] try: self.requirements.write_completion_result( task_id, state, claim_results=claim_results, review={"visual_claims": visual_claims}, ) except OSError: return None completed = self.actions.finalize_best_effort(task_id, reason=reason, invocation_id=self._invocation_id(task_id)) if isinstance(completed, Rejected): return None return "task_terminal", { "taskId": task_id, "lifecycle": "completed", "revisionId": state.active_revision, "code": ErrorCode.BEST_EFFORT_COMPLETED.value, "message": message, "issues": completed.payload.get("issues") or [], "verificationStatus": "completed_with_risks", "verificationWarnings": completed.payload.get("issues") or [], "completionResultPath": "completion-result.md", "budget": budget or {}, "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 not state.active_revision and action.atomic_id in {"extrude_add_blind", "extrude_add_two_sided"}: description += " Root extrusion uses the world XY datum: workplane.origin_mm must be [0, 0, Z], normal [0, 0, 1], and x_dir [1, 0, 0]. Profile coordinates are local to that plane." 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), root_xy_datum=not bool(state.active_revision))}} 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_DOCUMENT: content = { "protocol": "cad.v3.1.markdown-first", "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": ( "Write the frozen engineering-expanded requirements Markdown using every required heading. Clearly separate explicit user facts from engineering defaults. " "Conventional functional geometry is allowed for underspecified common parts, but never contradict explicit text or the image observation. Do not include runtime identifiers." ), } elif state.phase == TaskPhase.DRAFTING_COMPLETION_TARGET: content = { "protocol": "cad.v3.1.markdown-first", "requirements_markdown": self._read_markdown(task_id, state.requirements_document_path), "instruction": "Write # Completion Target with unique - [ ] checklist items. Each item must describe one independently observable final feature or condition. Do not add requirements not present in the frozen requirements document and do not include runtime identifiers.", } elif state.phase == TaskPhase.COMPILING_REQUIREMENTS: content = { "protocol": "cad.v3.1.markdown-first", "source_requirements": self.artifacts.read_source_requirements(task_id), "image_observation": self.artifacts.read_json(task_id, "documents/image-observation.json"), "requirements_markdown": self._read_markdown(task_id, state.requirements_document_path), "completion_target_markdown": self._read_markdown(task_id, state.completion_target_path), "verifier_registry": self.requirements.registry.expected_one_of_schema(), "instruction": "Compile exactly one ordered verifier bundle for each checklist item. The checklist text and all IDs are service-owned: output only assumptions and acceptance claims. Use deterministic verifiers for measurable defaults recorded in Markdown; use visual only for non-measurable appearance.", } elif state.phase == TaskPhase.DRAFTING_MODELING_PLAN: content = { "protocol": "cad.v3.1.markdown-first", "requirements_markdown": self._read_markdown(task_id, state.requirements_document_path), "completion_target_markdown": self._read_markdown(task_id, state.completion_target_path), "compiled_contract": self._requirements_contract(task_id, state), "instruction": "Write # Modeling Plan with a short ordered list of feature-construction steps. It is a frozen execution guide only: do not add, remove, or reinterpret requirements and do not include runtime identifiers.", } 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.1", "coordinate_protocol": self._coordinate_protocol(state), "phase": state.phase.value, "requirements_markdown": self._read_markdown(task_id, state.requirements_document_path), "completion_target_markdown": self._read_markdown(task_id, state.completion_target_path), "modeling_plan_markdown": self._read_markdown(task_id, state.modeling_plan_path), "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 _read_markdown(self, task_id: str, relative_path: str) -> str: if not relative_path: return "" try: path = self.artifacts.artifact_path(task_id, relative_path) return path.read_text(encoding="utf-8") if path.is_file() else "" except (OSError, ValueError): return "" 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 _coordinate_protocol(state: TaskState) -> dict[str, str]: protocol = { "system": "world_mm_right_handed", "sketch_mapping": "workplane.origin_mm is the world position of sketch local (0,0); profile points such as circle.center are sketch-local.", "vectors": "normal is positive extrusion direction; x_dir is sketch local +X expressed in world coordinates.", "hosted_features": "For existing solids, use the selected max_z/min_z face and its supplied normal; do not infer or hand-copy a world-space offset.", } if not state.active_revision: protocol["root_extrusion"] = "Root extrude_add_blind and extrude_add_two_sided are bound to world XY: origin=[0,0,Z], normal=[0,0,1], x_dir=[1,0,0]. Requirements decide Z only; never place a Z offset in Y." return protocol @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), root_xy_datum=not bool(state.active_revision))} 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.NO_PROGRESS_LIMIT: current = self.repository.get_state(task_id) or state terminal = self._best_effort_terminal( task_id, current, ErrorCode.NO_PROGRESS_LIMIT, "Further attempts repeated an already failed CAD path; publishing the last executable checkpoint.", ) if terminal: return terminal feedback[:] = [self._feedback(error)] return None 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. terminal = self._best_effort_terminal(task_id, state, error.code, error.message) if terminal: return terminal 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.REQUIREMENTS_SPEC_INVALID: # A frozen verifier contract is service-owned input at this stage. # CAD retries cannot repair it, so preserve the terminal diagnosis # instead of parking the task or blaming the author fragment. current = self.repository.get_state(task_id) if current is not None and current.phase != TaskPhase.FAILED: failed = transition(current, "failed", error=error.code) self.repository.compare_and_swap(failed, events=[{ "event": "requirements_contract_execution_failed", "tool": name, "message": error.message, }]) return "task_terminal", { "taskId": task_id, "lifecycle": "failed", "code": error.code.value, "message": error.message, "blockerType": "requirements_contract_invalid", "userActionRequired": False, "issues": [str(error.details.get("diagnostic") or 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: next_state = transition(state, "runtime_precondition_rejected", pending_action=None, error=ErrorCode.NO_PROGRESS_LIMIT) self.repository.compare_and_swap(next_state, 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, }]) terminal = self._best_effort_terminal( task_id, next_state, ErrorCode.NO_PROGRESS_LIMIT, "The same operation failure made no progress; publishing the last executable checkpoint.", ) if terminal: return terminal feedback[:] = [self._feedback(error)] return None 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. terminal = self._best_effort_terminal(task_id, state, error.code, error.message) if terminal: return terminal 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, } if error.code in { ErrorCode.CANDIDATE_BUILD_FAILED, ErrorCode.CLAIM_VERIFICATION_FAILED, ErrorCode.CANDIDATE_REVIEW_REJECTED, }: # The handler already preserved the last checkpoint and recorded # the field/runtime diagnostic. Continue with that evidence; a # planning miss is not an infrastructure terminal condition. feedback[:] = [self._feedback(error)] return None 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 terminal = self._best_effort_terminal( task_id, state, ErrorCode.FAILED_AUTHOR_FORMAT, f"{actor.capitalize()} repeatedly failed the canonical schema; publishing the last executable checkpoint.", ) if terminal: return terminal 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]], *, tool: str = "compile_requirements_spec", ) -> 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": tool, "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]], *, tool: str = "compile_requirements_spec", ) -> 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, tool=tool) @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)}"