优化流程

This commit is contained in:
2026-09-02 13:51:35 +08:00
parent 97a03c290b
commit f3eb5dff54
71 changed files with 5290 additions and 515 deletions
@@ -109,8 +109,8 @@ class ActionCommandHandler:
for event in self.repository.ledger_events(task_id)[-16:]:
if event.get("event") in {
"candidate_rejected", "candidate_build_failed", "candidate_recovery_failed",
"candidate_recovered_rejected", "final_review_repair",
}:
"candidate_recovered_rejected", "candidate_operation_skipped", "final_review_repair",
} or (event.get("event") == "accepted" and event.get("repair_required")):
sequence = event.get("sequence")
if isinstance(sequence, int):
refs.append(f"evidence_ledger_{sequence}")
@@ -134,8 +134,10 @@ class ActionCommandHandler:
"candidate_build_failed",
"candidate_recovery_failed",
"candidate_recovered_rejected",
"candidate_operation_skipped",
"runtime_precondition_rejected",
"final_review_repair",
"accepted",
}
diagnostics: list[dict[str, Any]] = []
for event in reversed(self.repository.ledger_events(task_id)):
@@ -153,6 +155,20 @@ class ActionCommandHandler:
message = event.get("message")
if isinstance(message, str) and message:
item["message"] = message[:500]
for field in ("issues", "failed_checklist_items"):
values = event.get(field)
if isinstance(values, list):
item[field] = [str(value)[:500] for value in values[:8] if isinstance(value, str)]
failures = event.get("operation_failures")
if isinstance(failures, list):
item["operation_failures"] = [
{key: str(value)[:500] for key, value in failure.items() if key in {"feature_id", "message"}}
for failure in failures[:8]
if isinstance(failure, dict)
]
fragment_hash = event.get("fragment_hash")
if isinstance(fragment_hash, str) and fragment_hash:
item["fragment_hash"] = fragment_hash
stage_id = event.get("stage_id")
candidate = None
if isinstance(stage_id, str) and stage_id:
@@ -263,7 +279,11 @@ class ActionCommandHandler:
state = state or self.repository.get_state(task_id)
if state is None or state.phase != TaskPhase.AWAITING_ACTION or state.pending_action is not None:
return False
return any(
has_earlier_checkpoint = any(
revision != state.active_revision
for revision in self.checkpoint_tokens(task_id, state).values()
)
return has_earlier_checkpoint and any(
event.get("event") == "geometry_conclusion"
and event.get("decision") == "rollback"
and event.get("working_head") == state.working_head
@@ -390,6 +410,7 @@ class ActionCommandHandler:
fragment,
selector_tokens=allowed_selector_tokens,
reference_tokens=list(references),
root_xy_datum=not bool(state.active_revision),
)
if errors:
return Rejected(WorkflowError(ErrorCode.AUTHOR_FORMAT_INVALID, "CDSL fragment violates the active operation schema.", field_errors=tuple(errors)))
@@ -452,8 +473,48 @@ class ActionCommandHandler:
if not self.repository.compare_and_swap(building, events=[{"event": "candidate_building", "candidate_id": candidate_id, "stage_id": stage.stage_id, "action_id": action.action_id, "fragment_hash": audit["fragment_hash"]}]):
return Rejected(self._stale())
try:
rebuilt = self.runtime.rebuild(cdsl, stage.output_dir, task_id, candidate_id)
claim_results = self._evaluate_claims(task_id, rebuilt)
rebuilt, operation_failures = self.runtime.rebuild_best_effort(cdsl, stage.output_dir, task_id, candidate_id)
attempted_feature_ids = {str(value) for value in audit.get("assigned_feature_ids") or () if isinstance(value, str)}
executed_feature_ids = {str(value) for value in rebuilt.get("executed_feature_ids") or () if isinstance(value, str)}
if attempted_feature_ids and not attempted_feature_ids.intersection(executed_feature_ids):
next_state = transition(building, "candidate_rejected", candidate_id="", candidate_stage_id="", repair_required=True, error=ErrorCode.CANDIDATE_BUILD_FAILED)
result = {"candidate_id": candidate_id, "status": "skipped", "code": ErrorCode.CANDIDATE_BUILD_FAILED.value, "operation_failures": operation_failures}
if not self._commit_invocation(next_state, [{
"event": "candidate_operation_skipped",
"candidate_id": candidate_id,
"stage_id": stage.stage_id,
"action_id": action.action_id,
"working_head": action.working_head,
"checkpoint_revision": state.active_revision,
"atomic_id": action.atomic_id,
"fragment_hash": fragment_hash,
"operation_failures": operation_failures,
"message": "The submitted feature did not execute; earlier executable features were retained.",
}], invocation, result):
return Rejected(self._stale())
return Rejected(WorkflowError(
ErrorCode.CANDIDATE_BUILD_FAILED,
"The submitted feature could not execute; the previous executable checkpoint was retained.",
details={"operation_failures": operation_failures},
))
try:
claim_results = self._evaluate_claims(task_id, rebuilt)
except Exception as error:
failed_state = transition(building, "failed", error=ErrorCode.REQUIREMENTS_SPEC_INVALID)
result = {"candidate_id": candidate_id, "status": "failed", "code": ErrorCode.REQUIREMENTS_SPEC_INVALID.value}
if not self._commit_invocation(failed_state, [{
"event": "requirements_contract_execution_failed",
"candidate_id": candidate_id,
"stage_id": stage.stage_id,
"action_id": action.action_id,
"message": str(error)[:1000],
}], invocation, result):
return Rejected(self._stale())
return Rejected(WorkflowError(
ErrorCode.REQUIREMENTS_SPEC_INVALID,
"The frozen requirements contract could not be evaluated; no CAD repair was attempted.",
details={"diagnostic": str(error)[:1000]},
))
operation_results = self._operation_candidate_results(
action,
contract,
@@ -466,47 +527,11 @@ class ActionCommandHandler:
*self._candidate_blockers(task_id, action.requirement_ids, claim_results),
*[item for item in operation_results if item.get("status") != "pass"],
]
candidate = {"schema_version": "cad.v3.candidate.v1", "candidate_id": candidate_id, "stage_id": stage.stage_id, "action_id": action.action_id, "working_head": action.working_head, "actual_atomic_id": action.atomic_id, "fragment_hash": audit["fragment_hash"], "selector_snapshot_id": audit["selector_snapshot_id"], "claim_results": claim_results, "operation_verifier_results": operation_results, "blockers": blockers, "health": rebuilt["health"], "render_manifest": rebuilt.get("render_manifest") or {}, "paths": rebuilt["paths"]}
candidate = {"schema_version": "cad.v3.candidate.v1", "candidate_id": candidate_id, "stage_id": stage.stage_id, "action_id": action.action_id, "working_head": action.working_head, "actual_atomic_id": action.atomic_id, "fragment_hash": audit["fragment_hash"], "selector_snapshot_id": audit["selector_snapshot_id"], "claim_results": claim_results, "operation_verifier_results": operation_results, "blockers": blockers, "operation_failures": operation_failures, "executed_feature_ids": rebuilt.get("executed_feature_ids") or [], "health": rebuilt["health"], "render_manifest": rebuilt.get("render_manifest") or {}, "paths": rebuilt["paths"]}
self.artifacts.write_stage_json(task_id, stage.stage_id, "candidate.json", candidate)
if blockers:
failure_class_fingerprint = self._failure_class_fingerprint(
state.active_revision, action.atomic_id, ErrorCode.CLAIM_VERIFICATION_FAILED,
)
prior_failures = self._failure_class_events(task_id, failure_class_fingerprint)
if len(prior_failures) >= 2:
failed_state = transition(building, "failed", error=ErrorCode.NO_PROGRESS_LIMIT)
result = {"candidate_id": candidate_id, "status": "failed", "code": ErrorCode.NO_PROGRESS_LIMIT.value}
if not self._commit_invocation(failed_state, [{
"event": "no_progress_limit", "candidate_id": candidate_id, "stage_id": stage.stage_id,
"action_id": action.action_id, "checkpoint_revision": state.active_revision,
"atomic_id": action.atomic_id, "normalized_error_code": ErrorCode.CLAIM_VERIFICATION_FAILED.value,
"failure_class_fingerprint": failure_class_fingerprint,
}], invocation, result):
return Rejected(self._stale())
return Rejected(WorkflowError(ErrorCode.NO_PROGRESS_LIMIT, "The same deterministic candidate failure made no progress after three attempts."))
failed_state = transition(building, "candidate_rejected", candidate_id="", candidate_stage_id="", repair_required=True, error=ErrorCode.CLAIM_VERIFICATION_FAILED)
result = {"candidate_id": candidate_id, "status": "rejected", "code": ErrorCode.CLAIM_VERIFICATION_FAILED.value, "claim_results": claim_results}
if not self._commit_invocation(failed_state, [{
"event": "candidate_rejected",
"candidate_id": candidate_id,
"stage_id": stage.stage_id,
"action_id": action.action_id,
"working_head": action.working_head,
"checkpoint_revision": state.active_revision,
"atomic_id": action.atomic_id,
"fragment_hash": fragment_hash,
"normalized_error_code": ErrorCode.CLAIM_VERIFICATION_FAILED.value,
"failure_exact_fingerprint": exact_fingerprint,
"failure_class_fingerprint": failure_class_fingerprint,
"attempt": len(prior_failures) + 1,
"reason": "deterministic_claim_failed",
"claim_results": claim_results,
}], invocation, result):
return Rejected(self._stale())
return Rejected(WorkflowError(ErrorCode.CLAIM_VERIFICATION_FAILED, "Candidate violates a deterministic claim required by this action or a global invariant.", details={"claim_results": blockers}))
review = transition(building, "candidate_built", candidate_id=candidate_id, candidate_stage_id=stage.stage_id)
result = {"candidate_id": candidate_id, "stage_id": stage.stage_id, "status": "awaiting_review", "claim_results": claim_results}
if not self._commit_invocation(review, [{"event": "candidate_built", "candidate_id": candidate_id, "action_id": action.action_id, "claim_results": claim_results}], invocation, result):
result = {"candidate_id": candidate_id, "stage_id": stage.stage_id, "status": "awaiting_review", "claim_results": claim_results, "operation_failures": operation_failures}
if not self._commit_invocation(review, [{"event": "candidate_built", "candidate_id": candidate_id, "action_id": action.action_id, "claim_results": claim_results, "operation_failures": operation_failures}], invocation, result):
return Rejected(self._stale())
return Accepted(result)
except OSError as error:
@@ -592,7 +617,9 @@ class ActionCommandHandler:
selectors = self.runtime.selector_tokens(self.artifacts.read_topology(task_id, state.active_revision))
references = self.runtime.reference_tokens(base)
cdsl, _audit = self.runtime.materialize_fragment(base, fragment, contract, selectors, references, require_through=self._action_requires_through(task_id, action.requirement_ids))
rebuilt = self.runtime.rebuild(cdsl, self.artifacts.stage_output_dir(task_id, state.candidate_stage_id), task_id, state.candidate_id)
rebuilt, operation_failures = self.runtime.rebuild_best_effort(cdsl, self.artifacts.stage_output_dir(task_id, state.candidate_stage_id), task_id, state.candidate_id)
if "operation_failures" not in locals():
operation_failures = []
claim_results = self._evaluate_claims(task_id, rebuilt)
audit = source.get("fragment_audit") if isinstance(source.get("fragment_audit"), dict) else {}
contract = self.runtime.operation_contract(action.atomic_id)
@@ -611,6 +638,7 @@ class ActionCommandHandler:
"fragment_hash": audit.get("fragment_hash") or canonical_hash(source.get("fragment") or {}),
"selector_snapshot_id": audit.get("selector_snapshot_id") or "", "claim_results": claim_results,
"operation_verifier_results": operation_results,
"operation_failures": operation_failures,
"blockers": [
*self._candidate_blockers(task_id, action.requirement_ids, claim_results),
*[item for item in operation_results if item.get("status") != "pass"],
@@ -699,51 +727,9 @@ class ActionCommandHandler:
event="candidate_review_storage_failure",
message=str(error),
)
if review.verdict != "accept" or deterministic_fail:
if published_revision:
return Rejected(WorkflowError(ErrorCode.STORAGE_FAILURE, "A published candidate cannot be rejected during recovery.", retryable=True))
failure_class_fingerprint = self._failure_class_fingerprint(
state.active_revision, action.atomic_id, ErrorCode.CANDIDATE_REVIEW_REJECTED,
)
reviewed_fragment_hash = str(candidate.get("fragment_hash") or "")
failure_exact_fingerprint = canonical_hash({
"active_revision": state.active_revision,
"atomic_id": action.atomic_id,
"fragment_hash": reviewed_fragment_hash,
}) if reviewed_fragment_hash else ""
prior_failures = self._failure_class_events(task_id, failure_class_fingerprint)
if len(prior_failures) >= 2:
next_state = transition(state, "failed", error=ErrorCode.NO_PROGRESS_LIMIT)
result = {"candidate_id": state.candidate_id, "status": "failed", "code": ErrorCode.NO_PROGRESS_LIMIT.value}
if not self._commit_invocation(next_state, [{
"event": "no_progress_limit", "candidate_id": state.candidate_id,
"stage_id": state.candidate_stage_id, "action_id": action.action_id,
"checkpoint_revision": state.active_revision, "atomic_id": action.atomic_id,
"normalized_error_code": ErrorCode.CANDIDATE_REVIEW_REJECTED.value,
"failure_class_fingerprint": failure_class_fingerprint,
}], invocation, result):
return Rejected(self._stale())
return Rejected(WorkflowError(ErrorCode.NO_PROGRESS_LIMIT, "The same candidate-review failure made no progress after three attempts."))
next_state = transition(state, "candidate_rejected", error=ErrorCode.CANDIDATE_REVIEW_REJECTED, repair_required=True)
result = {"candidate_id": state.candidate_id, "status": "rejected"}
if not self._commit_invocation(next_state, [{
"event": "candidate_rejected",
"candidate_id": state.candidate_id,
"stage_id": state.candidate_stage_id,
"action_id": action.action_id,
"working_head": action.working_head,
"review_path": "candidate-review.json",
"deterministic_fail": deterministic_fail,
"checkpoint_revision": state.active_revision,
"atomic_id": action.atomic_id,
"fragment_hash": reviewed_fragment_hash,
"failure_exact_fingerprint": failure_exact_fingerprint,
"normalized_error_code": ErrorCode.CANDIDATE_REVIEW_REJECTED.value,
"failure_class_fingerprint": failure_class_fingerprint,
"attempt": len(prior_failures) + 1,
}], invocation, result):
return Rejected(self._stale())
return Accepted(result)
operation_failures = candidate.get("operation_failures") if isinstance(candidate.get("operation_failures"), list) else []
candidate_blockers = candidate.get("blockers") if isinstance(candidate.get("blockers"), list) else []
needs_repair = review.verdict != "accept" or bool(deterministic_fail) or bool(operation_failures) or bool(candidate_blockers)
revision_id = self._next_revision(task_id)
if published_revision and published_revision != revision_id:
return Rejected(WorkflowError(ErrorCode.STORAGE_FAILURE, "Published candidate revision does not match the current checkpoint lineage.", retryable=True))
@@ -755,9 +741,9 @@ class ActionCommandHandler:
event="candidate_publish_storage_failure",
message=str(error),
)
next_state = transition(state, "candidate_accepted", active_revision=revision_id, repair_required=False)
event = {"event": "accepted", "action_id": action.action_id, "working_head_before": action.working_head, "parent_revision": state.active_revision, "revision_id": revision_id, "actual_atomic_id": action.atomic_id, "fragment_hash": candidate.get("fragment_hash"), "selector_snapshot_id": candidate.get("selector_snapshot_id"), "candidate_id": state.candidate_id, "review_path": f"revisions/{revision_id}/candidate-review.json", "coverage": actual}
result = {"candidate_id": state.candidate_id, "revision_id": revision_id, "paths": paths, "status": "accepted"}
next_state = transition(state, "candidate_accepted", active_revision=revision_id, repair_required=needs_repair, error=ErrorCode.CLAIM_VERIFICATION_FAILED if needs_repair else None)
event = {"event": "accepted", "action_id": action.action_id, "working_head_before": action.working_head, "parent_revision": state.active_revision, "revision_id": revision_id, "actual_atomic_id": action.atomic_id, "fragment_hash": candidate.get("fragment_hash"), "selector_snapshot_id": candidate.get("selector_snapshot_id"), "candidate_id": state.candidate_id, "review_path": f"revisions/{revision_id}/candidate-review.json", "coverage": actual, "issues": list(review.issues), "operation_failures": operation_failures, "repair_required": needs_repair}
result = {"candidate_id": state.candidate_id, "revision_id": revision_id, "paths": paths, "status": "accepted_with_issues" if needs_repair else "accepted"}
if not self._commit_invocation(next_state, [event], invocation, result):
return Rejected(self._stale())
return Accepted(result)
@@ -833,21 +819,6 @@ class ActionCommandHandler:
invocation = self.repository.begin_invocation(task_id, f"{task_id}_recover_build_{secrets.token_hex(8)}", key) if key else None
if invocation is not None and invocation.status == "finished" and invocation.result is not None:
return Accepted(invocation.result)
if blockers:
failed = transition(state, "candidate_rejected", candidate_id="", candidate_stage_id="", repair_required=True, error=ErrorCode.CLAIM_VERIFICATION_FAILED)
result = {"candidate_id": state.candidate_id, "status": "rejected", "code": ErrorCode.CLAIM_VERIFICATION_FAILED.value, "claim_results": claim_results}
event = {
"event": "candidate_recovered_rejected",
"candidate_id": state.candidate_id,
"stage_id": state.candidate_stage_id,
"action_id": action.action_id,
"working_head": action.working_head,
"claim_results": claim_results,
}
committed = self._commit_invocation(failed, [event], invocation, result) if invocation is not None else self.repository.compare_and_swap(failed, events=[event])
if not committed:
return Rejected(self._stale())
return Accepted(result)
review = transition(state, "candidate_built", candidate_id=state.candidate_id, candidate_stage_id=state.candidate_stage_id)
result = {"candidate_id": state.candidate_id, "stage_id": state.candidate_stage_id, "status": "awaiting_review", "claim_results": claim_results}
committed = self._commit_invocation(review, [{"event": "candidate_recovered", "candidate_id": state.candidate_id, "action_id": action.action_id, "claim_results": claim_results}], invocation, result) if invocation is not None else self.repository.compare_and_swap(review, events=[{"event": "candidate_recovered", "candidate_id": state.candidate_id, "action_id": action.action_id, "claim_results": claim_results}])
@@ -855,6 +826,43 @@ class ActionCommandHandler:
return Rejected(self._stale())
return Accepted(result)
def finalize_best_effort(self, task_id: str, *, reason: ErrorCode, invocation_id: str) -> Accepted | Rejected:
"""Publish the last executable checkpoint when further repair is bounded.
This is deliberately separate from strict final validation. It does
not claim that unmet acceptance targets passed; it makes the usable
model and its measured gaps durable instead of converting a planning
dead-end into a failed task with no deliverable.
"""
state = self.repository.get_state(task_id)
if state is None or not state.active_revision:
return Rejected(WorkflowError(ErrorCode.AUTHOR_DECISION_REJECTED, "Best-effort completion requires an executable checkpoint."))
if state.phase == TaskPhase.COMPLETED:
return Accepted({"status": "completed", "revision_id": state.active_revision})
key = self._key(task_id, "best_effort_complete", state.active_revision, {"reason": reason.value})
invocation = self.repository.begin_invocation(task_id, invocation_id, key)
if invocation.status == "finished" and invocation.result is not None:
return Accepted(invocation.result)
claim_results = self._evaluate_claims(task_id, self._facts(task_id, state.active_revision))
issues = [
f"{item.get('claim_kind')}: {item.get('status')}"
for item in claim_results
if item.get("status") != "pass"
]
next_state = transition(state, "best_effort_completed", error=ErrorCode.BEST_EFFORT_COMPLETED, repair_required=False)
result = {"status": "completed_with_warnings", "revision_id": state.active_revision, "claim_results": claim_results, "issues": issues}
if not self._commit_invocation(next_state, [{
"event": "completed_best_effort",
"revision_id": state.active_revision,
"termination_code": reason.value,
"message": "Further CAD repair was bounded; the last executable checkpoint was published.",
"issues": issues,
"claim_results": claim_results,
"completion_result_path": "completion-result.md",
}], invocation, result):
return Rejected(self._stale())
return Accepted(result)
def record_final_review(self, task_id: str, review: FinalReview, *, invocation_id: str) -> Accepted | Rejected:
state = self.repository.get_state(task_id)
if state is None or state.phase != TaskPhase.FINAL_VALIDATION:
@@ -901,6 +909,19 @@ class ActionCommandHandler:
message=str(error),
)
if review.verdict != "pass" or deterministic_fail or visual_not_passed:
accepted_fragment = next((
event.get("fragment_hash") for event in reversed(self.repository.ledger_events(task_id))
if event.get("event") == "accepted" and event.get("revision_id") == state.active_revision
), "")
atomic_id = next((
str(event.get("actual_atomic_id") or "") for event in reversed(self.repository.ledger_events(task_id))
if event.get("event") == "accepted" and event.get("revision_id") == state.active_revision
), "")
exact_fingerprint = canonical_hash({
"active_revision": state.active_revision,
"atomic_id": atomic_id,
"fragment_hash": accepted_fragment,
}) if accepted_fragment and atomic_id else ""
next_state = transition(state, "final_repair", repair_required=True, error=ErrorCode.CLAIM_VERIFICATION_FAILED if deterministic_fail else ErrorCode.CANDIDATE_REVIEW_REJECTED)
result = {"status": "repair", "revision_id": state.active_revision}
if not self._commit_invocation(next_state, [{
@@ -909,6 +930,18 @@ class ActionCommandHandler:
"claim_results": claim_results,
"review_claim_coverage": [item.model_dump(mode="json") for item in review.claim_coverage],
"visual_not_passed": [str(item.get("claim_id") or "") for item in visual_not_passed],
"issues": list(review.issues),
"evidence": list(review.evidence),
"failed_checklist_items": [
str(requirement.get("statement") or "")
for requirement in (self._requirements_contract(task_id) or {}).get("requirements") or ()
if isinstance(requirement, dict)
and any(str(claim.get("claim_id") or "") in {str(item.get("claim_id") or "") for item in deterministic_fail + visual_not_passed} for claim in requirement.get("acceptance_claims") or () if isinstance(claim, dict))
],
"atomic_id": atomic_id,
"fragment_hash": accepted_fragment,
"failure_exact_fingerprint": exact_fingerprint,
"normalized_error_code": ErrorCode.CLAIM_VERIFICATION_FAILED.value if deterministic_fail else ErrorCode.CANDIDATE_REVIEW_REJECTED.value,
}], invocation, result):
return Rejected(self._stale())
return Accepted(result)
@@ -9,12 +9,12 @@ from typing import Any, Literal
from app.cad_agent.application.llm_contracts import (
EmptyCommand,
ImageObservation,
RequirementsAuthorOutput,
MarkdownDocument,
StatelessCandidateReview,
StatelessGeometryConclusion,
StatelessRollbackCheckpoint,
StatelessTopologyRequest,
requirements_spec_schema,
compiled_requirements_schema,
stateless_final_review_schema,
stateless_next_action_schema,
)
@@ -37,7 +37,10 @@ def conformance_tools(runtime: CadRuntime, *, role: CapabilityRole) -> list[dict
if not atomic_ids:
raise RuntimeError("Runtime has no operations for conformance")
tools = [
_tool("submit_requirements_spec", requirements_spec_schema(default_registry().expected_one_of_schema())),
_tool("write_requirements_document", MarkdownDocument.model_json_schema()),
_tool("write_completion_target", MarkdownDocument.model_json_schema()),
_tool("compile_requirements_spec", compiled_requirements_schema(default_registry().expected_one_of_schema(exclude_claim_kinds=frozenset({"coaxial", "coplanar"})), 1)),
_tool("write_modeling_plan", MarkdownDocument.model_json_schema()),
_tool("propose_next_action", stateless_next_action_schema(atomic_ids)),
_tool("inspect_topology", StatelessTopologyRequest.model_json_schema()),
_tool("record_geometry_conclusion", StatelessGeometryConclusion.model_json_schema()),
@@ -54,7 +57,7 @@ def conformance_tools(runtime: CadRuntime, *, role: CapabilityRole) -> list[dict
def conformance_hash(tools: list[dict[str, Any]], *, role: CapabilityRole) -> str:
payload = {"protocol": "cad.v3.spec.v1", "role": role, "tools": tools}
payload = {"protocol": "cad.v3.1.markdown-first", "role": role, "tools": tools}
return sha256(json.dumps(payload, ensure_ascii=True, sort_keys=True, separators=(",", ":")).encode("utf-8")).hexdigest()
@@ -37,6 +37,28 @@ class SpecRequirementInput(StrictDto):
acceptance_claims: list[AcceptanceClaimInput] = Field(min_length=1, max_length=16)
class MarkdownDocument(StrictDto):
"""A frozen human-readable design artifact, never an executable payload."""
markdown: Annotated[str, Field(min_length=1, max_length=16_000)]
class CompiledRequirementInput(StrictDto):
"""One verifier bundle for one server-parsed checklist item.
The checklist text, ordering, source bindings, and all identifiers are
intentionally absent: the service owns them after Markdown is frozen.
"""
assumptions: list[Annotated[str, Field(min_length=1, max_length=360)]] = Field(default_factory=list, max_length=16)
acceptance_claims: list[AcceptanceClaimInput] = Field(min_length=1, max_length=16)
class CompiledRequirementsSpec(StrictDto):
requirements: list[CompiledRequirementInput] = Field(min_length=1, max_length=64)
# Kept only so an interrupted process with an already imported old tool schema
# fails at the workflow boundary instead of failing module import. New v3.1
# tasks never expose or accept this aggregate specification.
class RequirementsSpec(StrictDto):
outcome: Literal["ready"]
summary: Annotated[str, Field(min_length=1, max_length=2000)]
@@ -61,7 +83,9 @@ class EmptyCommand(StrictDto):
class NextAction(StrictDto):
working_head: Annotated[str, Field(pattern=r"^[a-z0-9_:-]{5,192}$")]
intent: ShortText
requirement_ids: list[Identifier] = Field(min_length=1, max_length=5)
# The server binds this list from every frozen checklist target. It is not
# author input, so a five-item UI-era limit must not reject a valid task.
requirement_ids: list[Identifier] = Field(min_length=1, max_length=64)
atomic_id: Identifier
expected_change: ShortText
@@ -194,6 +218,66 @@ def requirements_spec_schema(claim_one_of: dict[str, Any]) -> dict[str, Any]:
return schema
def compiled_requirements_schema(claim_one_of: dict[str, Any], target_count: int) -> dict[str, Any]:
schema = CompiledRequirementsSpec.model_json_schema()
definitions = schema.get("$defs", {})
requirement = definitions.get("CompiledRequirementInput") if isinstance(definitions, dict) else None
if isinstance(requirement, dict):
claims = requirement.get("properties", {}).get("acceptance_claims")
if isinstance(claims, dict):
claims["items"] = deepcopy(claim_one_of)
requirements = schema.get("properties", {}).get("requirements")
if isinstance(requirements, dict):
requirements["minItems"] = target_count
requirements["maxItems"] = target_count
return schema
def sanitize_compiled_requirements_arguments(raw_arguments_json: str) -> str | WorkflowError:
"""Drop harmless compiler chatter before strict requirements validation.
``compile_requirements_spec`` is a compiler stage: the service only needs
the ordered verifier bundles for the frozen checklist items. Real models
sometimes add explanatory fields such as a top-level ``assumptions`` or
per-item ``statement`` even when the dynamic tool schema forbids them. Those
fields are not executable and are not part of the frozen contract, so they
should not abort a task before modeling starts.
The verifier ``expected`` payload is intentionally not sanitized here. It
remains governed by the registry's strict per-claim schema because those
values drive deterministic validation.
"""
value = canonical_json_object(raw_arguments_json)
if isinstance(value, WorkflowError):
return value
requirements = value.get("requirements")
sanitized: dict[str, Any] = {}
if isinstance(requirements, list):
sanitized_requirements: list[Any] = []
for requirement in requirements:
if not isinstance(requirement, dict):
sanitized_requirements.append(requirement)
continue
item: dict[str, Any] = {}
if "assumptions" in requirement:
item["assumptions"] = requirement["assumptions"]
if "acceptance_claims" in requirement:
claims = requirement["acceptance_claims"]
if isinstance(claims, list):
item["acceptance_claims"] = [
{key: claim[key] for key in ("claim_kind", "expected") if isinstance(claim, dict) and key in claim}
if isinstance(claim, dict) else claim
for claim in claims
]
else:
item["acceptance_claims"] = claims
sanitized_requirements.append(item)
sanitized["requirements"] = sanitized_requirements
else:
sanitized["requirements"] = requirements
return json.dumps(sanitized, ensure_ascii=False, separators=(",", ":"))
def stateless_next_action_schema(atomic_ids: list[str]) -> dict[str, Any]:
schema = StatelessNextAction.model_json_schema()
properties = schema.get("properties", {})
@@ -228,18 +312,6 @@ def topology_request_schema(working_head: str) -> dict[str, Any]:
return schema
def geometry_conclusion_schema(working_head: str, evidence_refs: list[str]) -> dict[str, Any]:
"""Bind a diagnostic conclusion to evidence generated for this head."""
schema = GeometryConclusion.model_json_schema()
properties = schema.get("properties", {})
if isinstance(properties, dict):
properties["working_head"] = {"const": working_head}
evidence = properties.get("evidence_refs")
if isinstance(evidence, dict):
evidence["items"] = {"enum": evidence_refs}
return schema
def rollback_checkpoint_schema(working_head: str, checkpoint_tokens: list[str]) -> dict[str, Any]:
"""Bind a rollback request to immutable checkpoints in the active lineage."""
schema = RollbackCheckpoint.model_json_schema()
+210 -234
View File
@@ -1,26 +1,32 @@
"""One-pass requirements specification and server-owned contract artifacts."""
"""Immutable Markdown-first requirements artifacts and compiled contracts."""
from __future__ import annotations
from copy import deepcopy
from hashlib import sha256
import json
from typing import Any
import re
from typing import Any, Callable
from app.cad_agent.application.llm_contracts import (
RequirementsAuthorOutput,
RequirementsClarification,
RequirementsSpec,
requirements_spec_schema,
)
from app.cad_agent.application.results import Accepted, Rejected, Waiting
from app.cad_agent.application.llm_contracts import AcceptanceClaimInput, CompiledRequirementsSpec, MarkdownDocument, compiled_requirements_schema
from app.cad_agent.application.results import Accepted, Rejected
from app.cad_agent.domain.errors import ErrorCode, WorkflowError
from app.cad_agent.domain.state import TaskPhase, TaskState, transition
from app.cad_agent.domain.verifier_registry import VerifierRegistry
from app.cad_agent.ports import ArtifactStore, TaskRepository
_CHECKBOX = re.compile(r"^\s*- \[ \]\s+(.+?)\s*$")
_RECORD_BOUND_CLAIMS = frozenset({"coaxial", "coplanar"})
class RequirementsCommandHandler:
"""Persist frozen documents and compile their checklist into a contract.
The model never names targets or internal objects during compilation. The
service derives those values strictly from the immutable checklist.
"""
def __init__(self, repository: TaskRepository, artifacts: ArtifactStore, registry: VerifierRegistry) -> None:
self.repository = repository
self.artifacts = artifacts
@@ -28,283 +34,257 @@ class RequirementsCommandHandler:
self._evaluation_contract_oracles: dict[str, list[dict[str, Any]]] = {}
self._evaluation_capability_gaps: dict[str, list[dict[str, str]]] = {}
def register_evaluation_contract_oracle(
self,
task_id: str,
required_claims: list[dict[str, Any]],
*,
validation_capability_gaps: list[dict[str, Any]] | None = None,
) -> None:
"""Retain release-evaluation metadata without changing production decisions."""
def register_evaluation_contract_oracle(self, task_id: str, required_claims: list[dict[str, Any]], *, validation_capability_gaps: list[dict[str, Any]] | None = None) -> None:
self._evaluation_contract_oracles[task_id] = deepcopy(required_claims)
self._evaluation_capability_gaps[task_id] = [
{"id": str(item.get("id") or ""), "description": str(item.get("description") or "")}
for item in validation_capability_gaps or ()
if isinstance(item, dict)
for item in validation_capability_gaps or () if isinstance(item, dict)
]
def evaluation_review_context(self, task_id: str) -> dict[str, Any] | None:
claims = self._evaluation_contract_oracles.get(task_id)
if claims is None:
return None
return {
return None if claims is None else {
"evaluation_only": True,
"required_claims": deepcopy(claims),
"known_validation_capability_gaps": deepcopy(self._evaluation_capability_gaps.get(task_id, [])),
}
def spec_schema(self) -> dict[str, Any]:
return requirements_spec_schema(self.registry.expected_one_of_schema())
@staticmethod
def document_schema() -> dict[str, Any]:
return MarkdownDocument.model_json_schema()
def submit_spec(self, task_id: str, output: RequirementsAuthorOutput, *, invocation_id: str) -> Accepted | Rejected | Waiting:
def compiler_schema(self, task_id: str) -> dict[str, Any]:
return compiled_requirements_schema(
self.registry.expected_one_of_schema(exclude_claim_kinds=_RECORD_BOUND_CLAIMS),
len(self._checklist_items(task_id)),
)
def submit_requirements_document(self, task_id: str, document: MarkdownDocument, *, invocation_id: str) -> Accepted | Rejected:
return self._write_document(task_id, document, invocation_id=invocation_id, phase=TaskPhase.DRAFTING_REQUIREMENTS_DOCUMENT, path="requirements.md", event="requirements_document_written", validator=self._validate_requirements_document)
def submit_completion_target(self, task_id: str, document: MarkdownDocument, *, invocation_id: str) -> Accepted | Rejected:
return self._write_document(task_id, document, invocation_id=invocation_id, phase=TaskPhase.DRAFTING_COMPLETION_TARGET, path="completion-target.md", event="completion_target_written", validator=self._validate_completion_target)
def submit_modeling_plan(self, task_id: str, document: MarkdownDocument, *, invocation_id: str) -> Accepted | Rejected:
return self._write_document(task_id, document, invocation_id=invocation_id, phase=TaskPhase.DRAFTING_MODELING_PLAN, path="modeling-plan.md", event="modeling_plan_written", validator=self._validate_modeling_plan)
def submit_compiled_spec(self, task_id: str, output: CompiledRequirementsSpec, *, invocation_id: str) -> Accepted | Rejected:
replay = self._replay(task_id, invocation_id)
if replay is not None:
return replay
state = self.repository.get_state(task_id)
if state is None or state.phase != TaskPhase.DRAFTING_REQUIREMENTS:
return Rejected(WorkflowError(ErrorCode.AUTHOR_DECISION_REJECTED, "Requirements are not expected in the current workflow phase."))
value = output.root
if isinstance(value, RequirementsClarification):
return self._record_clarification(task_id, state, value, invocation_id=invocation_id)
if not isinstance(value, RequirementsSpec):
return Rejected(WorkflowError(ErrorCode.REQUIREMENTS_SPEC_INVALID, "Requirements output is not a supported specification."))
field_errors: list[dict[str, str]] = []
for requirement_index, requirement in enumerate(value.requirements):
for claim_index, claim in enumerate(requirement.acceptance_claims):
try:
errors = self.registry.validate_expected(claim.claim_kind, claim.expected)
except ValueError:
errors = [{"path": "", "message": "VERIFIER_UNAVAILABLE"}]
field_errors.extend({
"path": f"/requirements/{requirement_index}/acceptance_claims/{claim_index}/expected{error['path']}",
"message": error["message"],
} for error in errors)
if state is None or state.phase != TaskPhase.COMPILING_REQUIREMENTS:
return Rejected(WorkflowError(ErrorCode.AUTHOR_DECISION_REJECTED, "Requirements compilation is not expected in the current workflow phase."))
targets = self._checklist_items(task_id)
if len(output.requirements) != len(targets):
return Rejected(WorkflowError(ErrorCode.REQUIREMENTS_SPEC_INVALID, "The compiled requirements must contain exactly one entry for every frozen completion target.", field_errors=({"path": "/requirements", "message": f"Expected {len(targets)} entries, received {len(output.requirements)}."},)))
normalized_output, compiler_warnings = self._normalize_compiled_spec(output, targets)
field_errors = self._claim_errors(normalized_output)
if field_errors:
return Rejected(WorkflowError(
ErrorCode.REQUIREMENTS_SPEC_INVALID,
"Requirements specification contains an unreadable or non-executable acceptance target.",
field_errors=tuple(field_errors),
))
invocation = self.repository.begin_invocation(
task_id,
invocation_id,
self._key(task_id, "requirements_spec", state.working_head, value.model_dump(mode="json")),
)
return Rejected(WorkflowError(ErrorCode.REQUIREMENTS_SPEC_INVALID, "Requirements compilation contains an unreadable or non-executable acceptance target.", field_errors=tuple(field_errors)))
invocation = self.repository.begin_invocation(task_id, invocation_id, self._key(task_id, "requirements_compilation", state.working_head, normalized_output.model_dump(mode="json")))
if invocation.status == "finished" and invocation.result is not None:
return self._restore(invocation.result)
source_ids = list(self.artifacts.read_source_index(task_id))
image_observation = self.artifacts.read_json(task_id, "documents/image-observation.json") or {}
warnings = [str(item) for item in image_observation.get("uncertainties") or () if str(item)]
observation = self.artifacts.read_json(task_id, "documents/image-observation.json") or {}
warnings = [
*[str(value) for value in observation.get("uncertainties") or () if str(value)],
*compiler_warnings,
]
requirements: list[dict[str, Any]] = []
claim_position = 1
for position, item in enumerate(value.requirements, 1):
for position, (target, compiled) in enumerate(zip(targets, normalized_output.requirements, strict=True), 1):
claims: list[dict[str, Any]] = []
for claim in item.acceptance_claims:
deterministic = self.registry.definition(claim.claim_kind).deterministic
claims.append({
"claim_id": f"claim_{claim_position:03d}",
"claim_kind": claim.claim_kind,
"expected": claim.expected,
"verification_mode": "deterministic" if deterministic else "visual",
})
for claim in compiled.acceptance_claims:
definition = self.registry.definition(claim.claim_kind)
claims.append({"claim_id": f"claim_{claim_position:03d}", "claim_kind": claim.claim_kind, "expected": claim.expected, "verification_mode": "deterministic" if definition.deterministic else "visual"})
claim_position += 1
requirements.append({
"requirement_id": f"req_{position:03d}",
"source_ids": source_ids,
"statement": item.statement,
"assumptions": list(item.assumptions),
"acceptance_claims": claims,
})
spec_payload = {
"schema_version": "cad.requirements-spec.v1",
"summary": value.summary,
"assumptions": list(value.assumptions),
"requirements": [item.model_dump(mode="json") for item in value.requirements],
"image_observation_path": "documents/image-observation.json" if image_observation else "",
}
contract = {
"schema_version": "cad.requirements-contract.v3",
"task_id": task_id,
"summary": value.summary,
"assumptions": list(value.assumptions),
"requirements": requirements,
"verification_warnings": warnings,
}
contract["contract_hash"] = sha256(json.dumps(contract, ensure_ascii=True, sort_keys=True, separators=(",", ":")).encode("utf-8")).hexdigest()
requirements.append({"requirement_id": f"req_{position:03d}", "source_ids": source_ids, "statement": target, "assumptions": list(compiled.assumptions), "acceptance_claims": claims})
spec = {"schema_version": "cad.requirements-spec.v2", "requirements_document_path": state.requirements_document_path, "completion_target_path": state.completion_target_path, "image_observation_path": "documents/image-observation.json" if observation else "", "requirements": [item.model_dump(mode="json") for item in normalized_output.requirements]}
contract = {"schema_version": "cad.requirements-contract.v3.1", "task_id": task_id, "requirements_document_path": state.requirements_document_path, "completion_target_path": state.completion_target_path, "requirements": requirements, "verification_warnings": warnings}
contract["contract_hash"] = sha256(json.dumps(contract, ensure_ascii=True, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
try:
spec_path = self.artifacts.write_json_once(task_id, "documents/requirements-spec.json", spec_payload)
spec_path = self.artifacts.write_json_once(task_id, "documents/requirements-spec.json", spec)
contract_path = self.artifacts.write_requirements_contract(task_id, contract, invocation_id=invocation_id)
except OSError as error:
return self._park_for_storage_retry(state, str(error))
next_state = transition(
state,
"requirements_approved",
requirements_spec_path=spec_path,
requirements_contract_path=contract_path,
clarification_path="",
)
result = Accepted({"phase": next_state.phase.value, "contract_path": contract_path})
if not self._commit(next_state, [{
"event": "requirements_contract_frozen",
"invocation_id": invocation_id,
"contract_hash": contract["contract_hash"],
"contract_path": contract_path,
"requirement_count": len(requirements),
"verification_warnings": warnings,
}], invocation, result):
next_state = transition(state, "requirements_compiled", requirements_spec_path=spec_path, requirements_contract_path=contract_path)
result = Accepted({"phase": next_state.phase.value, "spec_path": spec_path, "contract_path": contract_path, "target_count": len(targets)})
if not self._commit(next_state, [{"event": "requirements_compiled", "invocation_id": invocation_id, "contract_hash": contract["contract_hash"], "spec_path": spec_path, "contract_path": contract_path, "target_count": len(targets), "verification_warnings": warnings}], invocation, result):
return Rejected(self._stale())
self.ensure_rendered_contract_views(task_id, next_state)
return result
def ensure_rendered_contract_views(self, task_id: str, state: TaskState) -> None:
if not state.requirements_contract_path:
return
contract = self.artifacts.read_requirements_contract(task_id, state.requirements_contract_path)
if not isinstance(contract, dict):
raise RuntimeError("Committed requirements contract is unavailable")
self.artifacts.write_requirements_contract(task_id, contract)
self.artifacts.write_text_once(task_id, "requirements.md", self._requirements_markdown(contract))
target = self._completion_target_markdown(contract)
self.artifacts.write_text_once(task_id, "completion-target.md", target)
def write_completion_result(
self,
task_id: str,
state: TaskState,
*,
claim_results: list[dict[str, Any]],
review: dict[str, Any],
) -> str:
def write_completion_result(self, task_id: str, state: TaskState, *, claim_results: list[dict[str, Any]], review: dict[str, Any]) -> str:
contract = self.artifacts.read_requirements_contract(task_id, state.requirements_contract_path) or {}
by_id = {str(item.get("claim_id") or ""): item for item in claim_results if isinstance(item, dict)}
visual = iter(review.get("visual_claims") or ())
rows = ["# Completion Result", "", f"Status: {'completed with risks' if contract.get('verification_warnings') else 'verified'}", ""]
rows = ["# Completion Result", "", "## Checklist", ""]
for requirement in contract.get("requirements") or ():
if not isinstance(requirement, dict):
continue
rows.append(f"## {requirement.get('statement')}")
statuses: list[str] = []
evidence: list[str] = []
for claim in requirement.get("acceptance_claims") or ():
if not isinstance(claim, dict):
continue
if claim.get("verification_mode") == "visual":
decision = next(visual, {})
status = str(decision.get("status") or "unknown")
evidence = str(decision.get("evidence") or "")
else:
result = by_id.get(str(claim.get("claim_id") or ""), {})
status = str(result.get("status") or "unknown")
evidence = json.dumps(result.get("evidence") or {}, ensure_ascii=False, sort_keys=True)
rows.append(f"- [{'x' if status == 'pass' else ' '}] {claim.get('claim_kind')}: {status}")
if evidence:
rows.append(f" - Evidence: {evidence}")
rows.append("")
warnings = [str(item) for item in contract.get("verification_warnings") or () if str(item)]
if warnings:
rows.extend(["## Verification Warnings", "", *[f"- {item}" for item in warnings], ""])
result = next(visual, {}) if claim.get("verification_mode") == "visual" else by_id.get(str(claim.get("claim_id") or ""), {})
statuses.append(str(result.get("status") or "unknown"))
value = result.get("evidence")
if value:
evidence.append(value if isinstance(value, str) else json.dumps(value, ensure_ascii=False, sort_keys=True))
rows.append(f"- [{'x' if statuses and all(value == 'pass' for value in statuses) else ' '}] {requirement.get('statement')}: {', '.join(statuses) or 'unknown'}")
rows.extend(f" - Evidence: {value}" for value in evidence)
return self.artifacts.write_text_once(task_id, "completion-result.md", "\n".join(rows).rstrip() + "\n")
def _record_clarification(
self,
task_id: str,
state: TaskState,
clarification: RequirementsClarification,
*,
invocation_id: str,
) -> Waiting | Rejected:
evidence = self.artifacts.read_source_requirements(task_id)
observation = self.artifacts.read_json(task_id, "documents/image-observation.json") or {}
evidence += "\n" + json.dumps(observation, ensure_ascii=False)
missing = [quote for quote in clarification.source_quotes if quote not in evidence]
if missing:
return Rejected(WorkflowError(
ErrorCode.REQUIREMENTS_SPEC_INVALID,
"Clarification quotes must be copied from the user request or image observation.",
field_errors=tuple({"path": "/source_quotes", "message": f"Unknown quote: {quote}"} for quote in missing),
))
invocation = self.repository.begin_invocation(
task_id,
invocation_id,
self._key(task_id, "requirements_clarification", state.working_head, clarification.model_dump(mode="json")),
)
payload = {"schema_version": "cad.requirements-clarification.v1", **clarification.model_dump(mode="json")}
def _write_document(self, task_id: str, document: MarkdownDocument, *, invocation_id: str, phase: TaskPhase, path: str, event: str, validator: Callable[[str], list[dict[str, str]]]) -> Accepted | Rejected:
replay = self._replay(task_id, invocation_id)
if replay is not None:
return replay
state = self.repository.get_state(task_id)
if state is None or state.phase != phase:
return Rejected(WorkflowError(ErrorCode.AUTHOR_DECISION_REJECTED, "This document is not expected in the current workflow phase."))
errors = validator(document.markdown)
if errors:
return Rejected(WorkflowError(ErrorCode.REQUIREMENTS_SPEC_INVALID, "Frozen Markdown document does not satisfy its required template.", field_errors=tuple(errors)))
invocation = self.repository.begin_invocation(task_id, invocation_id, self._key(task_id, event, state.working_head, document.model_dump(mode="json")))
if invocation.status == "finished" and invocation.result is not None:
return self._restore(invocation.result)
try:
path = self.artifacts.write_json_once(task_id, f"documents/requirements-clarification-{sha256(clarification.question.encode()).hexdigest()[:12]}.json", payload)
written = self.artifacts.write_text_once(task_id, path, document.markdown.strip() + "\n")
except OSError as error:
return self._park_for_storage_retry(state, str(error))
next_state = transition(state, "waiting_for_user", error=ErrorCode.WAITING_FOR_USER, clarification_path=path)
result = Waiting(WorkflowError(
ErrorCode.WAITING_FOR_USER,
clarification.question,
details={"questions": [clarification.question], "source_quotes": list(clarification.source_quotes)},
))
if not self._commit(next_state, [{
"event": "requirements_waiting_for_user",
"invocation_id": invocation_id,
"review_path": path,
"message": clarification.question,
"questions": [clarification.question],
"source_quotes": list(clarification.source_quotes),
}], invocation, result):
kwargs = {"requirements_document_path": written} if path == "requirements.md" else {"completion_target_path": written} if path == "completion-target.md" else {"modeling_plan_path": written}
next_state = transition(state, event, **kwargs)
result = Accepted({"phase": next_state.phase.value, "path": written})
if not self._commit(next_state, [{"event": event, "invocation_id": invocation_id, "path": written}], invocation, result):
return Rejected(self._stale())
return result
def _replay(self, task_id: str, invocation_id: str) -> Accepted | Waiting | None:
invocation = self.repository.get_invocation(task_id, invocation_id)
if invocation is None or invocation.status != "finished" or invocation.result is None:
return None
return self._restore(invocation.result)
def _checklist_items(self, task_id: str) -> list[str]:
path = self.artifacts.task_dir(task_id) / "completion-target.md"
text = path.read_text(encoding="utf-8") if path.is_file() else ""
return [match.group(1).strip() for line in text.splitlines() if (match := _CHECKBOX.match(line))]
def _claim_errors(self, output: CompiledRequirementsSpec) -> list[dict[str, str]]:
errors: list[dict[str, str]] = []
for requirement_index, requirement in enumerate(output.requirements):
for claim_index, claim in enumerate(requirement.acceptance_claims):
try:
messages = self.registry.validate_expected(claim.claim_kind, claim.expected)
except ValueError:
messages = [{"path": "", "message": "VERIFIER_UNAVAILABLE"}]
errors.extend({"path": f"/requirements/{requirement_index}/acceptance_claims/{claim_index}/expected{item['path']}", "message": item["message"]} for item in messages)
return errors
def _normalize_compiled_spec(self, output: CompiledRequirementsSpec, targets: list[str]) -> tuple[CompiledRequirementsSpec, list[str]]:
normalized = output.model_copy(deep=True)
warnings: list[str] = []
for target, requirement in zip(targets, normalized.requirements, strict=True):
normalized_claims = []
for claim in requirement.acceptance_claims:
if claim.claim_kind in _RECORD_BOUND_CLAIMS:
warnings.append(
f"{claim.claim_kind} verifier for checklist item '{target}' requires server-bound topology records, so it was compiled as visual review."
)
claim.claim_kind = "visual"
claim.expected = {"description": target[:360]}
normalized_claims.append(claim)
continue
if self._is_local_cylindrical_span_bbox(requirement.acceptance_claims, claim, target):
self._move_bbox_z_to_outer_cylindrical_span(requirement.acceptance_claims, claim)
warnings.append(
f"Global bbox Z verifier for checklist item '{target}' was omitted because the target describes a local cylindrical span, not the finished part envelope."
)
continue
claim.expected = self.registry.normalize_expected(claim.claim_kind, claim.expected)
normalized_claims.append(claim)
requirement.acceptance_claims = normalized_claims or [AcceptanceClaimInput.model_validate({
"claim_kind": "visual",
"expected": {"description": target[:360]},
})]
return normalized, list(dict.fromkeys(warnings))
@staticmethod
def _restore(payload: dict[str, Any]) -> Accepted | Waiting:
if payload.get("result_type") == "waiting":
error = payload.get("error") if isinstance(payload.get("error"), dict) else {}
return Waiting(WorkflowError(
ErrorCode(str(error.get("code") or ErrorCode.WAITING_FOR_USER.value)),
str(error.get("message") or "Requirements need a user decision."),
tuple(error.get("field_errors") or ()),
bool(error.get("retryable")),
dict(error.get("details") or {}),
))
def _is_local_cylindrical_span_bbox(claims: list[Any], claim: Any, target: str) -> bool:
if claim.claim_kind != "bbox_dimension_mm" or claim.expected.get("axis") != "z":
return False
if RequirementsCommandHandler._target_describes_finished_envelope(target):
return False
return any(
getattr(item, "claim_kind", "") == "outer_cylindrical_surface"
for item in claims
)
@staticmethod
def _target_describes_finished_envelope(target: str) -> bool:
lowered = target.lower()
return any(token in lowered for token in (
"overall",
"total",
"finished part",
"entire part",
"whole part",
"bounding box",
"envelope",
"总",
"整体",
"成品",
"全高",
"包围盒",
))
@staticmethod
def _move_bbox_z_to_outer_cylindrical_span(claims: list[Any], bbox_claim: Any) -> None:
value = bbox_claim.expected.get("value")
if not isinstance(value, (int, float)):
return
for item in claims:
if getattr(item, "claim_kind", "") != "outer_cylindrical_surface":
continue
expected = getattr(item, "expected", None)
if not isinstance(expected, dict) or "axial_span_mm" in expected:
continue
expected["axial_span_mm"] = value
if "tolerance_mm" not in expected and isinstance(bbox_claim.expected.get("tolerance_mm"), (int, float)):
expected["tolerance_mm"] = bbox_claim.expected["tolerance_mm"]
return
@staticmethod
def _validate_requirements_document(markdown: str) -> list[dict[str, str]]:
# Markdown is a human-facing semantic artifact. Its content is frozen
# verbatim and is not executable, so headings are guidance for the
# author rather than a server-enforced protocol.
return []
@staticmethod
def _validate_completion_target(markdown: str) -> list[dict[str, str]]:
values = [match.group(1).strip() for line in markdown.splitlines() if (match := _CHECKBOX.match(line))]
errors: list[dict[str, str]] = []
if not values:
errors.append({"path": "/markdown", "message": "Completion target requires at least one unchecked checklist item."})
if len(values) != len(set(values)):
errors.append({"path": "/markdown", "message": "Completion checklist items must be unique."})
return errors
@staticmethod
def _validate_modeling_plan(markdown: str) -> list[dict[str, str]]:
return []
def _replay(self, task_id: str, invocation_id: str) -> Accepted | None:
invocation = self.repository.get_invocation(task_id, invocation_id)
return self._restore(invocation.result) if invocation and invocation.status == "finished" and invocation.result else None
@staticmethod
def _restore(payload: dict[str, Any]) -> Accepted:
return Accepted(payload.get("payload") if isinstance(payload.get("payload"), dict) else payload)
def _commit(self, state: TaskState, events: list[dict[str, Any]], invocation: Any, result: Accepted | Waiting) -> bool:
payload = {"result_type": "waiting", "error": result.error.payload()} if isinstance(result, Waiting) else {"result_type": "accepted", "payload": result.payload}
return self.repository.compare_and_swap(state, events=events, invocation_id=invocation.invocation_id, invocation_result=payload)
def _commit(self, state: TaskState, events: list[dict[str, Any]], invocation: Any, result: Accepted) -> bool:
return self.repository.compare_and_swap(state, events=events, invocation_id=invocation.invocation_id, invocation_result={"result_type": "accepted", "payload": result.payload})
@staticmethod
def _key(task_id: str, kind: str, head: str, value: dict[str, Any]) -> str:
encoded = json.dumps(value, ensure_ascii=True, sort_keys=True, separators=(",", ":"))
return sha256(f"{task_id}|{kind}|{head}|{encoded}".encode("utf-8")).hexdigest()
@staticmethod
def _requirements_markdown(contract: dict[str, Any]) -> str:
rows = ["# Requirements", "", str(contract.get("summary") or ""), ""]
assumptions = [str(item) for item in contract.get("assumptions") or () if str(item)]
if assumptions:
rows.extend(["## Assumptions", "", *[f"- {item}" for item in assumptions], ""])
rows.extend(["## Requirements", ""])
for item in contract.get("requirements") or ():
if not isinstance(item, dict):
continue
rows.append(f"- {item.get('statement')}")
rows.extend(f" - Assumption: {value}" for value in item.get("assumptions") or ())
return "\n".join(rows).rstrip() + "\n"
@staticmethod
def _completion_target_markdown(contract: dict[str, Any]) -> str:
rows = ["# Completion Target", ""]
for requirement in contract.get("requirements") or ():
if not isinstance(requirement, dict):
continue
rows.append(f"## {requirement.get('statement')}")
for claim in requirement.get("acceptance_claims") or ():
if isinstance(claim, dict):
rows.append(f"- [ ] {claim.get('claim_kind')}: {json.dumps(claim.get('expected') or {}, ensure_ascii=False, sort_keys=True)}")
rows.append("")
return "\n".join(rows).rstrip() + "\n"
return sha256(f"{task_id}|{kind}|{head}|{encoded}".encode()).hexdigest()
@staticmethod
def _stale() -> WorkflowError:
@@ -312,9 +292,5 @@ class RequirementsCommandHandler:
def _park_for_storage_retry(self, state: TaskState, message: str) -> Rejected:
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],
}])
self.repository.compare_and_swap(waiting, events=[{"event": "waiting_retry", "code": ErrorCode.STORAGE_FAILURE.value, "message": message[:1000]}])
return Rejected(WorkflowError(ErrorCode.STORAGE_FAILURE, "Requirements artifact storage is temporarily unavailable.", retryable=True))
+308 -45
View File
@@ -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]: