优化流程
This commit is contained in:
@@ -17,11 +17,12 @@ from pydantic import BaseModel
|
||||
|
||||
from app.cad_agent.application.action_handlers import ActionCommandHandler
|
||||
from app.cad_agent.application.llm_contracts import (
|
||||
CandidateReview, EmptyCommand, FinalReview, GeometryConclusion, ImageObservation, NextAction,
|
||||
RequirementsAuthorOutput, RollbackCheckpoint, StatelessCandidateReview,
|
||||
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,
|
||||
@@ -230,16 +231,6 @@ class WorkflowCoordinator:
|
||||
if state is None:
|
||||
yield "task_terminal", {"taskId": task_id, "lifecycle": "failed", "code": ErrorCode.STORAGE_FAILURE.value, "message": "V3 task state is unavailable."}
|
||||
return
|
||||
if state.requirements_contract_path and state.phase not in {TaskPhase.COMPLETED, TaskPhase.FAILED, TaskPhase.CANCELLED}:
|
||||
try:
|
||||
self.requirements.ensure_rendered_contract_views(task_id, state)
|
||||
except Exception as error:
|
||||
yield self._service_failure(
|
||||
task_id,
|
||||
state,
|
||||
WorkflowError(ErrorCode.STORAGE_FAILURE, str(error)[:1000], retryable=True),
|
||||
)
|
||||
return
|
||||
if state.phase == TaskPhase.COMPLETED:
|
||||
yield "task_terminal", self._projected_terminal(task_id, state)
|
||||
return
|
||||
@@ -264,7 +255,7 @@ class WorkflowCoordinator:
|
||||
yield self._service_failure(task_id, state, recovered.error)
|
||||
return
|
||||
continue
|
||||
if state.phase == TaskPhase.DRAFTING_REQUIREMENTS:
|
||||
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")
|
||||
@@ -299,8 +290,8 @@ class WorkflowCoordinator:
|
||||
}])
|
||||
yield "image_observation", {"taskId": task_id, "status": "success", "path": "documents/image-observation.json"}
|
||||
continue
|
||||
spec_schema = self.requirements.spec_schema()
|
||||
tools = [self._tool("submit_requirements_spec", spec_schema)]
|
||||
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
|
||||
@@ -321,8 +312,8 @@ class WorkflowCoordinator:
|
||||
return
|
||||
continue
|
||||
name, raw, usage = result
|
||||
validation = canonical_validate(raw, RequirementsAuthorOutput)
|
||||
dynamic_error = canonical_validate_schema(raw, spec_schema) if not isinstance(validation, WorkflowError) else None
|
||||
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):
|
||||
@@ -333,7 +324,7 @@ class WorkflowCoordinator:
|
||||
return
|
||||
continue
|
||||
invocation_id = self._invocation_id(task_id)
|
||||
command = self.requirements.submit_spec(task_id, validation, invocation_id=invocation_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)
|
||||
@@ -341,7 +332,106 @@ class WorkflowCoordinator:
|
||||
yield terminal
|
||||
return
|
||||
continue
|
||||
yield "requirements_ready", self._event(task_id, name, self._result_payload(command), "waiting" if isinstance(command, Waiting) else "success", usage)
|
||||
event_payload = self._event(task_id, name, self._result_payload(command), "success", usage)
|
||||
event_payload["markdown"] = validation.markdown
|
||||
yield "requirements_document_ready", event_payload
|
||||
feedback = []
|
||||
continue
|
||||
if state.phase in {TaskPhase.DRAFTING_COMPLETION_TARGET, TaskPhase.DRAFTING_MODELING_PLAN}:
|
||||
document_schema = self.requirements.document_schema()
|
||||
tool_name = "write_completion_target" if state.phase == TaskPhase.DRAFTING_COMPLETION_TARGET else "write_modeling_plan"
|
||||
tools = [self._tool(tool_name, document_schema)]
|
||||
terminal = self._call_budget_terminal(task_id, state, call_budget, actor="author")
|
||||
if terminal:
|
||||
yield terminal
|
||||
return
|
||||
call_budget.record_attempt("author")
|
||||
result = await self._author_turn(task_id, active_author, tools, feedback)
|
||||
if isinstance(result, WorkflowError):
|
||||
if result.code == ErrorCode.AUTHOR_FORMAT_INVALID:
|
||||
terminal = self._format_failure(task_id, state, "author_turn", result, format_errors, feedback)
|
||||
yield "tool_call", {"taskId": task_id, "tool": "author_turn", "status": "error", "result": result.payload()}
|
||||
if terminal:
|
||||
yield terminal
|
||||
return
|
||||
continue
|
||||
active_author, terminal = self._transport_or_failure(task_id, state, result, active_author, transport_attempted)
|
||||
if terminal:
|
||||
yield terminal
|
||||
return
|
||||
continue
|
||||
name, raw, usage = result
|
||||
validation = canonical_validate(raw, MarkdownDocument)
|
||||
dynamic_error = canonical_validate_schema(raw, document_schema) if not isinstance(validation, WorkflowError) else None
|
||||
if dynamic_error is not None:
|
||||
validation = dynamic_error
|
||||
if isinstance(validation, WorkflowError):
|
||||
terminal = self._requirements_format_failure(task_id, state, validation, format_errors, feedback, tool=tool_name)
|
||||
yield "tool_call", self._event(task_id, name, validation.payload(), "error", usage)
|
||||
if terminal:
|
||||
yield terminal
|
||||
return
|
||||
continue
|
||||
command = self.requirements.submit_completion_target(task_id, validation, invocation_id=self._invocation_id(task_id)) if state.phase == TaskPhase.DRAFTING_COMPLETION_TARGET else self.requirements.submit_modeling_plan(task_id, validation, invocation_id=self._invocation_id(task_id))
|
||||
if isinstance(command, Rejected):
|
||||
terminal = self._requirements_rejection(task_id, state, command.error, format_errors, feedback, tool=tool_name)
|
||||
yield "tool_call", self._event(task_id, name, command.error.payload(), "error", usage)
|
||||
if terminal:
|
||||
yield terminal
|
||||
return
|
||||
continue
|
||||
event_payload = self._event(task_id, name, self._result_payload(command), "success", usage)
|
||||
event_payload["markdown"] = validation.markdown
|
||||
yield ("completion_target_ready" if state.phase == TaskPhase.DRAFTING_COMPLETION_TARGET else "modeling_plan_ready"), event_payload
|
||||
feedback = []
|
||||
continue
|
||||
if state.phase == TaskPhase.COMPILING_REQUIREMENTS:
|
||||
compiler_schema = self.requirements.compiler_schema(task_id)
|
||||
tools = [self._tool("compile_requirements_spec", compiler_schema)]
|
||||
terminal = self._call_budget_terminal(task_id, state, call_budget, actor="author")
|
||||
if terminal:
|
||||
yield terminal
|
||||
return
|
||||
call_budget.record_attempt("author")
|
||||
result = await self._author_turn(task_id, active_author, tools, feedback)
|
||||
if isinstance(result, WorkflowError):
|
||||
if result.code == ErrorCode.AUTHOR_FORMAT_INVALID:
|
||||
terminal = self._format_failure(task_id, state, "author_turn", result, format_errors, feedback)
|
||||
yield "tool_call", {"taskId": task_id, "tool": "author_turn", "status": "error", "result": result.payload()}
|
||||
if terminal:
|
||||
yield terminal
|
||||
return
|
||||
continue
|
||||
active_author, terminal = self._transport_or_failure(task_id, state, result, active_author, transport_attempted)
|
||||
if terminal:
|
||||
yield terminal
|
||||
return
|
||||
continue
|
||||
name, raw, usage = result
|
||||
sanitized_raw = sanitize_compiled_requirements_arguments(raw)
|
||||
if isinstance(sanitized_raw, WorkflowError):
|
||||
validation: CompiledRequirementsSpec | WorkflowError = sanitized_raw
|
||||
else:
|
||||
validation = canonical_validate(sanitized_raw, CompiledRequirementsSpec)
|
||||
dynamic_error = canonical_validate_schema(sanitized_raw, compiler_schema) if not isinstance(validation, WorkflowError) and isinstance(sanitized_raw, str) else None
|
||||
if dynamic_error is not None:
|
||||
validation = dynamic_error
|
||||
if isinstance(validation, WorkflowError):
|
||||
terminal = self._requirements_format_failure(task_id, state, validation, format_errors, feedback, tool="compile_requirements_spec")
|
||||
yield "tool_call", self._event(task_id, name, validation.payload(), "error", usage)
|
||||
if terminal:
|
||||
yield terminal
|
||||
return
|
||||
continue
|
||||
command = self.requirements.submit_compiled_spec(task_id, validation, invocation_id=self._invocation_id(task_id))
|
||||
if isinstance(command, Rejected):
|
||||
terminal = self._requirements_rejection(task_id, state, command.error, format_errors, feedback, tool="compile_requirements_spec")
|
||||
yield "tool_call", self._event(task_id, name, command.error.payload(), "error", usage)
|
||||
if terminal:
|
||||
yield terminal
|
||||
return
|
||||
continue
|
||||
yield "requirements_compiled", self._event(task_id, name, self._result_payload(command), "success", usage)
|
||||
feedback = []
|
||||
continue
|
||||
if state.phase == TaskPhase.AWAITING_ACTION:
|
||||
@@ -472,11 +562,7 @@ class WorkflowCoordinator:
|
||||
continue
|
||||
name, raw, usage = result
|
||||
if name == "record_geometry_conclusion":
|
||||
diagnostic_schema = geometry_conclusion_schema(state.working_head, list(self.actions.diagnostic_evidence_refs(task_id, state)))
|
||||
validation = canonical_validate(raw, GeometryConclusion)
|
||||
dynamic_error = canonical_validate_schema(raw, diagnostic_schema) if not isinstance(validation, WorkflowError) else None
|
||||
if dynamic_error is not None:
|
||||
validation = dynamic_error
|
||||
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)
|
||||
@@ -484,6 +570,13 @@ class WorkflowCoordinator:
|
||||
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)
|
||||
@@ -675,6 +768,11 @@ class WorkflowCoordinator:
|
||||
yield "final_review", {"taskId": task_id, "status": "success", "result": self._result_payload(command)}
|
||||
continue
|
||||
state = self.repository.get_state(task_id)
|
||||
terminal = self._best_effort_terminal(task_id, state, ErrorCode.NO_PROGRESS_LIMIT, "The workflow reached its bounded turn limit.") if state else None
|
||||
if terminal:
|
||||
yield "completion_result_ready", {"taskId": task_id, "status": "success", "path": "completion-result.md"}
|
||||
yield terminal
|
||||
return
|
||||
if state:
|
||||
failed = transition(state, "failed", error=ErrorCode.FAILED_INTERNAL)
|
||||
self.repository.compare_and_swap(failed, events=[{"event": "failed_internal", "message": "Workflow exceeded its finite turn limit."}])
|
||||
@@ -683,6 +781,11 @@ class WorkflowCoordinator:
|
||||
yield self._storage_failure(task_id, str(error))
|
||||
except Exception as error:
|
||||
state = self.repository.get_state(task_id)
|
||||
terminal = self._best_effort_terminal(task_id, state, ErrorCode.FAILED_INTERNAL, str(error)[:1000]) if state else None
|
||||
if terminal:
|
||||
yield "completion_result_ready", {"taskId": task_id, "status": "success", "path": "completion-result.md"}
|
||||
yield terminal
|
||||
return
|
||||
if state and state.phase not in {TaskPhase.COMPLETED, TaskPhase.FAILED, TaskPhase.CANCELLED}:
|
||||
failed = transition(state, "failed", error=ErrorCode.FAILED_INTERNAL)
|
||||
self.repository.compare_and_swap(failed, events=[{"event": "failed_internal", "message": str(error)[:1000]}])
|
||||
@@ -711,6 +814,15 @@ class WorkflowCoordinator:
|
||||
return None
|
||||
details = budget.payload()
|
||||
details["next_actor"] = actor
|
||||
terminal = self._best_effort_terminal(
|
||||
task_id,
|
||||
state,
|
||||
ErrorCode.CALL_BUDGET_EXHAUSTED,
|
||||
"Configured model-call budget is exhausted; publishing the last executable checkpoint.",
|
||||
budget=details,
|
||||
)
|
||||
if terminal:
|
||||
return terminal
|
||||
failed = transition(state, "failed", error=ErrorCode.CALL_BUDGET_EXHAUSTED)
|
||||
self.repository.compare_and_swap(failed, events=[{
|
||||
"event": "call_budget_exhausted",
|
||||
@@ -728,6 +840,49 @@ class WorkflowCoordinator:
|
||||
"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)
|
||||
@@ -1045,6 +1200,8 @@ class WorkflowCoordinator:
|
||||
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. "
|
||||
@@ -1057,7 +1214,7 @@ class WorkflowCoordinator:
|
||||
"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))}}
|
||||
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]]:
|
||||
@@ -1076,18 +1233,41 @@ class WorkflowCoordinator:
|
||||
state = self.repository.get_state(task_id)
|
||||
if state is None:
|
||||
return []
|
||||
if state.phase == TaskPhase.DRAFTING_REQUIREMENTS:
|
||||
if state.phase == TaskPhase.DRAFTING_REQUIREMENTS_DOCUMENT:
|
||||
content = {
|
||||
"protocol": "cad.v3.spec.v1",
|
||||
"protocol": "cad.v3.1.markdown-first",
|
||||
"source_requirements": self.artifacts.read_source_requirements(task_id),
|
||||
"image_observation": self.artifacts.read_json(task_id, "documents/image-observation.json"),
|
||||
"user_clarifications": self._user_clarifications(task_id),
|
||||
"instruction": (
|
||||
"Return one complete bounded requirements specification. Use deterministic claims only when the supplied registry can execute them; otherwise use a scoped visual claim. "
|
||||
"Unspecified design choices are assumptions and must not block generation. Return clarification only when two quoted source statements cannot both be followed and the user must choose; ask exactly one question. "
|
||||
"Never return source, task, draft, requirement, claim, revision, candidate, action, head, or evidence identifiers. Do not solve semantic conflicts by changing a user value."
|
||||
"Write the frozen engineering-expanded requirements Markdown using every required heading. Clearly separate explicit user facts from engineering defaults. "
|
||||
"Conventional functional geometry is allowed for underspecified common parts, but never contradict explicit text or the image observation. Do not include runtime identifiers."
|
||||
),
|
||||
}
|
||||
elif state.phase == TaskPhase.DRAFTING_COMPLETION_TARGET:
|
||||
content = {
|
||||
"protocol": "cad.v3.1.markdown-first",
|
||||
"requirements_markdown": self._read_markdown(task_id, state.requirements_document_path),
|
||||
"instruction": "Write # Completion Target with unique - [ ] checklist items. Each item must describe one independently observable final feature or condition. Do not add requirements not present in the frozen requirements document and do not include runtime identifiers.",
|
||||
}
|
||||
elif state.phase == TaskPhase.COMPILING_REQUIREMENTS:
|
||||
content = {
|
||||
"protocol": "cad.v3.1.markdown-first",
|
||||
"source_requirements": self.artifacts.read_source_requirements(task_id),
|
||||
"image_observation": self.artifacts.read_json(task_id, "documents/image-observation.json"),
|
||||
"requirements_markdown": self._read_markdown(task_id, state.requirements_document_path),
|
||||
"completion_target_markdown": self._read_markdown(task_id, state.completion_target_path),
|
||||
"verifier_registry": self.requirements.registry.expected_one_of_schema(),
|
||||
"instruction": "Compile exactly one ordered verifier bundle for each checklist item. The checklist text and all IDs are service-owned: output only assumptions and acceptance claims. Use deterministic verifiers for measurable defaults recorded in Markdown; use visual only for non-measurable appearance.",
|
||||
}
|
||||
elif state.phase == TaskPhase.DRAFTING_MODELING_PLAN:
|
||||
content = {
|
||||
"protocol": "cad.v3.1.markdown-first",
|
||||
"requirements_markdown": self._read_markdown(task_id, state.requirements_document_path),
|
||||
"completion_target_markdown": self._read_markdown(task_id, state.completion_target_path),
|
||||
"compiled_contract": self._requirements_contract(task_id, state),
|
||||
"instruction": "Write # Modeling Plan with a short ordered list of feature-construction steps. It is a frozen execution guide only: do not add, remove, or reinterpret requirements and do not include runtime identifiers.",
|
||||
}
|
||||
else:
|
||||
contract = self._requirements_contract(task_id, state) or {}
|
||||
compact = [{
|
||||
@@ -1117,7 +1297,7 @@ class WorkflowCoordinator:
|
||||
for token, value in tokens.items() if token in allowed
|
||||
][:16]
|
||||
instruction = "The exact operation contract and eligible selector summary are attached. Submit one fragment; call inspect_topology only when the selector summary is marked truncated."
|
||||
content = {"protocol": "cad.v3", "phase": state.phase.value, "requirements": compact, "verification_warnings": contract.get("verification_warnings") or [], "claim_coverage": [self._public_claim_result(item) for item in self.actions.claim_summary(task_id, state)], "model_summary": self.actions.model_summary(task_id, state), "pending_action": self._public_pending_context(state), "operation_contract": self._public_operation_payload(operation_payload), "selector_summary": selector_summary, "selector_summary_truncated": bool(action is not None and selector_shape == "required" and len(self._selector_tokens_for_contract(operation or {}, self.runtime.selector_tokens(self.artifacts.read_topology(task_id, state.active_revision)))) > len(selector_summary)), "recent_failures": self._recent_failure_constraints(task_id, state), "repair_diagnostics": self.actions.repair_diagnostics(task_id, state), "rollback_checkpoints": list(self.actions.checkpoint_tokens(task_id, state)) if self.actions.rollback_available(task_id, state) else [], "instruction": instruction}
|
||||
content = {"protocol": "cad.v3.1", "coordinate_protocol": self._coordinate_protocol(state), "phase": state.phase.value, "requirements_markdown": self._read_markdown(task_id, state.requirements_document_path), "completion_target_markdown": self._read_markdown(task_id, state.completion_target_path), "modeling_plan_markdown": self._read_markdown(task_id, state.modeling_plan_path), "requirements": compact, "verification_warnings": contract.get("verification_warnings") or [], "claim_coverage": [self._public_claim_result(item) for item in self.actions.claim_summary(task_id, state)], "model_summary": self.actions.model_summary(task_id, state), "pending_action": self._public_pending_context(state), "operation_contract": self._public_operation_payload(operation_payload), "selector_summary": selector_summary, "selector_summary_truncated": bool(action is not None and selector_shape == "required" and len(self._selector_tokens_for_contract(operation or {}, self.runtime.selector_tokens(self.artifacts.read_topology(task_id, state.active_revision)))) > len(selector_summary)), "recent_failures": self._recent_failure_constraints(task_id, state), "repair_diagnostics": self.actions.repair_diagnostics(task_id, state), "rollback_checkpoints": list(self.actions.checkpoint_tokens(task_id, state)) if self.actions.rollback_available(task_id, state) else [], "instruction": instruction}
|
||||
messages: list[dict[str, Any]] = [{"role": "system", "content": "You are the autonomous CAD author. Use exactly one offered structured tool call. Never emit Markdown plans or free-form JSON."}, {"role": "user", "content": json.dumps(content, ensure_ascii=False)}]
|
||||
return [*messages, *feedback[-2:]]
|
||||
|
||||
@@ -1133,6 +1313,15 @@ class WorkflowCoordinator:
|
||||
clarifications.append({"message_id": str((payload or {}).get("message_id") or ""), "text": text})
|
||||
return clarifications
|
||||
|
||||
def _read_markdown(self, task_id: str, relative_path: str) -> str:
|
||||
if not relative_path:
|
||||
return ""
|
||||
try:
|
||||
path = self.artifacts.artifact_path(task_id, relative_path)
|
||||
return path.read_text(encoding="utf-8") if path.is_file() else ""
|
||||
except (OSError, ValueError):
|
||||
return ""
|
||||
|
||||
def waiting_for_user_terminal(self, task_id: str, state: TaskState) -> dict[str, Any]:
|
||||
"""Expose the persisted requirement question when a task is parked.
|
||||
|
||||
@@ -1232,6 +1421,18 @@ class WorkflowCoordinator:
|
||||
action = state.pending_action
|
||||
return {"intent": action.intent, "operation": action.atomic_id, "expected_change": action.expected_change} if action else None
|
||||
|
||||
@staticmethod
|
||||
def _coordinate_protocol(state: TaskState) -> dict[str, str]:
|
||||
protocol = {
|
||||
"system": "world_mm_right_handed",
|
||||
"sketch_mapping": "workplane.origin_mm is the world position of sketch local (0,0); profile points such as circle.center are sketch-local.",
|
||||
"vectors": "normal is positive extrusion direction; x_dir is sketch local +X expressed in world coordinates.",
|
||||
"hosted_features": "For existing solids, use the selected max_z/min_z face and its supplied normal; do not infer or hand-copy a world-space offset.",
|
||||
}
|
||||
if not state.active_revision:
|
||||
protocol["root_extrusion"] = "Root extrude_add_blind and extrude_add_two_sided are bound to world XY: origin=[0,0,Z], normal=[0,0,1], x_dir=[1,0,0]. Requirements decide Z only; never place a Z offset in Y."
|
||||
return protocol
|
||||
|
||||
@staticmethod
|
||||
def _public_operation_payload(payload: dict[str, Any] | None) -> dict[str, Any] | None:
|
||||
if not isinstance(payload, dict):
|
||||
@@ -1265,7 +1466,7 @@ class WorkflowCoordinator:
|
||||
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))}
|
||||
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]] = []
|
||||
@@ -1334,10 +1535,25 @@ class WorkflowCoordinator:
|
||||
|
||||
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",
|
||||
@@ -1350,6 +1566,27 @@ class WorkflowCoordinator:
|
||||
"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
|
||||
@@ -1368,8 +1605,8 @@ class WorkflowCoordinator:
|
||||
if event.get("failure_class_fingerprint") == failure_class_fingerprint
|
||||
]
|
||||
if len(prior) >= 2:
|
||||
failed = transition(state, "failed", error=ErrorCode.NO_PROGRESS_LIMIT)
|
||||
self.repository.compare_and_swap(failed, events=[{
|
||||
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,
|
||||
@@ -1379,15 +1616,16 @@ class WorkflowCoordinator:
|
||||
"normalized_error_code": normalized_code,
|
||||
"failure_class_fingerprint": failure_class_fingerprint,
|
||||
}])
|
||||
return "task_terminal", {
|
||||
"taskId": task_id,
|
||||
"lifecycle": "failed",
|
||||
"code": ErrorCode.NO_PROGRESS_LIMIT.value,
|
||||
"message": "The same operation failure class made no progress after three attempts; the last checkpoint was preserved.",
|
||||
"blockerType": "no_progress_limit",
|
||||
"userActionRequired": False,
|
||||
"revisionId": state.active_revision,
|
||||
}
|
||||
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",
|
||||
@@ -1419,6 +1657,9 @@ class WorkflowCoordinator:
|
||||
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",
|
||||
@@ -1432,6 +1673,16 @@ class WorkflowCoordinator:
|
||||
"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,
|
||||
@@ -1446,6 +1697,14 @@ class WorkflowCoordinator:
|
||||
feedback[:] = [self._feedback(error)]
|
||||
if counters[name] < self.config.format_error_limit:
|
||||
return None
|
||||
terminal = self._best_effort_terminal(
|
||||
task_id,
|
||||
state,
|
||||
ErrorCode.FAILED_AUTHOR_FORMAT,
|
||||
f"{actor.capitalize()} repeatedly failed the canonical schema; publishing the last executable checkpoint.",
|
||||
)
|
||||
if terminal:
|
||||
return terminal
|
||||
failed = transition(state, "failed", error=ErrorCode.FAILED_AUTHOR_FORMAT)
|
||||
self.repository.compare_and_swap(failed, events=[{"event": "failed_author_format", "tool": name, "field_errors": list(error.field_errors)}])
|
||||
return "task_terminal", {
|
||||
@@ -1464,6 +1723,8 @@ class WorkflowCoordinator:
|
||||
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
|
||||
@@ -1482,7 +1743,7 @@ class WorkflowCoordinator:
|
||||
"lifecycle": "failed",
|
||||
"code": ErrorCode.REQUIREMENTS_SPEC_INVALID.value,
|
||||
"message": "Requirements specification remained unreadable after one field-level correction.",
|
||||
"tool": "submit_requirements_spec",
|
||||
"tool": tool,
|
||||
"field_errors": list(error.field_errors),
|
||||
"userActionRequired": False,
|
||||
}
|
||||
@@ -1494,10 +1755,12 @@ class WorkflowCoordinator:
|
||||
error: WorkflowError,
|
||||
counters: dict[str, int],
|
||||
feedback: list[dict[str, Any]],
|
||||
*,
|
||||
tool: str = "compile_requirements_spec",
|
||||
) -> tuple[str, dict[str, Any]] | None:
|
||||
if error.retryable or error.code == ErrorCode.STORAGE_FAILURE:
|
||||
return self._service_failure(task_id, state, error)
|
||||
return self._requirements_format_failure(task_id, state, error, counters, feedback)
|
||||
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]:
|
||||
|
||||
Reference in New Issue
Block a user