"""LLM turn coordinator for protocol v3. It selects only the structured schema visible from persisted state. Feature selection is server-owned: the scheduler chooses one atomic DAG node and the author can submit only that node's fragment. """ 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.feature_plan import FeaturePlan, plan_hash 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) _FEATURE_REPLAN_FAILURE_LIMIT = 3 @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.FEATURE_BUILDING: recovered = self.actions.recover_feature_build(task_id) yield "feature_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.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 in {TaskPhase.COMPILING_FEATURE_PLAN, TaskPhase.REPLANNING_FEATURE_SUBGRAPH}: exhausted = self._feature_replan_exhausted(task_id, state) if exhausted is not None: failed = transition(state, "failed", error=ErrorCode.NO_PROGRESS_LIMIT) self.repository.compare_and_swap(failed, events=[{ "event": "feature_plan_no_progress_limit", "code": ErrorCode.NO_PROGRESS_LIMIT.value, "message": "The same atomic feature exhausted its cross-plan replan budget.", "checkpoint_preserved": bool(state.active_revision), "revision_id": state.active_revision, **exhausted, }]) yield "task_terminal", { "taskId": task_id, "lifecycle": "failed", "revisionId": state.active_revision, "code": ErrorCode.NO_PROGRESS_LIMIT.value, "message": "The same atomic feature repeatedly failed after local replanning; the last executable checkpoint remains available.", } return plan_schema = self.requirements.feature_plan_schema(task_id) tools = [self._tool("write_feature_plan", plan_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, "write_feature_plan", result, format_errors, feedback) yield "feature_plan", {"taskId": task_id, "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, FeaturePlan) dynamic_error = canonical_validate_schema(raw, plan_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 "feature_plan", self._event(task_id, name, validation.payload(), "error", usage) if terminal: yield terminal return continue command = self.requirements.submit_feature_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="write_feature_plan") yield "feature_plan", self._event(task_id, name, command.error.payload(), "error", usage) if terminal: yield terminal return continue yield "feature_plan_ready", self._event(task_id, name, self._result_payload(command), "success", usage) feedback = [] continue if state.phase == TaskPhase.SCHEDULING_FEATURE: command = self.actions.schedule_next_feature(task_id) if isinstance(command, Rejected): yield "task_terminal", {"taskId": task_id, "lifecycle": "failed", "code": command.error.code.value, "message": command.error.message} return yield "feature_scheduled", {"taskId": task_id, "status": "success", "result": self._result_payload(command)} continue if state.phase == TaskPhase.FEATURE_PENDING: observed = action_observations.setdefault(state.working_head, set()) 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 "feature_result", {"taskId": task_id, "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 == "inspect_topology": validation = canonical_validate(raw, StatelessTopologyRequest) if isinstance(validation, WorkflowError): yield "feature_result", self._event(task_id, name, validation.payload(), "error", usage) 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 = [{"role": "tool", "content": json.dumps({"tool": name, "result": payload}, ensure_ascii=False)}] continue fragment = canonical_json_object(raw) if isinstance(fragment, WorkflowError): yield "feature_result", self._event(task_id, name, fragment.payload(), "error", usage) feedback = [self._feedback(fragment)] continue command = self.actions.submit_feature_fragment(task_id, fragment, invocation_id=self._invocation_id(task_id)) if isinstance(command, Rejected): after = self.repository.get_state(task_id) yield "feature_result", self._event(task_id, name, command.error.payload(), "error", usage) if after is not None and after.version != state.version: feedback = [self._feedback(command.error)] continue terminal = self._model_rejection_or_service_failure(task_id, state, name, command.error, format_errors, feedback) if terminal: yield terminal return continue yield "feature_result", 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: facts = self.actions._facts(task_id, state.active_revision) if not (facts.get("report") or {}).get("render_manifest"): try: self.runtime.render_review_bundle(str(self.artifacts.artifact_path(task_id, f"revisions/{state.active_revision}"))) except Exception as error: yield self._service_failure(task_id, state, WorkflowError(ErrorCode.RENDER_SERVICE_UNAVAILABLE, str(error)[:1000], retryable=True)) return yield "final_render_ready", {"taskId": task_id, "revisionId": state.active_revision, "status": "success"} 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 is not None and state.feature_plan_hash: failed = transition(state, "failed", error=ErrorCode.NO_PROGRESS_LIMIT) self.repository.compare_and_swap(failed, events=[{ "event": "feature_plan_no_progress_limit", "code": ErrorCode.NO_PROGRESS_LIMIT.value, "message": "The feature DAG reached its bounded turn limit.", "checkpoint_preserved": bool(state.active_revision), "revision_id": state.active_revision, }]) yield "task_terminal", {"taskId": task_id, "lifecycle": "failed", "revisionId": state.active_revision, "code": ErrorCode.NO_PROGRESS_LIMIT.value, "message": "The feature DAG reached its bounded turn limit; the last executable checkpoint remains available."} return 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) if state is not None and state.feature_plan_hash 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": "feature_plan_internal_failure", "message": str(error)[:1000], "checkpoint_preserved": bool(state.active_revision), "revision_id": state.active_revision, }]) yield "task_terminal", {"taskId": task_id, "lifecycle": "failed", "revisionId": state.active_revision, "code": ErrorCode.FAILED_INTERNAL.value, "message": str(error)[:1000]} return 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 if state.feature_plan_hash: 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 feature DAG converged.", "checkpoint_preserved": bool(state.active_revision), "revision_id": state.active_revision, **details, }]) return "task_terminal", { "taskId": task_id, "lifecycle": "failed", "revisionId": state.active_revision, "code": ErrorCode.CALL_BUDGET_EXHAUSTED.value, "message": "Configured model-call budget is exhausted before the feature DAG converged; the last executable checkpoint remains available.", "budget": details, "blockerType": "call_budget_exhausted", "userActionRequired": False, } 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.2.feature-dag", "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.2.feature-dag", "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.2.feature-dag", "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. An obround, slotted, or long-slot feature is not a rectangular_corner_through_bore_pattern: keep it visual unless a dedicated slot verifier is available. Every explicit centered, concentric, or coaxial bore must have concentric_bore_to_outer_cylinder coverage. coaxial_through_bore_group is only for two or more same-diameter inner bores. For one central bore concentric with an external cylinder, use concentric_bore_to_outer_cylinder with bore_diameter_mm and outer_diameter_mm.", } elif state.phase in {TaskPhase.COMPILING_FEATURE_PLAN, TaskPhase.REPLANNING_FEATURE_SUBGRAPH}: active_plan = self.artifacts.read_json(task_id, state.feature_plan_path) if state.feature_plan_path else None contract = self._requirements_contract(task_id, state) or {} deterministic_claim_ids = sorted( str(claim.get("claim_id") or "") for requirement in contract.get("requirements") or () if isinstance(requirement, dict) for claim in requirement.get("acceptance_claims") or () if isinstance(claim, dict) and claim.get("verification_mode") == "deterministic" and isinstance(claim.get("claim_id"), str) and claim.get("claim_id") ) visual_claim_ids = sorted( str(claim.get("claim_id") or "") for requirement in contract.get("requirements") or () if isinstance(requirement, dict) for claim in requirement.get("acceptance_claims") or () if isinstance(claim, dict) and claim.get("verification_mode") != "deterministic" and isinstance(claim.get("claim_id"), str) and claim.get("claim_id") ) required_replacements: list[str] = [] parent_plan_hash = "" if isinstance(active_plan, dict): try: parsed_plan = FeaturePlan.model_validate(active_plan) parent_plan_hash = plan_hash(parsed_plan) required_replacements = sorted(self.requirements._required_replacements(parsed_plan, task_id)) except ValueError: pass content = { "protocol": "cad.v3.2.feature-dag", "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": contract, "claim_ownership_binding": { "node_claim_ids_must_be_drawn_only_from": deterministic_claim_ids, "final_claim_ids_must_equal": visual_claim_ids, "visual_only_nodes_must_use_empty_claim_ids": True, }, "supported_atomic_ids": list(self.runtime.supported_atomic_ids()), "active_feature_plan": active_plan, "plan_lineage_binding": { "parent_plan_hash": parent_plan_hash, "replaces_node_ids": required_replacements, }, "feature_node_statuses": self._feature_node_statuses(task_id, active_plan), "replanning_evidence": self._replanning_evidence(task_id, state), "instruction": ( "Write the complete feature-plan JSON. Each node is exactly one runtime atomic feature. " "Every deterministic frozen claim must belong to exactly one node; every visual claim must be in final_claim_ids. " "Never place a visual claim in any node claim_ids. Nodes for ribs, chamfers, slots, or other visual-only work are valid with claim_ids: []. " "Use dependency edges only for direct geometric prerequisites and unique fixed priorities. " "The tool schema binds parent_plan_hash and replaces_node_ids to the service values: copy those exact values, never use the requirements-contract hash. " "For a revision, preserve completed nodes byte-for-byte and use new IDs for replacements." ), } else: contract = self._requirements_contract(task_id, state) or {} action = state.pending_feature feature_node_turn = state.phase == TaskPhase.FEATURE_PENDING and action is not None compact = [{ "requirement_id": item.get("requirement_id"), "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)] if feature_node_turn: owned_requirements = set(action.requirement_ids) compact = [ item for item in compact if str(item.get("requirement_id") or "") in owned_requirements ] 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 = "The service has scheduled one atomic feature from the immutable DAG. Submit exactly that feature's CDSL fragment." 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 = [] sketch_workplane_candidates = [] 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." if ( feature_node_turn and state.active_revision and str((operation or {}).get("fragment_shape", {}).get("sketch") or "forbidden") == "required" ): sketch_workplane_candidates = self._sketch_workplane_candidates( self.artifacts.read_topology(task_id, state.active_revision) ) if sketch_workplane_candidates: instruction += ( " sketch_workplane_candidates are measured planar material faces. " "For a blind cut, select a plane whose material region covers the intended profile; " "do not choose a newer or higher face merely because it is the last feature." ) current_claims = self.actions.claim_summary(task_id, state) if feature_node_turn: current_claims = [ item for item in current_claims if str(item.get("claim_id") or "") in set(action.claim_ids) ] content = {"protocol": "cad.v3.2.feature-dag", "coordinate_protocol": self._coordinate_protocol(state), "phase": state.phase.value, "requirements": compact, "verification_warnings": contract.get("verification_warnings") or [], "claim_coverage": [self._public_claim_result(item) for item in current_claims], "model_summary": self.actions.model_summary(task_id, state), "pending_feature": self._public_pending_context(state), "direct_upstream_facts": self._direct_upstream_facts(task_id, 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)), "sketch_workplane_candidates": sketch_workplane_candidates, "recent_failures": self._recent_failure_constraints(task_id, state), "instruction": instruction} if not feature_node_turn: content.update({ "requirements_markdown": self._read_markdown(task_id, state.requirements_document_path), "completion_target_markdown": self._read_markdown(task_id, state.completion_target_path), "feature_plan": self.artifacts.read_json(task_id, state.feature_plan_path) if state.feature_plan_path else None, "feature_plan_hash": state.feature_plan_hash, "feature_node_statuses": self._feature_node_statuses(task_id, None), }) 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 _feature_node_statuses(self, task_id: str, plan_payload: dict[str, Any] | None) -> dict[str, str]: try: from app.cad_agent.domain.feature_plan import FeaturePlan, FeatureScheduler state = self.repository.get_state(task_id) plan = FeaturePlan.model_validate(plan_payload) if isinstance(plan_payload, dict) else FeaturePlan.model_validate(self.artifacts.read_json(task_id, state.feature_plan_path) if state and state.feature_plan_path else None) return FeatureScheduler(plan, self.repository.ledger_events(task_id)).statuses() except (ValueError, TypeError, OSError): return {} def _direct_upstream_facts(self, task_id: str, state: TaskState) -> list[dict[str, Any]]: """Project only verified direct dependencies for a scheduled node. The current model summary describes the working head. This additional projection lets the author distinguish the exact prerequisite nodes without exposing arbitrary historical topology or old failed stages. """ pending = state.pending_feature if pending is None or not pending.depends_on_node_ids: return [] verified: dict[str, dict[str, Any]] = {} for event in self.repository.ledger_events(task_id): if event.get("event") == "feature_node_verified": node_id = str(event.get("node_id") or "") if node_id in pending.depends_on_node_ids: verified[node_id] = event result: list[dict[str, Any]] = [] for node_id in pending.depends_on_node_ids: event = verified.get(node_id) if event is None: continue revision_id = str(event.get("revision_id") or "") verification = self.artifacts.read_json(task_id, f"revisions/{revision_id}/node-verification.json") if revision_id else None evidence = verification if isinstance(verification, dict) else {} result.append({ "node_id": node_id, "revision_id": revision_id, "feature_id": str(event.get("feature_id") or evidence.get("feature_id") or ""), "claim_results": [ { "claim_id": str(item.get("claim_id") or ""), "status": str(item.get("status") or ""), "claim_kind": str(item.get("claim_kind") or ""), "evidence": item.get("evidence") if isinstance(item.get("evidence"), dict) else {}, } for item in evidence.get("claim_results") or () if isinstance(item, dict) ], "operation_verifier_results": [ { "claim_kind": str(item.get("claim_kind") or ""), "status": str(item.get("status") or ""), "evidence": item.get("evidence") if isinstance(item.get("evidence"), dict) else {}, } for item in evidence.get("operation_verifier_results") or () if isinstance(item, dict) ], "health": evidence.get("health") if isinstance(evidence.get("health"), dict) else {}, }) return result @staticmethod def _sketch_workplane_candidates(topology: dict[str, Any] | None, *, limit: int = 12) -> list[dict[str, Any]]: """Expose compact, measured planes for sketch operations without selectors. Sketch extrusions deliberately do not use B-rep selector tokens. The author still needs enough geometry to choose a material face instead of placing a profile on the most recently created boss. """ records = topology.get("records") if isinstance(topology, dict) else None if not isinstance(records, list): return [] candidates: list[dict[str, Any]] = [] seen: set[tuple[float, ...]] = set() for record in records: geometry = record.get("geometry") if isinstance(record, dict) else None center = geometry.get("center_mm") if isinstance(geometry, dict) else None normal = geometry.get("normal") if isinstance(geometry, dict) else None bbox = geometry.get("bbox_mm") if isinstance(geometry, dict) else None if ( not isinstance(geometry, dict) or geometry.get("surface_type") != "plane" or not isinstance(center, list) or not isinstance(normal, list) or len(center) != 3 or len(normal) != 3 or not isinstance(bbox, list) or len(bbox) != 6 ): continue try: point = [round(float(value), 4) for value in center] direction = [round(float(value), 4) for value in normal] bounds = [round(float(value), 4) for value in bbox] except (TypeError, ValueError): continue key = tuple(direction + point + bounds) if key in seen: continue seen.add(key) footprint = abs((bounds[3] - bounds[0]) * (bounds[4] - bounds[1])) candidates.append({ "point_mm": point, "normal": direction, "bbox_mm": bounds, "footprint_bbox_area_mm2": round(footprint, 4), }) # Favor outward horizontal faces, then the broadest support surface. # A base top commonly supports an outer slot while a taller boss does # not; ordering by Z would teach the opposite choice. candidates.sort(key=lambda item: ( -float(item["normal"][2]), -float(item["footprint_bbox_area_mm2"]), -float(item["point_mm"][2]), item["bbox_mm"], )) return candidates[:limit] def _replanning_evidence(self, task_id: str, state: TaskState) -> list[dict[str, Any]]: """Provide bounded server evidence that led to this local replan.""" evidence: list[dict[str, Any]] = [] for event in reversed(self.repository.ledger_events(task_id)): if event.get("plan_hash") != state.feature_plan_hash: continue if event.get("event") == "feature_node_failed": evidence.append({ "kind": "node_failure", "node_id": str(event.get("node_id") or ""), "failure_class": str(event.get("failure_class") or ""), "attempt": int(event.get("attempt") or 0), "message": str(event.get("message") or "")[:500], "blockers": (event.get("blockers") or [])[:8] if isinstance(event.get("blockers"), list) else [], }) elif event.get("event") == "final_visual_reviewed" and event.get("visual_not_passed"): evidence.append({ "kind": "final_visual_review", "claim_ids": [str(value) for value in event.get("visual_not_passed") or () if str(value)], "issues": [str(value)[:500] for value in event.get("issues") or () if str(value)], "evidence": [str(value)[:500] for value in event.get("evidence") or () if str(value)], }) elif event.get("event") == "feature_plan_completion_failed": evidence.append({ "kind": "final_deterministic_validation", "claim_ids": [str(value) for value in event.get("failed_claim_ids") or () if str(value)], "message": str(event.get("message") or "")[:500], "claim_results": [ self._public_claim_result(item) for item in event.get("claim_results") or () if isinstance(item, dict) and item.get("deterministic") and item.get("status") != "pass" ], }) if len(evidence) == 8: break return evidence 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 { "node_id": action.node_id, "plan_hash": action.plan_hash, "intent": action.intent, "operation": action.atomic_id, "expected_change": action.expected_change, "claim_ids": list(action.claim_ids), "depends_on_node_ids": list(action.depends_on_node_ids), } 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)): is_current_feature_failure = ( event.get("event") == "feature_node_failed" and event.get("plan_hash") == state.feature_plan_hash and state.pending_feature is not None and event.get("node_id") == state.pending_feature.node_id ) if not is_current_feature_failure and event.get("checkpoint_revision") != state.active_revision: continue normalized_error_code = str(event.get("normalized_error_code") or event.get("code") or "") if not normalized_error_code: continue constraints.append({ "atomic_id": str(event.get("atomic_id") or ""), "normalized_error_code": normalized_error_code, "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 def _feature_replan_exhausted(self, task_id: str, state: TaskState) -> dict[str, Any] | None: """Bound repeated replacement plans at one immutable checkpoint. Node IDs must change on every subgraph revision, so a node-local retry counter alone cannot stop a planner from replacing the same failed atomic operation forever. A successful feature creates a new revision; therefore checkpoint + atomic operation is a stable, narrow boundary for this cross-plan budget. """ if state.phase != TaskPhase.REPLANNING_FEATURE_SUBGRAPH: return None terminal = [ event for event in self.repository.ledger_events(task_id) if event.get("event") == "feature_node_failed" and bool(event.get("terminal")) and str(event.get("checkpoint_revision") or "") == state.active_revision ] if not terminal: return None atomic_id = str(terminal[-1].get("atomic_id") or "") if not atomic_id: return None matching = [ event for event in terminal if str(event.get("atomic_id") or "") == atomic_id ] if len(matching) < _FEATURE_REPLAN_FAILURE_LIMIT: return None return { "atomic_id": atomic_id, "checkpoint_revision": state.active_revision, "terminal_failure_count": len(matching), "limit": _FEATURE_REPLAN_FAILURE_LIMIT, "node_ids": [str(event.get("node_id") or "") for event in matching], } @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 if state.feature_plan_hash: 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), "checkpoint_preserved": bool(state.active_revision), "revision_id": state.active_revision, }]) return "task_terminal", { "taskId": task_id, "lifecycle": "failed", "revisionId": state.active_revision, "code": ErrorCode.FAILED_AUTHOR_FORMAT.value, "message": f"{actor.capitalize()} repeatedly failed the canonical schema; the last executable checkpoint remains available.", "tool": name, "field_errors": list(error.field_errors), } 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) if tool == "write_feature_plan": # A plan revision is authored against an already-frozen contract. # Its field errors must use the plan tool's own retry budget, not # the requirements compiler's shared counter. Otherwise one # earlier requirements correction can make the first replan # attempt terminal, even though the state remains perfectly # recoverable in REPLANNING_FEATURE_SUBGRAPH. return self._format_failure(task_id, state, tool, error, counters, feedback) 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)}"