Files
cdsl-cad/backend/app/cad_agent/application/workflow.py
T
2026-09-04 11:17:36 +08:00

2322 lines
134 KiB
Python

"""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,
AuthorGuidance,
AuthorGuidanceSelection,
CadRuntime,
ModelGateway,
NullAuthorGuidance,
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,
author_guidance: AuthorGuidance | None = None,
) -> 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
self.author_guidance = author_guidance or NullAuthorGuidance()
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, Any]] | 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, guidance = 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,
**guidance.usage_metadata(),
})
return WorkflowError(ErrorCode.AUTHOR_TRANSPORT_UNAVAILABLE, str(error)[:1000], retryable=True)
call = validate_one_tool_call(response["tool_calls"], name)
if isinstance(call, WorkflowError):
# A provider response is still a billable author attempt even when
# it violates the one-tool-call protocol. Keep guidance audit
# metadata on that record so A/B reports do not silently omit the
# failures this corpus is intended to reduce.
self.repository.record_usage(task_id, {
**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": "",
"retry_reason": "invalid_tool_call",
"cache_hit": False,
**guidance.usage_metadata(),
})
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,
**guidance.usage_metadata(),
}
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]]) -> tuple[list[dict[str, Any]], AuthorGuidanceSelection]:
state = self.repository.get_state(task_id)
if state is None:
return [], AuthorGuidanceSelection(fallback_reason="task_state_unavailable")
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),
})
guidance = self.author_guidance.select(
phase=state.phase,
atomic_id=state.pending_feature.atomic_id if state.pending_feature is not None else "",
repair_required=state.repair_required,
supported_atomic_ids=self.runtime.supported_atomic_ids(),
)
system = "You are the autonomous CAD author. Use exactly one offered structured tool call. Never emit Markdown plans or free-form JSON."
if guidance.content:
system += "\n\nThe following is non-authoritative CDSL author guidance. The current tool schema, operation contract, and server facts take precedence.\n\n" + guidance.content
messages: list[dict[str, Any]] = [{"role": "system", "content": system}, {"role": "user", "content": json.dumps(content, ensure_ascii=False)}]
return [*messages, *feedback[-2:]], guidance
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)}"