Files
cdsl-cad/backend/app/services/autonomous_cdsl_generation.py
T

2873 lines
174 KiB
Python

"""Autonomous, append-only CDSL authoring loop.
The author model is intentionally not asked to satisfy a provider-specific
strict JSON schema. It observes a frozen requirements document and real CAD
state, then submits one small JSON fragment at a time. Validation happens at
the only trust boundary that matters: the complete, materialised CDSL is
rebuilt by the CDSL-only engine in a staging directory before it can become a
revision.
"""
from __future__ import annotations
import asyncio
import ast
import base64
from collections.abc import AsyncIterator, Awaitable, Callable
from copy import deepcopy
import hashlib
import json
import math
from pathlib import Path
import re
import secrets
import shutil
from typing import Any
from app.services.cdsl_fragment import (
AutonomousFragmentError,
autonomous_candidate_prompt_tokens,
autonomous_selector_tokens,
materialize_autonomous_fragment,
)
from app.services.engine_service import (
feature_atomic_contract,
load_engine,
step_to_glb,
topology_sidecars,
topology_snapshot,
validate_cdsl,
)
from app.services.review_renderer import ReviewRenderError, render_checkpoint, render_section
from app.services.storage import WorkspaceStore, now_iso, read_json, write_json
from app.services.visual_review import VisualReviewError, review_candidate_batch, review_checkpoint
from app.settings import ProviderConfig, ProviderModel, Settings
Completion = Callable[[list[dict[str, Any]], list[dict[str, Any]], ProviderConfig, ProviderModel, str | None], Awaitable[dict[str, Any]]]
class AutonomousGenerationError(RuntimeError):
pass
def _is_author_quota_error(error: Exception) -> bool:
"""Return true only for an explicit provider quota exhaustion response."""
message = str(error).lower()
return "llm request failed (429)" in message and ("quota" in message or "exhausted" in message)
def _is_author_transport_error(error: Exception) -> bool:
"""Identify a retryable author-provider outage, never a CAD failure."""
return str(error).lower().startswith("llm connection failed after")
def parse_tool_arguments(raw: str) -> tuple[dict[str, Any], bool]:
"""Parse ordinary function-call arguments from imperfect compatible APIs.
Providers occasionally return a JavaScript object literal (unquoted keys)
or a Python-style dictionary despite advertising OpenAI-compatible tool
calling. Accept only those shallow syntax repairs, then require a normal
object. The result still goes through each tool's own validation and the
CDSL/Engine trust boundary; this is transport recovery, not a schema
bypass.
"""
source = str(raw or "{}").strip()
if source.startswith("```") and source.endswith("```"):
source = re.sub(r"^```(?:json)?\s*|\s*```$", "", source, flags=re.IGNORECASE).strip()
try:
parsed = json.loads(source)
if not isinstance(parsed, dict):
raise ValueError("tool arguments must be an object")
return parsed, False
except json.JSONDecodeError as json_error:
# JavaScript-style object literals: {kind: "face", limit: 12}.
repaired = re.sub(
r"([,{]\s*)([A-Za-z_$][A-Za-z0-9_$-]*)(\s*:)",
r'\1"\2"\3',
source,
)
if repaired != source:
try:
parsed = json.loads(repaired)
if not isinstance(parsed, dict):
raise ValueError("tool arguments must be an object")
return parsed, True
except json.JSONDecodeError:
pass
# Python-style literal dictionaries are parsed without evaluation.
try:
parsed = ast.literal_eval(source)
except (ValueError, SyntaxError) as literal_error:
raise ValueError(str(json_error)) from literal_error
if not isinstance(parsed, dict):
raise ValueError("tool arguments must be an object")
return parsed, True
_CHECKLIST_LINE = re.compile(r"^\s*(?:[-*+]\s+|\d+[.)]\s+)\[([ xX?])\]\s+(.+?)\s*$")
def _checklist_key(text: str) -> str:
return re.sub(r"\s+", " ", str(text or "").strip()).casefold()
def parse_completion_checklist(markdown: str, *, require_unchecked: bool = False) -> list[str]:
"""Read a small, human-authored Markdown checkbox list without JSON rules.
The requirements document remains unrestricted prose. This separate
document gives the author a durable list of observable completion claims
that can be carried into every turn and audited before publication.
"""
items: list[str] = []
keys: set[str] = set()
for line in str(markdown or "").splitlines():
match = _CHECKLIST_LINE.match(line)
if not match:
continue
state, item = match.groups()
item = item.strip()
key = _checklist_key(item)
if not item:
raise AutonomousGenerationError("completion.md contains an empty checklist item")
if require_unchecked and state.lower() != " ":
raise AutonomousGenerationError("completion.md must begin with every checklist item unchecked ([ ])")
if key in keys:
raise AutonomousGenerationError(f"completion.md contains a duplicate checklist item: {item}")
keys.add(key)
items.append(item)
if not items:
raise AutonomousGenerationError("completion.md must contain at least one Markdown checklist item such as '- [ ] continuous shaft bore'")
return items
def parse_completion_audit(markdown: str, checklist: list[str]) -> list[dict[str, str]]:
"""Match an author audit to the frozen checklist and require evidence.
This deliberately validates only the audit bookkeeping. It never maps a
checklist item to geometry or generates CDSL; the engine and independent
visual review remain the geometry trust boundaries.
"""
expected = {_checklist_key(item): item for item in checklist}
found: dict[str, dict[str, str]] = {}
for line in str(markdown or "").splitlines():
match = _CHECKLIST_LINE.match(line)
if not match:
continue
marker, raw = match.groups()
item, separator, evidence = raw.partition("::")
key = _checklist_key(item)
if key not in expected:
raise AutonomousGenerationError(f"completion audit contains an item not present in completion.md: {item.strip()}")
if key in found:
raise AutonomousGenerationError(f"completion audit repeats an item: {expected[key]}")
status = {"x": "complete", "?": "uncertain", " ": "missing"}[marker.lower()]
evidence = evidence.strip()
if status == "complete" and not evidence:
raise AutonomousGenerationError(f"completion audit needs evidence after '::' for completed item: {expected[key]}")
found[key] = {"item": expected[key], "status": status, "evidence": evidence}
missing = [item for key, item in expected.items() if key not in found]
if missing:
raise AutonomousGenerationError("completion audit must account for every frozen checklist item; missing: " + "; ".join(missing))
return [found[_checklist_key(item)] for item in checklist]
def _format_correction_card(
engine: Any,
*,
fragment_json: str,
error_message: str,
head: str,
previous: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""Build an exact, runtime-derived correction instruction for the author.
Free-form authoring is deliberate, but an author should never have to
guess which parameter spelling the installed engine accepts after local
validation has already established the answer. This card is evidence,
not a geometry template: it exposes only the contract of the feature the
author attempted and leaves the workplane, profile and dimensions to it.
"""
raw = str(fragment_json or "")
atomic_id = ""
attempted_params: dict[str, Any] = {}
attempted_feature_count = 0
try:
fragment = json.loads(raw)
feature = fragment.get("feature") if isinstance(fragment, dict) else None
if not isinstance(feature, dict) and isinstance(fragment, dict):
features = fragment.get("features")
attempted_feature_count = len(features) if isinstance(features, list) else 0
feature = features[0] if isinstance(features, list) and features and isinstance(features[0], dict) else None
elif isinstance(feature, dict):
attempted_feature_count = 1
atomic_id = str((feature or {}).get("atomic_id") or "")
attempted_params = (feature or {}).get("params") if isinstance((feature or {}).get("params"), dict) else {}
except json.JSONDecodeError:
pass
contract: dict[str, Any] | None = None
if atomic_id:
try:
contract = feature_atomic_contract(engine, atomic_id)
except (ValueError, RuntimeError):
contract = None
signature = f"{head}|{atomic_id or 'unknown'}|{error_message}"
prior_signature = str((previous or {}).get("signature") or "")
repeat_count = int((previous or {}).get("repeat_count") or 0) + 1 if signature == prior_signature else 1
card: dict[str, Any] = {
"signature": signature,
"repeat_count": repeat_count,
"working_head": head,
"error": error_message,
"atomic_id": atomic_id or None,
"instruction": "Correct this exact error in the next submit_cdsl_fragment call. Submit only a replacement fragment; do not inspect, measure, render, complete, or repeat the invalid parameter spelling.",
}
if contract is not None:
operation = _operation_contract_payload(engine, atomic_id)
required = contract["required_params"]
author_required = operation["author_required_params"]
optional = contract["optional_params"]
params_shape = {name: "required value" for name in author_required}
if atomic_id.startswith("extrude_"):
params_shape["distance_mm"] = "positive number"
elif atomic_id.startswith("revolve_"):
params_shape["angle_deg"] = "positive number up to 360"
params_shape["axis"] = {
"origin_mm": "[x_mm, y_mm, z_mm]",
"direction": "[dx, dy, dz] non-zero vector",
}
card.update({
"required_params": required,
"author_required_params": author_required,
"optional_params": optional,
"server_injected_params": operation["server_injected_params"],
"position_format": operation["position_format"],
"additional_params_forbidden": True,
"requires_sketch": contract["requires_sketch"],
"selector_tokens_allowed": operation["selector_rule"] is not None,
"minimal_params_shape": params_shape,
"valid_feature_shape": {"atomic_id": atomic_id, "params": params_shape},
"unsupported_attempted_params": sorted(
key for key in attempted_params
if key not in {*required, *optional}
),
})
selector_error = "selector" in error_message.lower()
if selector_error and operation["selector_rule"]:
# Finish operations are selector-only. Showing their parameter
# contract without the required opaque edge handles made a
# correction card look complete when it could never rebuild.
selector_kind = str(operation["selector_rule"].get("kind") or "topology")
card.update({
"selector_tokens_required": True,
"selector_token_kind": selector_kind,
})
valid_shape = card.get("valid_feature_shape")
if isinstance(valid_shape, dict):
valid_shape["selector_tokens"] = [f"1..64 opaque {selector_kind} token(s) returned by inspect_topology"]
match = re.search(r"A fragment may add at most (\d+) feature\(s\)", error_message)
if match:
allowed = int(match.group(1))
card.update({
"attempted_feature_count": attempted_feature_count,
"max_features_per_fragment": allowed,
"instruction": (
f"This fragment contains {attempted_feature_count or 'multiple'} feature(s), but this task accepts at most {allowed} feature(s) per submit_cdsl_fragment call. "
"Split the work into coherent batches within that limit. Each batch is rebuilt and independently reviewed before it can become a checkpoint."
),
})
return card
def _requires_edge_selector_recovery(correction: dict[str, Any]) -> bool:
"""Return whether a finish operation needs a fresh edge-token lookup."""
return (
str(correction.get("atomic_id") or "") in {"chamfer", "fillet"}
and bool(correction.get("selector_tokens_required"))
)
def _fragment_selector_tokens(fragment: dict[str, Any]) -> list[str]:
"""Read a fragment's authored selector tokens without changing it."""
feature = fragment.get("feature") if isinstance(fragment.get("feature"), dict) else None
if feature is None:
features = fragment.get("features")
feature = features[0] if isinstance(features, list) and features and isinstance(features[0], dict) else None
raw = (feature or {}).get("selector_tokens", fragment.get("selector_tokens", []))
return [item for item in raw if isinstance(item, str)] if isinstance(raw, list) else []
def _final_repair_card(task: dict[str, Any], review: dict[str, Any]) -> dict[str, Any]:
"""Persist final-review evidence with a non-binding rollback recommendation.
The author, not the server, decides whether a rollback is necessary. A
reviewer can nevertheless identify that the last appended feature is
geometrically wrong. In that case the immediate parent checkpoint is the
narrowest lossless rollback candidate and should be made explicit as an
opaque token. Without that hint the author can select the root checkpoint
and discard correct upstream work along with one bad cut.
"""
branch_id = str(task.get("active_branch_id") or "main")
current_revision = str(task.get("active_revision") or "")
evidence = list(review.get("evidence") or [])
card: dict[str, Any] = {
"working_head": f"{branch_id}:{current_revision}",
"evidence_revision": current_revision,
"evidence_stale_after_checkpoint": False,
"review_verdict": str(review.get("verdict") or "repair"),
"confidence": review.get("confidence"),
"requirement_ids": list(review.get("requirement_ids") or []),
"affected_node_ids": list(review.get("affected_node_ids") or []),
"evidence": evidence,
"instruction": "Choose a rollback ancestor when an existing feature is geometrically wrong or cannot be repaired by append-only CDSL. Otherwise inspect the current checkpoint as needed, then add one small corrective fragment. If later checkpoints exist after this review, treat its evidence as a list of requirements to re-evaluate, not as a current count or coordinate fact. If a candidate produces no geometric change, inspect the current model or CDSL before choosing the next action.",
"topology_observed": False,
}
last_feature_failure = any(
any(marker in str(item).lower() for marker in ("the last ", "last extrude", "last feature"))
and any(marker in str(item).lower() for marker in ("wrong", "rather than", "incorrect", "error"))
for item in evidence
)
revisions = {
str(item.get("revision_id") or ""): item
for item in task.get("revisions") or ()
if isinstance(item, dict)
}
parent_revision = str((revisions.get(current_revision) or {}).get("parent_revision_id") or "")
if last_feature_failure and parent_revision:
card["rollback_guidance"] = {
"recommended_checkpoint_token": _checkpoint_token(branch_id, parent_revision),
"reason": "The final review identifies the last appended feature as geometrically wrong. This token targets the immediate predecessor, removing only that feature while preserving earlier checkpoints.",
"decision_rule": "This is a recommendation, not an automatic rollback. Choose it only when you agree the bad feature cannot be repaired by an append-only fragment; otherwise submit the corrective fragment.",
}
return card
def _preferred_tool_call(calls: list[dict[str, Any]], *, allowed_names: set[str] | None = None) -> dict[str, Any]:
"""Choose a state-changing call when a provider emits several at once.
The loop intentionally handles one call per turn so all evidence remains
current. Providers nevertheless often batch an observation followed by
``complete_task`` or a fragment submission. Taking the first call in
that response repeatedly starves the action and trips no-progress even
when the author has already requested it. This ordering only chooses an
already authored call; it never synthesizes a CAD operation.
"""
priority = {
"record_step_review": 0,
"commit_candidate": 0,
"discard_candidate": 0,
"rollback_checkpoint": 0,
"complete_task": 1,
"submit_cdsl_fragment": 1,
}
permitted = [
call for call in calls
if not allowed_names
or str(((call.get("function") if isinstance(call.get("function"), dict) else {}) or {}).get("name") or "") in allowed_names
]
candidates = permitted or calls
def key(call: dict[str, Any]) -> tuple[int, int]:
function = call.get("function") if isinstance(call.get("function"), dict) else {}
return priority.get(str(function.get("name") or ""), 2), calls.index(call)
return min(candidates, key=key)
def autonomous_tools() -> list[dict[str, Any]]:
"""Small portable schemas for ordinary OpenAI-compatible tool calling."""
point3 = {"type": "array", "items": {"type": "number"}, "minItems": 3, "maxItems": 3}
empty = {"type": "object", "properties": {}, "additionalProperties": False}
return [
{
"type": "function",
"function": {
"name": "write_requirements_document",
"description": "First tool only. Write the complete free-form frozen requirements.md: faithfully preserve every source requirement, then add understanding, assumptions, coordinate conventions, completion standard and any unresolved dimensions you decide to assume. Never remove, replace, or weaken source intent.",
"parameters": {"type": "object", "properties": {"markdown": {"type": "string", "minLength": 1}}, "required": ["markdown"], "additionalProperties": False},
},
},
{
"type": "function",
"function": {
"name": "write_completion_checklist",
"description": "Second tool only, after requirements.md. Write frozen completion.md as a short Markdown checklist. Use one observable completion claim per '- [ ] item' line. Do not include CDSL, IDs, a modelling plan, or checked items.",
"parameters": {"type": "object", "properties": {"markdown": {"type": "string", "minLength": 1}}, "required": ["markdown"], "additionalProperties": False},
},
},
{"type": "function", "function": {"name": "inspect_model", "description": "Inspect the active checkpoint's compact geometry, feature and sketch summary. Use before deciding the next CDSL step.", "parameters": empty}},
{
"type": "function",
"function": {
"name": "read_cdsl_slice",
"description": "Read only the requested committed feature and sketch records. Use inspect_model first to learn their IDs.",
"parameters": {"type": "object", "properties": {"feature_ids": {"type": "array", "items": {"type": "string"}, "maxItems": 12}, "sketch_ids": {"type": "array", "items": {"type": "string"}, "maxItems": 12}}, "additionalProperties": False},
},
},
{
"type": "function",
"function": {
"name": "measure_model",
"description": "Return deterministic bbox, volume, body count and local topology measurements for the active checkpoint or successful staged candidate.",
"parameters": {"type": "object", "properties": {"feature_id": {"type": "string"}}, "additionalProperties": False},
},
},
{
"type": "function",
"function": {
"name": "inspect_topology",
"description": "Inspect compact current topology and opaque selector tokens. Only copy supplied tokens into a fragment's selector_tokens; never invent selector objects.",
"parameters": {"type": "object", "properties": {"kind": {"type": "string", "enum": ["face", "edge", "vertex", "plane", "axis", "body"]}, "limit": {"type": "integer", "minimum": 1, "maximum": 64}}, "additionalProperties": False},
},
},
{
"type": "function",
"function": {
"name": "get_cdsl_operation_contract",
"description": "Read the exact runtime input contract for one supported atomic operation before authoring an unfamiliar fragment. This documents fields only; it never creates geometry or CDSL.",
"parameters": {"type": "object", "properties": {"atomic_id": {"type": "string", "minLength": 1}}, "required": ["atomic_id"], "additionalProperties": False},
},
},
{
"type": "function",
"function": {
"name": "render_views",
"description": "Render deterministic top, bottom, front, back, left, right and isometric technical views of the checkpoint or staged candidate. Images are supplied on the next turn only when requested.",
"parameters": {"type": "object", "properties": {}, "additionalProperties": False},
},
},
{
"type": "function",
"function": {
"name": "render_section",
"description": "Render a true OpenCascade section through the current model. Use it for hole depth, internal cavities or slot placement.",
"parameters": {"type": "object", "properties": {"origin_mm": point3, "normal": point3}, "required": ["origin_mm", "normal"], "additionalProperties": False},
},
},
{
"type": "function",
"function": {
"name": "submit_cdsl_fragment",
"description": "Submit the next coherent append-only CAD batch as JSON. Include batch_goal and a fragment with 1..6 ordered feature operations using {sketches,features}; single {sketch,feature} remains accepted. For revolve batches, pass shared_revolve_axis as {origin_mm,direction} once instead of repeating axis within fragment_json. Do not include CDSL ids, dependencies, sketch_id or raw selectors. Topology-sensitive operations may use only selector_tokens from the current checkpoint.",
"parameters": {"type": "object", "properties": {"batch_goal": {"type": "string", "minLength": 8, "maxLength": 800}, "fragment_json": {"type": "string", "minLength": 2}, "shared_revolve_axis": {"type": "object", "properties": {"origin_mm": point3, "direction": point3}, "required": ["origin_mm", "direction"], "additionalProperties": False}}, "required": ["batch_goal", "fragment_json"], "additionalProperties": False},
},
},
{
"type": "function",
"function": {
"name": "record_geometry_conclusion",
"description": "Required after a candidate rebuilt with unchanged geometry or a completion audit finds unresolved requirements. Cite one or more current diagnostic evidence_refs, state the root cause, then choose modify, rollback, or complete. For modify, optimization_plan must explain the next materially different modelling action. This records a decision only and never writes CDSL.",
"parameters": {
"type": "object",
"properties": {
"root_cause": {"type": "string", "enum": ["duplicate_feature", "selector_miss", "invalid_plane_or_direction", "unsupported_operation", "incomplete_requirements", "unknown"]},
"evidence_refs": {"type": "array", "items": {"type": "string", "minLength": 1}, "minItems": 1, "maxItems": 3},
"decision": {"type": "string", "enum": ["modify", "rollback", "complete"]},
"optimization_plan": {"type": "object", "additionalProperties": True},
},
"required": ["root_cause", "evidence_refs", "decision"],
"additionalProperties": False,
},
},
},
{
"type": "function",
"function": {
"name": "rollback_checkpoint",
"description": "Move the working head to any ancestor checkpoint token returned by inspect_model, or root. This creates a new branch and supersedes descendants without deleting history.",
"parameters": {"type": "object", "properties": {"checkpoint_token": {"type": "string", "minLength": 1}, "reason": {"type": "string"}}, "required": ["checkpoint_token"], "additionalProperties": False},
},
},
{
"type": "function",
"function": {
"name": "complete_task",
"description": "Request final publication only after every requirement in requirements.md has been checked. The backend will rebuild, render all seven views and request independent visual review.",
"parameters": {"type": "object", "properties": {"self_review": {"type": "string", "minLength": 1}}, "required": ["self_review"], "additionalProperties": False},
},
},
]
def _runtime_summary(engine: Any) -> list[dict[str, Any]]:
atomic_ids = sorted(str(item) for item in getattr(engine, "SUPPORTED_ATOMIC_IDS", ()) if str(item))
result = []
for atomic_id in atomic_ids:
try:
contract = feature_atomic_contract(engine, atomic_id)
except ValueError:
continue
result.append({
"atomic_id": atomic_id,
"requires_sketch": contract["requires_sketch"],
"selector_required": bool(contract.get("selector_slot")),
})
return result
def _operation_contract_payload(engine: Any, atomic_id: str) -> dict[str, Any]:
"""Return runtime documentation, never authored CAD geometry."""
contract = feature_atomic_contract(engine, atomic_id)
slot = contract.get("selector_slot") if isinstance(contract.get("selector_slot"), dict) else None
runtime_required = list(contract["required_params"])
server_injected: list[str] = []
selector_rule: dict[str, Any] | None = None
# Some runtime parameters are deliberately server-owned because their
# values must come from the immutable current topology snapshot. This
# documents the existing materializer boundary; it does not choose a
# plane, face, axis, or any other authored geometry.
if atomic_id.startswith("hole_") or atomic_id == "hole_wizard":
if "host_face" in runtime_required or "host_face" in contract["optional_params"]:
server_injected.append("host_face")
selector_rule = {"kind": "face", "min_items": 1, "max_items": 1}
elif slot:
slot_path = str(slot.get("path") or "")
if slot_path == "params.mirror_plane":
server_injected.append("mirror_plane")
selector_rule = {"kind": "plane", "min_items": int(slot.get("min_items") or 1), "max_items": int(slot.get("max_items") or 1)}
elif slot_path == "params.host_face":
server_injected.append("host_face")
selector_rule = {"kind": "face", "min_items": int(slot.get("min_items") or 1), "max_items": int(slot.get("max_items") or 1)}
elif slot_path == "feature.selectors":
selector_rule = {
"kind": "edge" if atomic_id in {"chamfer", "fillet"} else "edge_or_face",
"min_items": int(slot.get("min_items") or 1),
"max_items": int(slot.get("max_items") or 1),
}
if atomic_id.startswith("pattern_") and "source_feature_ids" in runtime_required:
server_injected.append("source_feature_ids")
author_required = [name for name in runtime_required if name not in server_injected]
return {
"atomic_id": contract["atomic_id"],
"summary": contract["summary"],
"requires_sketch": contract["requires_sketch"],
"runtime_required_params": runtime_required,
"author_required_params": author_required,
"optional_params": contract["optional_params"],
"server_injected_params": server_injected,
"position_format": contract.get("position_format") or None,
"selector_rule": selector_rule,
"authoring_rule": (
"For every revolve feature, supply params.axis as {origin_mm:[x,y,z], direction:[dx,dy,dz]}. "
"When one batch has several revolve features with the same axis, declare that explicit author-defined "
"axis once as top-level revolve_axis and the server copies it only to those missing params.axis values. "
"Never use selector_tokens for a revolve axis."
if atomic_id.startswith("revolve_") else
"Put only opaque selector tokens returned by inspect_topology in selector_tokens. "
"The server injects server_injected_params from those tokens."
if selector_rule else
"Do not add selector_tokens for this operation."
),
}
def _checkpoint_token(branch_id: str, revision_id: str) -> str:
"""Use a short opaque rollback handle instead of exposing revision IDs."""
if not revision_id:
return "root"
import hashlib
# The digest must be deterministic for a restart; it is only an opaque
# command handle, never a persisted CAD identifier.
digest = hashlib.sha256(f"{branch_id}|{revision_id}".encode("utf-8")).hexdigest()[:16]
return f"checkpoint_{digest}"
def _rollback_tokens(task: dict[str, Any]) -> dict[str, str]:
"""Return only the active branch lineage as author-facing rollback handles."""
branch_id = str(task.get("active_branch_id") or "main")
revisions = {
str(item.get("revision_id") or ""): item
for item in task.get("revisions") or ()
if isinstance(item, dict)
}
tokens = {"root": ""}
pointer = str(task.get("active_revision") or "")
while pointer:
tokens[_checkpoint_token(branch_id, pointer)] = pointer
pointer = str((revisions.get(pointer) or {}).get("parent_revision_id") or "")
return tokens
def _model_paths(store: WorkspaceStore, task_id: str, task: dict[str, Any]) -> tuple[Path | None, Path | None, Path | None, str]:
"""Return active candidate artifacts first, otherwise active checkpoint."""
candidate_id = str(task.get("active_candidate_id") or "")
if candidate_id:
try:
candidate_dir = store.candidate_dir(task_id, candidate_id)
except ValueError:
candidate_dir = None
if candidate_dir and (candidate_dir / "candidate.json").is_file() and (candidate_dir / "model.cdsl.json").is_file():
return candidate_dir / "model.cdsl.json", candidate_dir / "model.topology.json", candidate_dir / "model.step", candidate_id
revision_id = str(task.get("active_revision") or task.get("current_revision") or "")
if not revision_id:
return None, None, None, ""
revision = store.revision_dir(task_id, revision_id)
return revision / "model.cdsl.json", revision / "model.topology.json", revision / "model.step", revision_id
def _geometry_fingerprint(snapshot: dict[str, Any]) -> str:
"""Return an identity-free signature of the final selectable geometry.
CDSL and STEP byte streams legitimately change when a new feature is
appended, even if a boolean operation misses the solid or repeats an
existing cut. The runtime topology has the opposite property: after
removing transient feature/body/record identities, matching face and edge
geometry represents the same final solid. Round only numerical noise from
repeated OpenCascade rebuilds, not meaningful CAD dimensions.
"""
def normalize(value: Any) -> Any:
if isinstance(value, float):
rounded = round(value, 7)
return 0.0 if rounded == 0 else rounded
if isinstance(value, list):
return [normalize(item) for item in value]
if isinstance(value, dict):
return {str(key): normalize(item) for key, item in sorted(value.items())}
return value
geometry_records = []
for record in snapshot.get("records") or ():
if not isinstance(record, dict) or record.get("synthetic") is True:
continue
kind = str(record.get("kind") or "")
geometry = record.get("geometry")
# Vertex enumeration is not stable across equivalent OpenCascade
# rebuilds. Faces and edges describe the solid boundary and remain
# stable after feature/body identities are removed.
if kind not in {"face", "edge"} or not isinstance(geometry, dict):
continue
normalized_geometry = normalize(geometry)
if kind == "edge":
# An edge bounds an undirected curve. OpenCascade may emit the
# same edge with start/end reversed after an equivalent rebuild,
# including for closed circles, so endpoint order is never a
# meaningful solid difference.
start = normalized_geometry.get("start_mm")
end = normalized_geometry.get("end_mm")
if isinstance(start, list) and isinstance(end, list):
start_key = json.dumps(start, separators=(",", ":"))
end_key = json.dumps(end, separators=(",", ":"))
if end_key < start_key:
normalized_geometry["start_mm"] = end
normalized_geometry["end_mm"] = start
geometry_records.append({"kind": kind, "geometry": normalized_geometry})
if not geometry_records:
return ""
canonical = sorted(
json.dumps(item, ensure_ascii=True, sort_keys=True, separators=(",", ":"))
for item in geometry_records
)
return hashlib.sha256("\n".join(canonical).encode("utf-8")).hexdigest()
def _compact_measurement_records(records: list[dict[str, Any]], *, limit: int = 16) -> tuple[dict[str, int], list[dict[str, Any]]]:
"""Return targeted measurement facts without serialising a full snapshot."""
useful = (
"bbox_mm", "center_mm", "normal", "plane_normal", "surface_type", "curve_type",
"radius_mm", "length_mm", "area_mm2",
)
counts: dict[str, int] = {}
sample: list[dict[str, Any]] = []
for record in records:
kind = str(record.get("kind") or "unknown")
counts[kind] = counts.get(kind, 0) + 1
if len(sample) >= limit:
continue
geometry = record.get("geometry") if isinstance(record.get("geometry"), dict) else {}
sample.append({
"kind": kind,
"geometry": {key: deepcopy(geometry[key]) for key in useful if key in geometry},
})
return counts, sample
def _parent_geometry_fingerprint(store: WorkspaceStore, task_id: str, parent_revision_id: str) -> str | None:
if not parent_revision_id:
return None
snapshot = read_json(store.revision_dir(task_id, parent_revision_id) / "model.topology.json")
return _geometry_fingerprint(snapshot) if isinstance(snapshot, dict) else None
def _parent_model_volume(store: WorkspaceStore, task_id: str, parent_revision_id: str) -> float | None:
"""Read the parent rebuild volume when it is a finite positive value."""
if not parent_revision_id:
return None
report = read_json(store.revision_dir(task_id, parent_revision_id) / "rebuild-report.json")
engine_result = report.get("engine_result") if isinstance(report, dict) else None
try:
volume = float((engine_result or {}).get("volume_mm3"))
except (TypeError, ValueError):
return None
return volume if math.isfinite(volume) and volume > 0 else None
def _material_volume_tolerance(health: dict[str, Any]) -> float:
"""Return a scale-aware lower bound for observable material change.
OpenCascade can perturb reported volume by a few thousandths of a cubic
millimetre after a redundant overlapping boolean. That must not become a
checkpoint. The bound remains tiny relative to the enclosing model so a
real small machined feature still passes.
"""
dimensions = ((health.get("bbox_mm") or {}).get("dimensions") if isinstance(health, dict) else None) or []
try:
enclosing_volume = abs(float(dimensions[0]) * float(dimensions[1]) * float(dimensions[2]))
except (IndexError, TypeError, ValueError):
enclosing_volume = 0.0
return max(0.01, enclosing_volume * 1e-8)
def _has_material_volume_change(health: dict[str, Any], parent_volume_mm3: float | None) -> bool:
if parent_volume_mm3 is None:
return True
try:
candidate_volume = float(health.get("volume_mm3"))
except (TypeError, ValueError):
return False
return abs(candidate_volume - parent_volume_mm3) > _material_volume_tolerance(health)
def _candidate_health(engine_result: dict[str, Any], step_path: Path, glb_path: Path) -> dict[str, Any]:
bbox = engine_result.get("bbox_mm")
minimum = bbox.get("min") if isinstance(bbox, dict) else None
maximum = bbox.get("max") if isinstance(bbox, dict) else None
values = [*(minimum or []), *(maximum or [])] if isinstance(minimum, list) and isinstance(maximum, list) else []
if len(values) != 6 or not all(isinstance(value, (int, float)) and math.isfinite(float(value)) for value in values):
raise AutonomousGenerationError("Engine returned an invalid model bounding box")
if not step_path.is_file() or step_path.stat().st_size <= 0:
raise AutonomousGenerationError("Engine did not produce a valid STEP artifact")
if not glb_path.is_file() or glb_path.stat().st_size <= 0:
raise AutonomousGenerationError("STEP preview conversion did not produce a valid GLB artifact")
dimensions = [float(maximum[index]) - float(minimum[index]) for index in range(3)]
if any(value < 0 or not math.isfinite(value) for value in dimensions):
raise AutonomousGenerationError("Engine returned an invalid model extent")
try:
volume = float(engine_result.get("volume_mm3"))
except (TypeError, ValueError) as error:
raise AutonomousGenerationError("Engine returned no finite model volume") from error
if not math.isfinite(volume) or volume <= 0:
raise AutonomousGenerationError("Engine returned a non-positive model volume")
try:
solid_count = int(engine_result.get("solid_count"))
except (TypeError, ValueError) as error:
raise AutonomousGenerationError("Engine returned no valid solid count") from error
if solid_count < 1:
raise AutonomousGenerationError("Engine returned no valid solid body")
return {
"bbox_mm": {"min": [float(value) for value in minimum], "max": [float(value) for value in maximum], "dimensions": dimensions},
"volume_mm3": volume,
"solid_count": solid_count,
"feature_count": len(engine_result.get("feature_results") or []),
"topology_record_count": len(engine_result.get("topology_records") or []),
}
def build_candidate(
*,
settings: Settings,
store: WorkspaceStore,
task_id: str,
cdsl: dict[str, Any],
fragment_audit: dict[str, Any],
parent_revision_id: str,
) -> dict[str, Any]:
"""Fully rebuild an uncommitted candidate in its isolated staging folder."""
candidate_id, candidate_dir = store.new_candidate(task_id)
engine = load_engine(settings)
cdsl_copy = deepcopy(cdsl)
cdsl_copy["part_id"] = task_id
try:
write_json(candidate_dir / "model.cdsl.json", cdsl_copy)
write_json(candidate_dir / "fragment.json", fragment_audit)
validate_cdsl(cdsl_copy, engine)
step_path = candidate_dir / "model.step"
glb_path = candidate_dir / "model.glb"
engine_result = engine.run_cdsl_only(cdsl_copy, step_path)
if str(engine_result.get("engine") or "") != "cdsl_only":
raise AutonomousGenerationError("Engine did not run the CDSL-only execution path")
preview = step_to_glb(step_path, glb_path)
health = _candidate_health(engine_result, step_path, glb_path)
snapshot = topology_snapshot(engine_result, task_id=task_id, revision_id=candidate_id, preview=preview)
geometry_fingerprint = _geometry_fingerprint(snapshot)
parent_geometry_fingerprint = _parent_geometry_fingerprint(store, task_id, parent_revision_id)
parent_volume = _parent_model_volume(store, task_id, parent_revision_id)
fingerprints_match = bool(geometry_fingerprint and parent_geometry_fingerprint and geometry_fingerprint == parent_geometry_fingerprint)
volume_changed = _has_material_volume_change(health, parent_volume)
if fingerprints_match or not volume_changed:
raise AutonomousGenerationError(
"CANDIDATE_GEOMETRY_UNCHANGED: the candidate rebuilt successfully but did not cause a material change above engine tolerance. "
"Reassess the next modelling step before submitting another candidate."
)
selector, edges = topology_sidecars(engine_result, preview, snapshot=snapshot)
write_json(candidate_dir / "model.topology.json", snapshot)
write_json(candidate_dir / "model.selector.json", selector)
write_json(candidate_dir / "model.edges.json", edges)
write_json(candidate_dir / "rebuild-report.json", {"engine_result": engine_result, "preview": preview, "health": health, "validated_at": now_iso()})
candidate = {
"schema_version": "cad.autonomous-candidate.v1",
"candidate_id": candidate_id,
"status": "built",
"created_at": now_iso(),
"parent_revision_id": parent_revision_id,
"health": health,
"geometry_fingerprint": geometry_fingerprint,
"parent_volume_mm3": parent_volume,
"material_volume_tolerance_mm3": _material_volume_tolerance(health),
"feature_ids": fragment_audit.get("assigned_feature_ids") or [],
"sketch_ids": fragment_audit.get("assigned_sketch_ids") or [],
"compatibility_fixes": fragment_audit.get("compatibility_fixes") or [],
}
write_json(candidate_dir / "candidate.json", candidate)
return candidate
except Exception as error:
write_json(candidate_dir / "candidate.json", {
"schema_version": "cad.autonomous-candidate.v1", "candidate_id": candidate_id, "status": "failed",
"created_at": now_iso(), "parent_revision_id": parent_revision_id, "error": str(error),
})
store.clear_active_candidate(task_id, candidate_id)
raise
def commit_candidate(
*,
store: WorkspaceStore,
task_id: str,
candidate_id: str,
summary: str,
) -> dict[str, Any]:
"""Promote immutable staging artifacts to an append-only checkpoint revision."""
task = store.read_task(task_id) or {}
if str(task.get("active_candidate_id") or "") != candidate_id:
raise AutonomousGenerationError("The requested candidate is no longer active")
candidate_dir = store.candidate_dir(task_id, candidate_id)
candidate = read_json(candidate_dir / "candidate.json")
if not isinstance(candidate, dict) or candidate.get("status") != "built":
raise AutonomousGenerationError("Only a successful staged candidate can be committed")
candidate_review_path = Path(str(candidate.get("candidate_review_path") or ""))
candidate_prefix = Path("candidates") / candidate_id
try:
review_relative = candidate_review_path.relative_to(candidate_prefix)
except ValueError as error:
raise AutonomousGenerationError("A staged candidate must have an independent review stored in its own audit directory") from error
if not review_relative.parts or not (candidate_dir / review_relative).is_file():
raise AutonomousGenerationError("The staged candidate independent review is missing")
candidate_review = read_json(candidate_dir / review_relative, {})
if (
not isinstance(candidate_review, dict)
or candidate_review.get("verdict") != "accept"
or candidate_review.get("batch_goal_status") != "achieved"
):
raise AutonomousGenerationError("Only an accepted independent candidate review can create a checkpoint")
revision_id, revision_dir = store.next_revision(task_id)
try:
shutil.copytree(candidate_dir, revision_dir, dirs_exist_ok=True)
except Exception:
shutil.rmtree(revision_dir, ignore_errors=True)
raise
relative = lambda name: (Path("revisions") / revision_id / name).as_posix()
revision = {
"revision_id": revision_id,
"status": "success",
"created_at": now_iso(),
"cdsl_path": relative("model.cdsl.json"),
"step_path": relative("model.step"),
"glb_path": relative("model.glb"),
"report_path": relative("rebuild-report.json"),
"selector_path": relative("model.selector.json"),
"edges_path": relative("model.edges.json"),
"topology_path": relative("model.topology.json"),
"fragment_path": relative("fragment.json"),
"summary": summary or "Autonomous CDSL checkpoint",
"reference_ids": [],
"engine": "cdsl_only",
"parent_revision_id": str(candidate.get("parent_revision_id") or ""),
"branch_id": str(task.get("active_branch_id") or "main"),
"visibility": "checkpoint",
"node_id": "",
"candidate_id": candidate_id,
# Retain the legacy field for old task readers while new clients use
# candidate_review_path to distinguish an independent verdict.
"step_review_path": relative(review_relative.as_posix()),
"candidate_review_path": relative(review_relative.as_posix()),
}
store.update_task(task_id, revision)
store.clear_active_candidate(task_id, candidate_id)
return {"task_id": task_id, **revision}
class AutonomousCdslGenerationRunner:
def __init__(self, settings: Settings, store: WorkspaceStore, complete: Completion) -> None:
self.settings = settings
self.store = store
self.complete = complete
self.tools = autonomous_tools()
def _author_tools(
self,
task: dict[str, Any],
*,
requirements_frozen: bool,
state: dict[str, Any] | None = None,
) -> list[dict[str, Any]]:
"""Expose only actions that can advance the current durable state.
Function-call models tend to choose an advertised tool even when the
natural-language instruction says it is no longer needed. Removing
the immutable writer after its successful first use makes the frozen
document a hard part of the protocol rather than a repeated-choice
trap for the author.
"""
if not requirements_frozen:
# The first durable transition is the frozen requirements
# document. Do not merely describe that ordering in prose: an
# ordinary tool-call model may batch an attractive CAD action in
# the same response, and allowing it would bypass the document.
return [next(tool for tool in self.tools if str((tool.get("function") or {}).get("name") or "") == "write_requirements_document")]
by_name = {
str((tool.get("function") or {}).get("name") or ""): tool
for tool in self.tools
}
if (state or {}).get("completion_checklist_required") and not (state or {}).get("completion_checklist_written"):
return [by_name["write_completion_checklist"]]
completion_state_enforced = any(
key in (state or {})
for key in ("completion_checklist_required", "completion_checklist_written", "completion_ledger")
)
def completion_action_tools() -> list[dict[str, Any]]:
if not completion_state_enforced:
return [by_name["complete_task"]]
ledger = (state or {}).get("completion_ledger") if isinstance((state or {}).get("completion_ledger"), dict) else {}
ready = (
bool(task.get("active_revision"))
and str(ledger.get("verified_revision") or "") == str(task.get("active_revision") or "")
and bool(ledger.get("items"))
and all(str(item.get("status") or "") == "complete" for item in ledger.get("items") or () if isinstance(item, dict))
)
# Coverage is supplied only by the independent candidate reviewer.
# Letting the author write the final audit again would restore the
# self-certification path this protocol removes.
return [by_name["complete_task"]] if ready else []
def repair_observation_tools(*, include_topology: bool = False) -> list[dict[str, Any]]:
"""Return useful, not already exhausted, repair observations.
A model can legitimately inspect several different artifacts
before choosing a correction. Repeating a no-argument inspection
on an unchanged checkpoint, however, only consumes context and
triggers a watchdog. Keep the genuinely parameterized tools
available while removing identical model/render observations and
bound the total observation phase before an action is required.
"""
head = self._head_key(task)
completed = {
str(item)
for item in ((state or {}).get("completed_repair_observations_by_head") or {}).get(head, ())
if str(item)
}
count = int((((state or {}).get("repair_observation_counts_by_head") or {}).get(head)) or 0)
if count >= self.settings.agent_tool_calls_per_cycle:
return []
names = ["inspect_model", "read_cdsl_slice", "measure_model", "render_views", "render_section"]
if include_topology:
names.insert(0, "inspect_topology")
return [
by_name[name]
for name in names
if name not in {"inspect_model", "render_views"} or name not in completed
]
def noop_rejection() -> dict[str, Any] | None:
rejection = (state or {}).get("geometry_rejection")
if not isinstance(rejection, dict):
return None
# States written before the fingerprinted protocol are treated as
# active only on their original head. New states must match the
# actual solid when created; a committed material candidate and a
# rollback both explicitly clear this active marker.
recorded = str(rejection.get("geometry_fingerprint") or "")
if recorded:
return rejection
return rejection if str(rejection.get("working_head") or "") == self._head_key(task) else None
def noop_tools(rejection: dict[str, Any]) -> list[dict[str, Any]]:
fingerprint = str(rejection.get("geometry_fingerprint") or "root")
diagnoses = ((state or {}).get("geometry_diagnoses_by_fingerprint") or {}).get(fingerprint) or {}
observations = diagnoses.get("observations") if isinstance(diagnoses, dict) else {}
observations = observations if isinstance(observations, dict) else {}
conclusion = diagnoses.get("conclusion") if isinstance(diagnoses, dict) else None
if not isinstance(conclusion, dict):
tool_names = [
name for kind, name in (("inspect", "inspect_model"), ("measure", "measure_model"), ("render", "render_views"))
if kind not in observations
]
return [by_name[name] for name in tool_names] + [by_name["record_geometry_conclusion"]]
decision = str(conclusion.get("decision") or "")
if decision == "modify":
return [by_name["submit_cdsl_fragment"], by_name["rollback_checkpoint"]]
if decision == "rollback":
return [by_name["rollback_checkpoint"]]
if decision == "complete":
if self._completion_has_known_incomplete(task, state):
# A current audit already proves that completion is false.
# Keeping only the audit tool here created a self-sustaining
# audit loop: it could never change the model. Reopen the
# conclusion so the author must choose modify or rollback.
diagnoses["conclusion"] = None
rejection["conclusion_required"] = True
rejection["instruction"] = "The current completion audit has unresolved requirements. Completion is unavailable; record a modify or rollback conclusion using the existing evidence."
state["geometry_rejection"] = rejection
self._record(state, "geometry_conclusion_reopened", {
"message": "Completion conclusion was reopened because the current checklist has unresolved items.",
})
return [by_name["record_geometry_conclusion"]]
return completion_action_tools()
return [by_name["record_geometry_conclusion"]]
candidate_id = str(task.get("active_candidate_id") or "")
if candidate_id:
# New candidates are synchronously judged before this method is
# reached again. A leftover staged candidate can only come from a
# pre-review run or an interrupted reviewer call, so the author
# must not be offered a self-review or self-commit escape hatch.
return [by_name["rollback_checkpoint"]]
correction = (state or {}).get("format_correction")
if (
isinstance(correction, dict)
and str(correction.get("working_head") or "") == self._head_key(task)
and str(correction.get("error") or "").startswith("CANDIDATE_ATTEMPT_LIMIT:")
):
# Older runs classified the working-head budget as a CDSL format
# failure. It is a durable-state decision, so discard that stale
# correction card before selecting the next author action.
state["format_correction"] = {}
state["candidate_action_required"] = {
"working_head": self._head_key(task),
"reason": "candidate_attempt_limit",
"instruction": "This working head has no remaining material candidate attempts. Inspect the current model as needed, then choose a rollback checkpoint rather than repeating the same submission.",
}
correction = {}
action_required = (state or {}).get("candidate_action_required")
if isinstance(action_required, dict) and str(action_required.get("working_head") or "") == self._head_key(task):
if str(action_required.get("reason") or "") == "edge_selector_recovery":
return [by_name["inspect_topology"]]
if str(action_required.get("reason") or "") == "edge_selector_recovery_submit":
return [by_name["submit_cdsl_fragment"], by_name["rollback_checkpoint"]]
if str(action_required.get("reason") or "") == "edge_selector_recovery_no_edges":
return [by_name["rollback_checkpoint"]]
if str(action_required.get("reason") or "") == "duplicate_repair_observation":
return [by_name["submit_cdsl_fragment"], by_name["rollback_checkpoint"]]
if str(action_required.get("reason") or "") == "candidate_review_rejected":
# The rejected candidate's render, deterministic report and
# structured reviewer evidence are already in context. More
# generic inspection cannot alter that evidence and was a
# recurring source of no-progress loops.
return [by_name["submit_cdsl_fragment"], by_name["rollback_checkpoint"]]
if str(action_required.get("reason") or "") == "candidate_attempt_limit":
# The build budget is exhausted. More observations cannot
# make a rejected fragment executable; the remaining durable
# choices are to retreat or, if its checklist permits, ask
# for final publication of the current checkpoint.
return [by_name["rollback_checkpoint"], *completion_action_tools()]
if str(action_required.get("reason") or "") == "duplicate_fragment_limit":
return [by_name["rollback_checkpoint"]]
return [*repair_observation_tools(), by_name["rollback_checkpoint"]]
if isinstance(correction, dict) and str(correction.get("working_head") or "") == self._head_key(task):
# The server has already determined the exact local contract.
# Force a direct correction before further observations can bury
# that information in history or spend another retry cycle.
return [by_name["submit_cdsl_fragment"]]
rejection = noop_rejection()
if rejection is not None:
# A no-op rebuild has already spent an engine attempt. Do not let
# the author re-observe arbitrary data or submit another fragment
# until it records a decision from one bounded evidence set.
return noop_tools(rejection)
final_repair = (state or {}).get("final_repair")
if isinstance(final_repair, dict) and str(final_repair.get("working_head") or "") == self._head_key(task):
# A final review found unmet requirements. The author owns the
# geometric decision: it may roll back the faulty ancestor or
# append a corrective feature. A new repair checkpoint makes the
# prior review's exact counts/coordinates stale, so once current
# topology is available it must be free to inspect its CDSL,
# measurements or rendering before deciding. The ordinary
# no-progress budget still bounds purely observational loops.
if not final_repair.get("topology_observed"):
return [by_name[name] for name in ("inspect_topology", "rollback_checkpoint")]
return [*repair_observation_tools(), by_name["get_cdsl_operation_contract"], by_name["submit_cdsl_fragment"], by_name["rollback_checkpoint"]]
# The root head is already described in every author context as an
# empty model. Re-exposing read-only inspection here creates an
# unproductive loop: no observation can reveal more state until the
# first additive feature exists. Force the author to establish that
# base body; later heads regain the full observation tool set.
if not str(task.get("active_revision") or "") and not str(task.get("active_candidate_id") or ""):
return [by_name["submit_cdsl_fragment"]]
events = (state or {}).get("recent_events") or []
last_kind = str((events[-1] or {}).get("kind") or "") if events and isinstance(events[-1], dict) else ""
# A new checkpoint always has a fresh topology snapshot. Resolve that
# snapshot before selecting the next additive operation: it is cheap,
# gives the author the only valid face/edge handles, and prevents it
# from inventing a sketch-cut merely because it lacks a host-face
# token for a hole. More expensive measurements and renders remain
# available after that snapshot has been observed.
if last_kind in {"checkpoint", "rollback", "candidate_discarded", ""}:
return [
by_name[name]
for name in ("inspect_topology",)
] + completion_action_tools()
if last_kind == "inspect_topology":
return [
by_name[name]
for name in ("get_cdsl_operation_contract", "submit_cdsl_fragment", "measure_model", "render_views", "render_section", "rollback_checkpoint")
] + completion_action_tools()
if last_kind in {"measure_model", "render_views", "render_section"}:
return [
by_name[name]
for name in ("get_cdsl_operation_contract", "submit_cdsl_fragment", "rollback_checkpoint")
] + completion_action_tools()
if last_kind == "get_cdsl_operation_contract":
# A contract is requested immediately before authoring. Reopening
# the full observation menu here only spends a model turn without
# improving that pending CDSL decision.
return [by_name[name] for name in ("submit_cdsl_fragment", "rollback_checkpoint")] + completion_action_tools()
if last_kind == "tool_error":
# An invalid fragment or a stale token must not funnel the author
# into completion. Re-expose the current snapshot so it can
# recover concrete evidence before retrying the small feature.
return [
by_name[name]
for name in ("inspect_topology", "get_cdsl_operation_contract", "measure_model", "render_views", "render_section", "submit_cdsl_fragment", "rollback_checkpoint")
] + completion_action_tools()
return [
tool for name, tool in by_name.items()
if name not in {"write_requirements_document", "write_completion_checklist", "complete_task"}
] + completion_action_tools()
def _state(
self,
task_id: str,
*,
request: str,
conversation_id: str,
provider: ProviderConfig,
model: ProviderModel,
initial_messages: list[dict[str, Any]],
frozen_attachment_ids: list[str],
fresh: bool,
) -> dict[str, Any]:
current = self.store.read_agent_state(task_id)
if not fresh and isinstance(current, dict):
current.setdefault("completed_repair_observations_by_head", {})
current.setdefault("repair_observation_counts_by_head", {})
current.setdefault("repair_observation_keys_by_head", {})
task = self.store.read_task(task_id) or {}
checklist_exists = bool(self.store.read_completion_checklist(task_id))
current.setdefault("completion_checklist_required", bool(self.store.read_requirements_document(task_id)) and not checklist_exists)
current.setdefault("completion_checklist_written", checklist_exists)
current.setdefault("completion_ledger", {})
current.setdefault("geometry_diagnoses_by_fingerprint", {})
current.setdefault("rejected_fragment_fingerprints_by_geometry", {})
current.setdefault("duplicate_fragment_rejections_by_geometry", {})
current.setdefault("edge_selector_recovery", {})
current.setdefault("last_candidate_review", {})
final_repair = current.get("final_repair")
if isinstance(final_repair, dict) and str(final_repair.get("working_head") or "") == self._head_key(task):
# Pre-v2.1 state had no provenance for retained final-review
# evidence. It necessarily predates any persisted repair
# checkpoint, so migrate it to advisory evidence requiring
# fresh inspection rather than treating old counts as current.
if not str(final_repair.get("evidence_revision") or ""):
final_repair["evidence_stale_after_checkpoint"] = True
final_repair["latest_checkpoint_revision"] = str(task.get("active_revision") or "")
current["final_repair"] = final_repair
return current
return {
"schema_version": "cad.autonomous-agent-state.v1",
"request": request,
"conversation_id": conversation_id,
"provider_id": provider.id,
"model_id": model.id,
"author_vision": model.vision,
"initial_messages": initial_messages,
"initial_context_delivered": False,
"frozen_attachment_ids": list(dict.fromkeys(item for item in frozen_attachment_ids if item)),
"recent_events": [],
"last_review": {},
"last_candidate_review": {},
"no_progress": 0,
"cycle_tool_calls": 0,
"candidate_attempts_by_head": {},
"format_correction": {},
"geometry_rejection": {},
"geometry_diagnoses_by_fingerprint": {},
"rejected_fragment_fingerprints_by_geometry": {},
"duplicate_fragment_rejections_by_geometry": {},
"candidate_action_required": {},
"edge_selector_recovery": {},
"completion_checklist_required": False,
"completion_checklist_written": False,
"completion_ledger": {},
"completed_repair_observations_by_head": {},
"repair_observation_counts_by_head": {},
"repair_observation_keys_by_head": {},
"pending_images": [],
"last_diagnostic": "",
}
def _save_state(self, task_id: str, state: dict[str, Any]) -> None:
state["updated_at"] = now_iso()
state["recent_events"] = list(state.get("recent_events") or [])[-16:]
state["pending_images"] = list(state.get("pending_images") or [])[-7:]
self.store.set_agent_state(task_id, state)
def _record(self, state: dict[str, Any], kind: str, payload: dict[str, Any]) -> None:
event_payload = {"kind": kind, "at": now_iso(), **payload}
state.setdefault("recent_events", []).append(event_payload)
state["last_diagnostic"] = str(payload.get("message") or payload.get("error") or state.get("last_diagnostic") or "")
def _completion_items(self, task_id: str) -> list[str]:
checklist = self.store.read_completion_checklist(task_id)
return parse_completion_checklist(checklist) if checklist else []
def _completion_ledger(self, task_id: str, state: dict[str, Any]) -> dict[str, Any]:
"""Return a state ledger aligned to the immutable completion list."""
items = self._completion_items(task_id)
existing = state.get("completion_ledger") if isinstance(state.get("completion_ledger"), dict) else {}
prior = {
_checklist_key(str(item.get("item") or "")): item
for item in existing.get("items") or ()
if isinstance(item, dict) and str(item.get("item") or "")
}
values = []
for item in items:
previous = prior.get(_checklist_key(item), {})
values.append({
"item": item,
"status": str(previous.get("status") or "missing"),
"evidence": str(previous.get("evidence") or ""),
"updated_at": str(previous.get("updated_at") or ""),
})
ledger = {
"items": values,
"verified_revision": str(existing.get("verified_revision") or ""),
"audited_at": str(existing.get("audited_at") or ""),
}
state["completion_ledger"] = ledger
return ledger
def _completion_context(self, task_id: str, task: dict[str, Any], state: dict[str, Any]) -> dict[str, Any] | None:
checklist = self.store.read_completion_checklist(task_id)
if not checklist:
return None
ledger = self._completion_ledger(task_id, state)
active_revision = str(task.get("active_revision") or "")
return {
"checklist_markdown": checklist,
"verified_for_current_checkpoint": bool(active_revision and ledger.get("verified_revision") == active_revision),
"items": [
{"item": item["item"], "status": item["status"], "evidence": item["evidence"]}
for item in ledger["items"]
],
}
def _invalidate_completion_audit(self, task_id: str, state: dict[str, Any], *, reason: str) -> None:
if not self.store.read_completion_checklist(task_id):
return
ledger = self._completion_ledger(task_id, state)
ledger["verified_revision"] = ""
ledger["audited_at"] = ""
state["completion_ledger"] = ledger
self._record(state, "completion_audit_stale", {
"message": "Completion checklist must be re-audited for the current checkpoint.",
"reason": reason,
})
def _completion_gate_error(self, task_id: str, task: dict[str, Any], state: dict[str, Any]) -> str | None:
checklist = self.store.read_completion_checklist(task_id)
if not checklist:
return "COMPLETION_CHECKLIST_REQUIRED: write_completion_checklist must be completed before final publication"
ledger = self._completion_ledger(task_id, state)
active_revision = str(task.get("active_revision") or "")
if not active_revision or ledger.get("verified_revision") != active_revision:
return "INDEPENDENT_REVIEW_REQUIRED: the current checkpoint has no matching independent reviewer ledger"
incomplete = [str(item.get("item") or "") for item in ledger.get("items") or () if str(item.get("status") or "") != "complete"]
if incomplete:
return "COMPLETION_CHECKLIST_INCOMPLETE: unresolved items: " + "; ".join(incomplete)
return None
def _completion_has_known_incomplete(self, task: dict[str, Any], state: dict[str, Any]) -> bool:
"""Whether a current audit explicitly rules out final publication.
A missing or stale audit remains an evidence-gathering case. Only an
audit for the active revision with actual unresolved items invalidates
a prior no-op ``complete`` conclusion.
"""
ledger = state.get("completion_ledger") if isinstance(state.get("completion_ledger"), dict) else {}
active_revision = str(task.get("active_revision") or "")
if not active_revision or str(ledger.get("verified_revision") or "") != active_revision:
return False
items = [item for item in ledger.get("items") or () if isinstance(item, dict)]
return bool(items) and any(str(item.get("status") or "") != "complete" for item in items)
@staticmethod
def _author_events(state: dict[str, Any]) -> list[dict[str, Any]]:
"""Keep only the newest detailed evidence in the next author turn.
Tool results are durable audit artifacts, not conversation history.
Repeating an old 64-record measurement or selector bank consumes input
tokens without changing the next CAD decision. The current model and
completion ledger remain separately present, and any older artifact
can be requested again through its dedicated read tool.
"""
private_keys = {"candidate_id", "revision_id", "branch_id", "path", "working_head"}
entries = [raw for raw in state.get("recent_events") or () if isinstance(raw, dict)]
detailed_indexes = [
index for index, raw in enumerate(entries)
if isinstance(raw.get("result"), dict) or raw.get("kind") in {"read_cdsl_slice", "get_cdsl_operation_contract"}
]
newest_detailed = detailed_indexes[-1] if detailed_indexes else -1
result: list[dict[str, Any]] = []
for index, raw in enumerate(entries):
sanitized = {key: deepcopy(value) for key, value in raw.items() if key not in private_keys}
if index != newest_detailed and "result" in sanitized:
sanitized.pop("result", None)
sanitized["message"] = str(sanitized.get("message") or "Earlier tool evidence is archived; request it again only if needed.")
result.append(sanitized)
return result
def _prompt_messages(self, task_id: str, state: dict[str, Any], engine: Any) -> list[dict[str, Any]]:
task = self.store.read_task(task_id) or {}
requirements = self.store.read_requirements_document(task_id)
source_requirements = self.store.read_source_requirements(task_id)
model_summary = self._inspect_model_payload(task_id, task)
current_fingerprint = self._active_geometry_fingerprint(task_id, task)
diagnosis = ((state.get("geometry_diagnoses_by_fingerprint") or {}).get(current_fingerprint) or {})
observations = diagnosis.get("observations") if isinstance(diagnosis, dict) else {}
context = {
"source_requirements_markdown": source_requirements or "No source-requirements.md artifact is available.",
"requirements_markdown": requirements or "requirements.md has not been written yet.",
"completion_coverage": self._completion_context(task_id, task, state),
"current_model": model_summary,
"last_step_review": {
key: value
for key, value in (state.get("last_review") or {}).items()
if key not in {"candidate_id", "path"}
},
"last_candidate_review": state.get("last_candidate_review") or None,
"recent_tool_results": self._author_events(state),
"format_correction": state.get("format_correction") or None,
"geometry_rejection": state.get("geometry_rejection") or None,
"geometry_diagnosis": {
"geometry_fingerprint": current_fingerprint[:12],
"available_evidence_refs": [
str(item.get("ref") or "") for item in (observations or {}).values()
if isinstance(item, dict) and str(item.get("ref") or "")
],
"conclusion": diagnosis.get("conclusion") if isinstance(diagnosis, dict) else None,
},
"candidate_action_required": state.get("candidate_action_required") or None,
# This survives event trimming. It is the only token bank a
# finish-recovery fragment may reuse.
"edge_selector_recovery": state.get("edge_selector_recovery") or None,
"final_repair": state.get("final_repair") or None,
"runtime_operations": _runtime_summary(engine),
# A compact, always-present protocol. Exact operation fields are
# provided on demand by get_cdsl_operation_contract, avoiding a
# full tutorial for holes, revolves and edge finishes every turn.
"cdsl_authoring_basics": {
"fragment_shape": {"sketch": "optional {workplane, profile}", "feature": "{atomic_id, params, optional selector_tokens}"},
"workplane": {
"origin_mm": "[x,y,z]",
"x_dir": "[x,y,z]",
"normal": "[x,y,z]",
"local_mapping": "A profile point [u,v] maps to origin_mm + u*x_dir + v*(normal cross x_dir). Calculate this basis before choosing signs; do not assume local v is global +Z.",
},
"profiles": {"supported_types": ["circle", "polygon", "analytic_contours"], "polygon": "ordered [u,v] vertices", "circle": "radius_mm plus optional center"},
"rules": [
"Do not supply ids, dependencies, sketch_id, raw selectors, named planes, or unknown fields.",
"Use a workplane object with origin_mm, x_dir and normal for every sketch. A profile point [u,v] maps to origin_mm + u*x_dir + v*(normal cross x_dir); calculate the basis before choosing signs.",
"Use selector_tokens only from the current inspect_topology result.",
"Before every atomic operation that is not already in the current feature list, call get_cdsl_operation_contract for that exact atomic_id. Never infer one operation's params from a similarly named operation.",
"Every revolve_add or revolve_cut needs an author-defined axis: params.axis={origin_mm:[x,y,z],direction:[dx,dy,dz]}. For a same-axis batch, put that object once in top-level revolve_axis; never request or use an axis selector token.",
"For an X-Z tooth profile extruded across a Y-wide bar, verify the basis explicitly: origin [0,9,0], x_dir [1,0,0], normal [0,-1,0] makes local v point to global +Z and a positive blind extrusion travel toward -Y. This is a coordinate example only; choose dimensions from the frozen requirements.",
"One hole feature may include multiple positions when they share one host face, diameter and depth.",
],
},
}
if not task.get("active_revision") and not task.get("active_candidate_id") and requirements:
# This is protocol documentation for the LLM, not a geometry
# template or server-side lowering rule. The author still selects
# every workplane, profile and dimension from requirements.md.
context["root_fragment_reference"] = {
"instruction": "The model is empty. Create its first solid now with one sketch and extrude_add_blind. submit_cdsl_fragment takes one JSON string containing this fragment object; do not wrap it in a complete CDSL document.",
"fragment_shape": {
"sketch": {
"workplane": {
"origin_mm": [0, 0, 0],
"x_dir": [1, 0, 0],
"normal": [0, 0, 1],
},
"profile": {"type": "polygon", "vertices": [[-20, -10], [20, -10], [20, 10], [-20, 10]]},
},
"feature": {"atomic_id": "extrude_add_blind", "params": {"distance_mm": 8}},
},
"rules": [
"This example is format-only: replace all coordinates and dimensions with values from requirements.md.",
"Use a workplane with origin_mm, x_dir and normal, each a three-number array.",
"For a rectangular base, use profile.type polygon and ordered [u, v] vertices.",
"Do not supply id, depends_on, sketch_id, selectors or selector_tokens for the base feature.",
"Create only the base solid in this first fragment. Add cuts, holes, rounds and chamfers in later fragments after inspecting the built checkpoint.",
],
}
encoded = json.dumps(context, ensure_ascii=False)
limit = self.settings.agent_context_char_limit
if len(encoded) > limit:
# The frozen requirements are the author contract and must remain
# verbatim. Spend the context budget by dropping recreatable tool
# history first, then reduce capability prose to operation names.
context["recent_tool_results"] = list(context["recent_tool_results"])[-3:]
encoded = json.dumps(context, ensure_ascii=False)
if len(encoded) > limit:
context["recent_tool_results"] = []
context["runtime_operations"] = [
{"atomic_id": item["atomic_id"], "requires_sketch": item["requires_sketch"]}
for item in context["runtime_operations"]
]
encoded = json.dumps(context, ensure_ascii=False)
requirements_frozen = bool(requirements)
requirements_protocol = (
"Your first and only action must be write_requirements_document. "
"Write the complete requirements.md before any observation or modelling tool. It must preserve every source requirement; it may only add clarifying assumptions, never remove, replace, or weaken source intent. "
if not requirements_frozen else
"requirements.md is frozen. Its writer is no longer available: do not attempt to rewrite or amend it. Reread both supplied contracts every turn; frozen requirements may add detail but can never override source requirements. "
)
system = (
"You are an autonomous CDSL CAD author. "
+ requirements_protocol
+ (
"Now write_completion_checklist: a short frozen Markdown list of the independently observable completion claims from requirements.md. "
"Use one unchecked '- [ ] claim' per line before modelling. "
if requirements_frozen and state.get("completion_checklist_required") and not state.get("completion_checklist_written") else
"Use completion_coverage from the independent reviewer as the running missing-work list. Publication is blocked unless its ledger marks every frozen item complete at the current checkpoint. "
)
+ "Iteratively make a correct model. Do not ask the user for missing dimensions; choose engineering assumptions and record them in requirements.md before it is frozen. "
f"Choose the next coherent batch yourself: each submit_cdsl_fragment may contain 1..{self.settings.agent_max_features_per_fragment} ordered features and must state its batch_goal. "
"Keep topology-sensitive operations in a later batch unless their selector tokens came from the current checkpoint. Observe and measure before topology-sensitive operations. "
"submit_cdsl_fragment accepts normal JSON text, not a strict provider schema. The server validates the resulting complete CDSL and runs a full CDSL-only rebuild. "
"Never put CDSL object IDs, dependencies, sketch_id, revision IDs, hashes or raw selectors in a fragment. Use only opaque selector tokens from inspect_topology and opaque checkpoint tokens from inspect_model for rollback. "
"When edge_selector_recovery is present, copy one or more token strings from its tokens list exactly. Do not invent token names, inspect topology again, or omit selector_tokens; the only alternative is rollback_checkpoint. "
"The server exposes only tools that can advance the current state. After every checkpoint, obtain current topology before selecting the next feature. Reuse only the opaque tokens from that immediate result. "
"Every rebuilt batch is rendered and independently reviewed before any checkpoint is created. You cannot approve your own candidate: an accepted review commits it, while a rejected review preserves evidence but leaves the working checkpoint unchanged. You may rollback any ancestor when evidence shows the current approach is wrong. "
"Do not claim completion in prose: call complete_task only after checking every requirement. Keep visible prose short and do not expose raw CDSL or selector identifiers."
" When final_repair is present, it is authoritative that the model is incomplete or wrong, so do not call complete_task. Its reported evidence may predate later repair checkpoints: reread current model/CDSL/measurements before relying on exact counts or coordinates. If a submitted candidate is rejected because the solid did not change, or independent coverage reports unresolved requirements, collect at most one inspect_model, one measure_model, and one render_views result for that unchanged geometry. Then call record_geometry_conclusion citing returned evidence_refs. Until that conclusion, submit_cdsl_fragment, rollback_checkpoint, and complete_task are unavailable. After it, follow the chosen decision and do not repeat an already rejected fragment."
)
messages: list[dict[str, Any]] = [{"role": "system", "content": system}]
if not state.get("initial_context_delivered"):
messages.extend(item for item in state.get("initial_messages") or [] if isinstance(item, dict))
messages.append({"role": "user", "content": encoded})
images = [Path(item) for item in state.get("pending_images") or []]
valid_images = [path for path in images if path.is_file()]
if valid_images and state.get("author_vision") is True:
content: list[dict[str, Any]] = [{"type": "text", "text": "Requested render evidence for this decision:"}]
for image in valid_images[:7]:
media = "image/jpeg" if image.suffix.lower() in {".jpg", ".jpeg"} else "image/png"
content.append({"type": "image_url", "image_url": {"url": f"data:{media};base64,{base64.b64encode(image.read_bytes()).decode('ascii')}"}})
messages.append({"role": "user", "content": content})
state["pending_images"] = []
elif valid_images:
# Rendering remains available for audit, but an author model that
# does not declare vision must not receive image content that its
# provider cannot process.
state["pending_images"] = []
messages.append({
"role": "user",
"content": "Render evidence was created but this author model is text-only. Use inspect_model, measure_model, or inspect_topology for deterministic evidence.",
})
return messages
def _artifact_data(self, task_id: str, task: dict[str, Any]) -> tuple[dict[str, Any] | None, dict[str, Any] | None, dict[str, Any] | None, str]:
cdsl_path, topology_path, _, identifier = _model_paths(self.store, task_id, task)
cdsl = read_json(cdsl_path) if cdsl_path and cdsl_path.is_file() else None
topology = read_json(topology_path) if topology_path and topology_path.is_file() else None
report_path = cdsl_path.parent / "rebuild-report.json" if cdsl_path else None
report = read_json(report_path) if report_path and report_path.is_file() else None
return cdsl if isinstance(cdsl, dict) else None, topology if isinstance(topology, dict) else None, report if isinstance(report, dict) else None, identifier
def _inspect_model_payload(self, task_id: str, task: dict[str, Any]) -> dict[str, Any]:
cdsl, topology, report, identifier = self._artifact_data(task_id, task)
if cdsl is None:
return {"working_head": "root", "bbox_mm": None, "volume": None, "solid_count": 0, "sketches": [], "features": [], "checkpoints": [{"token": "root", "current": True}]}
engine_result = report.get("engine_result") if isinstance(report, dict) and isinstance(report.get("engine_result"), dict) else {}
geometry = cdsl.get("geometry") if isinstance(cdsl.get("geometry"), dict) else {}
summaries = []
for feature in cdsl.get("features") or []:
if isinstance(feature, dict):
summaries.append({"id": feature.get("id"), "atomic_id": feature.get("atomic_id"), "name": feature.get("name") or "", "depends_on": feature.get("depends_on") or []})
task_revisions = task.get("revisions") or []
active_revision = str(task.get("active_revision") or "")
branch_id = str(task.get("active_branch_id") or "main")
return {
"working_head": "staged_candidate" if identifier.startswith("candidate_") else _checkpoint_token(branch_id, active_revision),
"active_candidate": bool(task.get("active_candidate_id")),
"bbox_mm": engine_result.get("bbox_mm"),
"volume": engine_result.get("volume_mm3"),
"solid_count": engine_result.get("solid_count"),
"sketches": [{"id": item.get("id"), "profile_type": (item.get("profile") or {}).get("type")} for item in geometry.get("sketches") or [] if isinstance(item, dict)],
"features": summaries,
"topology_record_count": len((topology or {}).get("records") or []),
"checkpoints": [
{"token": token, "current": revision_id == active_revision}
for token, revision_id in _rollback_tokens(task).items()
],
}
def _head_key(self, task: dict[str, Any]) -> str:
return f"{str(task.get('active_branch_id') or 'main')}:{str(task.get('active_revision') or 'root')}"
def _active_geometry_fingerprint(self, task_id: str, task: dict[str, Any]) -> str:
_, topology, _, _ = self._artifact_data(task_id, task)
return _geometry_fingerprint(topology) if isinstance(topology, dict) else "root"
def _diagnosis_state(self, state: dict[str, Any], fingerprint: str) -> dict[str, Any]:
diagnoses = state.setdefault("geometry_diagnoses_by_fingerprint", {})
current = diagnoses.setdefault(fingerprint, {"observations": {}, "conclusion": None})
if not isinstance(current, dict):
current = {"observations": {}, "conclusion": None}
diagnoses[fingerprint] = current
if not isinstance(current.get("observations"), dict):
current["observations"] = {}
return current
def _active_noop_rejection(self, task_id: str, task: dict[str, Any], state: dict[str, Any]) -> dict[str, Any] | None:
rejection = state.get("geometry_rejection")
if not isinstance(rejection, dict):
return None
fingerprint = self._active_geometry_fingerprint(task_id, task)
recorded = str(rejection.get("geometry_fingerprint") or "")
if recorded:
return rejection if recorded == fingerprint else None
return rejection if str(rejection.get("working_head") or "") == self._head_key(task) else None
def _record_geometry_diagnostic(
self,
task_id: str,
task: dict[str, Any],
state: dict[str, Any],
kind: str,
) -> str | None:
rejection = self._active_noop_rejection(task_id, task, state)
if rejection is None:
return None
fingerprint = str(rejection.get("geometry_fingerprint") or self._active_geometry_fingerprint(task_id, task))
diagnosis = self._diagnosis_state(state, fingerprint)
observations = diagnosis["observations"]
if kind in observations:
raise AutonomousGenerationError(f"GEOMETRY_DIAGNOSTIC_ALREADY_CONSUMED: {kind} was already collected for this unchanged geometry")
evidence_ref = f"diag_{fingerprint[:12]}_{kind}"
observations[kind] = {"ref": evidence_ref, "at": now_iso()}
self._record(state, "geometry_diagnostic", {
"message": f"Collected {kind} evidence for the unchanged geometry.",
"kind": kind,
"evidence_ref": evidence_ref,
"geometry_fingerprint": fingerprint[:12],
})
return evidence_ref
def _record_repair_observation(
self,
task: dict[str, Any],
state: dict[str, Any],
name: str,
arguments: dict[str, Any],
) -> bool:
"""Track a bounded sequence of fresh observations for a repair head."""
final_repair = state.get("final_repair")
head = self._head_key(task)
if not isinstance(final_repair, dict) or str(final_repair.get("working_head") or "") != head:
return False
try:
encoded_arguments = json.dumps(arguments, ensure_ascii=True, sort_keys=True, separators=(",", ":"))
except (TypeError, ValueError):
encoded_arguments = repr(arguments)
observation_key = f"{name}:{encoded_arguments}"
keys_by_head = state.setdefault("repair_observation_keys_by_head", {})
prior_keys = {str(item) for item in keys_by_head.get(head, ()) if str(item)}
if observation_key in prior_keys:
state["candidate_action_required"] = {
"working_head": head,
"reason": "duplicate_repair_observation",
"instruction": "This exact observation was already returned for the unchanged checkpoint. Do not request it again; submit a materially different corrective fragment or rollback to a strict ancestor.",
}
self._record(state, "duplicate_repair_observation", {
"message": "The same repair observation was already returned; choose a corrective fragment or rollback.",
"tool": name,
})
return False
prior_keys.add(observation_key)
keys_by_head[head] = sorted(prior_keys)
counts = state.setdefault("repair_observation_counts_by_head", {})
counts[head] = int(counts.get(head) or 0) + 1
if name in {"inspect_model", "render_views"}:
completed = state.setdefault("completed_repair_observations_by_head", {})
names = {str(item) for item in completed.get(head, ()) if str(item)}
names.add(name)
completed[head] = sorted(names)
self._record(state, "repair_observation", {
"message": "Author obtained fresh repair evidence from the current checkpoint.",
"tool": name,
"count": counts[head],
"limit": self.settings.agent_tool_calls_per_cycle,
})
return True
def _candidate_result_payload(self, task_id: str, candidate: dict[str, Any], *, status: str, message: str = "") -> dict[str, Any]:
return {
"taskId": task_id,
"candidateId": candidate.get("candidate_id") or "",
"status": status,
"message": message,
"featureIds": candidate.get("feature_ids") or [],
"health": candidate.get("health") or {},
"compatibilityFixes": candidate.get("compatibility_fixes") or [],
}
def _apply_candidate_coverage(
self,
task_id: str,
state: dict[str, Any],
review: dict[str, Any],
revision_id: str,
) -> None:
"""Persist only independent-review coverage for the accepted checkpoint."""
coverage = review.get("coverage") if isinstance(review.get("coverage"), list) else []
state["completion_ledger"] = {
"items": [
{
"item": str(item.get("item") or ""),
"status": str(item.get("status") or "uncertain"),
"evidence": str(item.get("evidence") or ""),
"updated_at": now_iso(),
}
for item in coverage if isinstance(item, dict)
],
"verified_revision": revision_id,
"audited_at": now_iso(),
}
async def _review_staged_candidate(
self,
task_id: str,
task: dict[str, Any],
state: dict[str, Any],
candidate: dict[str, Any],
batch_goal: str,
) -> dict[str, Any]:
candidate_id = str(candidate.get("candidate_id") or "")
candidate_dir = self.store.candidate_dir(task_id, candidate_id)
cdsl = read_json(candidate_dir / "model.cdsl.json", {})
report = read_json(candidate_dir / "rebuild-report.json", {})
topology = read_json(candidate_dir / "model.topology.json", {})
if not isinstance(cdsl, dict) or not isinstance(report, dict) or not isinstance(topology, dict):
raise AutonomousGenerationError("Candidate review artifacts are incomplete")
review_dir = candidate_dir / "reviews" / "candidate-review"
try:
manifest = await asyncio.to_thread(
render_checkpoint,
self.settings,
step_path=candidate_dir / "model.step",
output_dir=review_dir,
)
except Exception as error:
stored = read_json(candidate_dir / "candidate.json", candidate)
if isinstance(stored, dict):
stored.update({
"status": "review_failed",
"review_failed_at": now_iso(),
"review_error": str(error),
})
write_json(candidate_dir / "candidate.json", stored)
self.store.clear_active_candidate(task_id, candidate_id)
raise AutonomousGenerationError(f"CANDIDATE_REVIEW_FAILED: {error}") from error
geometry = cdsl.get("geometry") if isinstance(cdsl.get("geometry"), dict) else {}
engine_result = report.get("engine_result") if isinstance(report.get("engine_result"), dict) else {}
try:
review = await review_candidate_batch(
self.settings,
manifest=manifest,
requirements=self.store.read_requirements_document(task_id),
source_requirements=self.store.read_source_requirements(task_id),
checklist=self._completion_items(task_id),
batch_goal=batch_goal,
node_id=candidate_id,
deterministic_report={
"health": candidate.get("health") or {},
"engine_result": {
"bbox_mm": engine_result.get("bbox_mm"),
"volume_mm3": engine_result.get("volume_mm3"),
"solid_count": engine_result.get("solid_count"),
},
"features": [
{"atomic_id": item.get("atomic_id"), "params": item.get("params"), "sketch_id": item.get("sketch_id")}
for item in cdsl.get("features") or () if isinstance(item, dict)
],
"topology": [
{"kind": item.get("kind"), "geometry": item.get("geometry"), "feature_id": item.get("feature_id")}
for item in (topology.get("records") or ())[:160] if isinstance(item, dict)
],
},
)
except Exception as error:
stored = read_json(candidate_dir / "candidate.json", candidate)
if isinstance(stored, dict):
stored.update({
"status": "review_failed",
"review_failed_at": now_iso(),
"review_error": str(error),
})
write_json(candidate_dir / "candidate.json", stored)
self.store.clear_active_candidate(task_id, candidate_id)
raise AutonomousGenerationError(f"CANDIDATE_REVIEW_FAILED: {error}") from error
write_json(review_dir / "candidate-review.json", review)
candidate_path = candidate_dir / "candidate.json"
stored = read_json(candidate_path, candidate)
if isinstance(stored, dict):
stored["candidate_review_path"] = (Path("candidates") / candidate_id / "reviews" / "candidate-review" / "candidate-review.json").as_posix()
stored["candidate_review_verdict"] = review["verdict"]
write_json(candidate_path, stored)
return review
def _result_payload(self, built: dict[str, Any], lifecycle: str, checkpoint: bool) -> dict[str, Any]:
return {
"taskId": built["task_id"], "revisionId": built["revision_id"], "cdslPath": built["cdsl_path"], "stepPath": built["step_path"], "glbPath": built["glb_path"], "reportPath": built["report_path"],
"summary": built.get("summary") or "Autonomous CDSL checkpoint", "referenceIds": [], "engine": "cdsl_only",
"checkpoint": checkpoint, "lifecycle": lifecycle,
}
async def _commit_reviewed_candidate(
self,
task_id: str,
task: dict[str, Any],
state: dict[str, Any],
candidate_id: str,
candidate_review: dict[str, Any] | None = None,
) -> tuple[dict[str, Any], list[tuple[str, dict[str, Any]]]]:
"""Promote an already reviewed candidate without another author turn."""
previous_head = self._head_key(task)
built = await asyncio.to_thread(
commit_candidate,
store=self.store,
task_id=task_id,
candidate_id=candidate_id,
summary="Autonomous CDSL checkpoint",
)
state["candidate_action_required"] = {}
self._invalidate_completion_audit(task_id, state, reason="checkpoint_changed")
if isinstance(candidate_review, dict):
self._apply_candidate_coverage(task_id, state, candidate_review, str(built["revision_id"]))
final_repair = state.get("final_repair")
if isinstance(final_repair, dict) and str(final_repair.get("working_head") or "") == previous_head:
final_repair["working_head"] = f"{str(built.get('branch_id') or task.get('active_branch_id') or 'main')}:{str(built.get('revision_id') or '')}"
final_repair["topology_observed"] = False
final_repair["evidence_stale_after_checkpoint"] = True
final_repair["latest_checkpoint_revision"] = str(built.get("revision_id") or "")
state["final_repair"] = final_repair
self._record(state, "checkpoint", {"message": "Reviewed candidate committed as checkpoint"})
events = [
("checkpoint", {"taskId": task_id, "revisionId": built["revision_id"], "status": "success"}),
("cad_result", self._result_payload(built, "running", True)),
]
return built, events
async def _final_review(self, task_id: str, state: dict[str, Any], self_review: str) -> tuple[dict[str, Any], dict[str, Any]]:
task = self.store.read_task(task_id) or {}
active_revision = str(task.get("active_revision") or "")
if not active_revision:
raise AutonomousGenerationError("Cannot complete an empty model")
cdsl_path = self.store.current_cdsl_path(task_id)
if cdsl_path is None:
raise AutonomousGenerationError("The active checkpoint has no CDSL artifact")
cdsl = read_json(cdsl_path)
if not isinstance(cdsl, dict):
raise AutonomousGenerationError("The active CDSL artifact is invalid")
engine = load_engine(self.settings)
final_dir = self.store.revision_dir(task_id, active_revision) / "final-validation"
final_dir.mkdir(parents=True, exist_ok=True)
step_path = final_dir / "model.step"
cdsl_copy = deepcopy(cdsl)
cdsl_copy["part_id"] = task_id
validate_cdsl(cdsl_copy, engine)
engine_result = await asyncio.to_thread(engine.run_cdsl_only, cdsl_copy, step_path)
glb_path = final_dir / "model.glb"
preview = await asyncio.to_thread(step_to_glb, step_path, glb_path)
health = _candidate_health(engine_result, step_path, glb_path)
topology_path = self.store.revision_dir(task_id, active_revision) / "model.topology.json"
topology = read_json(topology_path, {}) if topology_path.is_file() else {}
geometry = cdsl.get("geometry") if isinstance(cdsl.get("geometry"), dict) else {}
# Evidence is descriptive only. It never compiles requirements into
# geometry, but prevents the reviewer from asserting features that do
# not exist in the actual final CDSL/topology.
deterministic_evidence = {
"features": [
{"atomic_id": item.get("atomic_id"), "params": item.get("params"), "sketch_id": item.get("sketch_id")}
for item in cdsl.get("features") or () if isinstance(item, dict)
],
"sketches": [
{"profile": item.get("profile"), "workplane": item.get("workplane")}
for item in geometry.get("sketches") or () if isinstance(item, dict)
],
"topology": [
{"kind": record.get("kind"), "geometry": record.get("geometry"), "feature_id": record.get("feature_id")}
for record in (topology.get("records") or ())[:160] if isinstance(record, dict)
],
}
render_dir = self.store.revision_dir(task_id, active_revision) / "review"
manifest = await asyncio.to_thread(render_checkpoint, self.settings, step_path=step_path, output_dir=render_dir)
source_images: list[Path] = []
conversation_id = str(state.get("conversation_id") or "")
conversation = self.store.read_conversation(conversation_id) if conversation_id else None
allowed_attachment_ids = {str(item) for item in state.get("frozen_attachment_ids") or () if str(item)}
for attachment in (conversation or {}).get("attachments") or ():
if not isinstance(attachment, dict) or str(attachment.get("id") or "") not in allowed_attachment_ids:
continue
if str(attachment.get("kind") or "") != "image":
continue
try:
path = self.store.conversation_attachment_path(conversation_id, str(attachment.get("path") or ""))
except ValueError:
continue
if path.is_file():
source_images.append(path)
review = await review_checkpoint(
self.settings,
manifest=manifest,
requirements=self.store.read_requirements_document(task_id),
source_requirements=self.store.read_source_requirements(task_id),
node_id="final",
deterministic_report={
"self_review": self_review,
"health": health,
"engine": "cdsl_only",
"preview": preview,
"final_model_evidence": deterministic_evidence,
},
source_images=source_images,
final_checkpoint=True,
)
write_json(render_dir / "visual-review.json", review)
self.store.update_revision_metadata(task_id, active_revision, {
"render_manifest_path": (Path("revisions") / active_revision / "review" / "render-manifest.json").as_posix(),
"visual_review_path": (Path("revisions") / active_revision / "review" / "visual-review.json").as_posix(),
"final_validation_path": (Path("revisions") / active_revision / "final-validation" / "rebuild-report.json").as_posix(),
})
write_json(final_dir / "rebuild-report.json", {"engine_result": engine_result, "preview": preview, "health": health, "validated_at": now_iso()})
return review, health
async def _call_author(
self,
messages: list[dict[str, Any]],
tools: list[dict[str, Any]],
provider: ProviderConfig,
model: ProviderModel,
forced_tool: str | None,
) -> dict[str, Any]:
"""Call the author while preserving the normal function-call contract."""
return await self.complete(messages, tools, provider, model, forced_tool)
def _quota_fallback_author(
self,
provider: ProviderConfig,
model: ProviderModel,
) -> tuple[ProviderConfig, ProviderModel] | None:
"""Use the configured default only when it is a different author."""
fallback_provider, fallback_model = self.settings.resolve_model(None, None)
if fallback_provider.id == provider.id and fallback_model.id == model.id:
return None
return fallback_provider, fallback_model
def _transport_fallback_author(
self,
provider: ProviderConfig,
state: dict[str, Any],
) -> tuple[ProviderConfig, ProviderModel] | None:
"""Select one different configured author after a connection outage."""
tried = {str(item) for item in state.get("author_transport_failed_providers") or () if str(item)}
tried.add(provider.id)
for candidate in self.settings.providers:
if candidate.id in tried or not candidate.configured or not candidate.models:
continue
return candidate, candidate.models[0]
return None
async def run(
self,
*,
task_id: str,
request: str,
conversation_id: str,
provider: ProviderConfig,
model: ProviderModel,
initial_messages: list[dict[str, Any]],
frozen_attachment_ids: list[str] | None = None,
already_started: bool = False,
) -> AsyncIterator[tuple[str, dict[str, Any]]]:
if not already_started:
self.store.start_generation(task_id, request=request)
state = self._state(
task_id,
request=request,
conversation_id=conversation_id,
provider=provider,
model=model,
initial_messages=initial_messages,
frozen_attachment_ids=frozen_attachment_ids or [],
fresh=False,
)
engine = load_engine(self.settings)
requirements = self.store.read_requirements_document(task_id)
if requirements:
yield "requirements_document", {"taskId": task_id, "status": "frozen", "path": "requirements.md"}
self._save_state(task_id, state)
try:
while True:
task = self.store.read_task(task_id) or {}
if task.get("lifecycle") != "running":
return
# A cycle is one model response and all tool calls it carries.
# The persistent no-progress counter separately limits an
# endless sequence of otherwise valid observation cycles.
state["cycle_tool_calls"] = 0
requirements_frozen = bool(self.store.read_requirements_document(task_id))
available_tools = self._author_tools(task, requirements_frozen=requirements_frozen, state=state)
# State-machine transitions such as a staged-candidate review
# have exactly one legal action. Force that action instead of
# relying on an OpenAI-compatible provider to infer it from an
# omitted tool list; some providers otherwise hallucinate a
# tool used in an earlier turn.
force = None
if len(available_tools) == 1:
force = str((available_tools[0].get("function") or {}).get("name") or "") or None
messages = self._prompt_messages(task_id, state, engine)
try:
response = await self._call_author(
messages,
available_tools,
provider,
model,
force,
)
except RuntimeError as error:
if _is_author_quota_error(error):
fallback = self._quota_fallback_author(provider, model)
if fallback is None:
raise
previous = f"{provider.id}/{model.id}"
provider, model = fallback
state.update({"provider_id": provider.id, "model_id": model.id, "author_vision": model.vision})
message = f"Author quota exhausted for {previous}; continuing with {provider.id}/{model.id}."
self._record(state, "author_failover", {"message": message})
self._save_state(task_id, state)
yield "agent_thinking", {"taskId": task_id, "message": message}
continue
if _is_author_transport_error(error):
# A staging candidate may already be fully rebuilt and
# simply be waiting for its mandated review. Preserve
# that state and retry the author request instead of
# turning a transient network outage into a CAD loss.
fallback = self._transport_fallback_author(provider, state)
failed = state.setdefault("author_transport_failed_providers", [])
if provider.id not in failed:
failed.append(provider.id)
if fallback is not None:
previous = f"{provider.id}/{model.id}"
provider, model = fallback
state.update({"provider_id": provider.id, "model_id": model.id, "author_vision": model.vision})
message = f"Author connection failed for {previous}; continuing with {provider.id}/{model.id}."
else:
message = "Author connection failed; retaining the current CAD state and retrying shortly."
self._record(state, "author_transport_retry", {"message": message, "diagnostic": str(error)})
self._save_state(task_id, state)
yield "agent_thinking", {"taskId": task_id, "message": message}
await asyncio.sleep(3)
continue
raise
state["initial_context_delivered"] = True
# The initial request and attachments belong only to the
# requirements-authoring turn. Keep durable state compact;
# later turns use the frozen document and structured evidence.
state["initial_messages"] = []
choice = ((response.get("choices") or [{}])[0] or {}).get("message") or {}
content = str(choice.get("content") or "").strip()
if content:
safe_text = content[:1200]
self._record(state, "author_text", {"message": safe_text})
yield "agent_thinking", {"taskId": task_id, "message": safe_text}
calls = choice.get("tool_calls") or []
if not isinstance(calls, list) or not calls:
state["no_progress"] = int(state.get("no_progress") or 0) + 1
self._record(state, "no_tool", {"message": "Author returned no tool call; continue with an observation or action."})
self._save_state(task_id, state)
if int(state["no_progress"]) >= self.settings.agent_consecutive_no_progress_limit:
raise AutonomousGenerationError("NO_PROGRESS_LIMIT: author repeatedly returned no actionable tool call")
continue
# State-specific tool exposure is calculated before this
# response. Executing several calls from one completion would
# let an author issue the same expensive observation (notably
# multiple OCC section renders) after that state has already
# changed. One action per author turn keeps the next call's
# tool list and evidence truthful while avoiding wasted image
# tokens and no-progress budget.
allowed_names = {
str((tool.get("function") or {}).get("name") or "")
for tool in available_tools
if isinstance(tool, dict)
}
selected_call = _preferred_tool_call(calls, allowed_names=allowed_names)
if len(calls) > 1:
selected_function = selected_call.get("function") if isinstance(selected_call.get("function"), dict) else {}
self._record(state, "extra_tool_calls_deferred", {
"message": f"Processed the highest-priority action ({selected_function.get('name') or 'unknown'}) from {len(calls)} tool calls; request any deferred evidence on the next turn.",
})
for call in [selected_call]:
state["cycle_tool_calls"] = int(state.get("cycle_tool_calls") or 0) + 1
if state["cycle_tool_calls"] > self.settings.agent_tool_calls_per_cycle:
raise AutonomousGenerationError("TOOL_CALL_LIMIT: too many tool calls without a checkpoint commit or rollback")
function = call.get("function") if isinstance(call, dict) and isinstance(call.get("function"), dict) else {}
name = str(function.get("name") or "")
raw_arguments = str(function.get("arguments") or "{}")
if name not in allowed_names:
message = f"{name or 'Unknown tool'} is not available in the current durable state; use one of: {', '.join(sorted(allowed_names))}."
self.store.append_agent_audit(task_id, "tool-state-rejected", {"tool": name, "allowed_tools": sorted(allowed_names), "raw_arguments": raw_arguments})
self._record(state, "tool_error", {"tool": name, "message": message})
state["no_progress"] = int(state.get("no_progress") or 0) + 1
yield "tool_call", {"taskId": task_id, "tool": name, "status": "error", "message": message}
self._save_state(task_id, state)
continue
try:
arguments, repaired_arguments = parse_tool_arguments(raw_arguments)
except ValueError as error:
result = {"ok": False, "code": "TOOL_ARGUMENTS_INVALID", "message": str(error)}
self.store.append_agent_audit(task_id, "tool-arguments-invalid", {"tool": name, "raw_arguments": raw_arguments, "message": result["message"]})
self._record(state, "tool_error", {"tool": name, "message": result["message"]})
state["no_progress"] = int(state.get("no_progress") or 0) + 1
yield "tool_call", {"taskId": task_id, "tool": name, "status": "error", "message": result["message"]}
self._save_state(task_id, state)
continue
if repaired_arguments:
self.store.append_agent_audit(task_id, "tool-arguments-repaired", {"tool": name, "raw_arguments": raw_arguments, "arguments": arguments})
self._record(state, "tool_arguments_repaired", {"tool": name, "message": "Recovered a non-standard tool argument object."})
self.store.append_agent_audit(task_id, "tool-call", {"tool": name, "arguments": arguments})
yield "tool_call", {"taskId": task_id, "tool": name, "status": "running"}
events, progressed = await self._execute_tool(task_id, request, state, engine, name, arguments)
for event_name, payload in events:
yield event_name, payload
if progressed:
state["no_progress"] = 0
else:
correction = state.get("format_correction") if name == "submit_cdsl_fragment" else None
if isinstance(correction, dict) and str(correction.get("working_head") or "") == self._head_key(task):
repeat_count = int(correction.get("repeat_count") or 1)
if repeat_count >= self.settings.agent_format_error_repeat_limit:
self._save_state(task_id, state)
raise AutonomousGenerationError(
"AUTHORING_FORMAT_LOOP: repeated identical fragment contract violation "
f"for {correction.get('atomic_id') or 'unknown'} after {repeat_count} attempts: "
f"{correction.get('error') or 'invalid fragment'}"
)
# The first correction is useful feedback and the
# second gets the same precise card. Do not let
# either masquerade as CAD progress, but keep the
# separate exact-signature limit authoritative.
state["no_progress"] = 0
else:
state["no_progress"] = int(state.get("no_progress") or 0) + 1
self._save_state(task_id, state)
if int(state["no_progress"]) >= self.settings.agent_consecutive_no_progress_limit:
raise AutonomousGenerationError("NO_PROGRESS_LIMIT: no checkpoint commit or rollback was made after repeated attempts")
if any(event_name == "task_terminal" for event_name, _ in events):
return
except Exception as error:
message = str(error)
self.store.finish_generation(task_id, lifecycle="failed", failure={
"schema_version": "cad.autonomous-failure.v1", "stage": "autonomous_agent", "message": message,
"requirements_path": "requirements.md", "active_revision": str((self.store.read_task(task_id) or {}).get("active_revision") or ""),
"active_candidate_id": str((self.store.read_task(task_id) or {}).get("active_candidate_id") or ""),
"last_diagnostic": state.get("last_diagnostic") or "", "recent_events": state.get("recent_events") or [],
})
yield "task_terminal", {"taskId": task_id, "lifecycle": "failed", "message": message}
async def _execute_tool(
self,
task_id: str,
request: str,
state: dict[str, Any],
engine: Any,
name: str,
arguments: dict[str, Any],
) -> tuple[list[tuple[str, dict[str, Any]]], bool]:
task = self.store.read_task(task_id) or {}
events: list[tuple[str, dict[str, Any]]] = []
try:
if name in {"record_step_review", "commit_candidate", "discard_candidate", "audit_completion_checklist"}:
raise AutonomousGenerationError(
"AUTHOR_SELF_REVIEW_DISABLED: independent candidate review is the only path to a checkpoint or completion ledger"
)
if name == "write_requirements_document":
if self.store.read_requirements_document(task_id):
raise AutonomousGenerationError("requirements.md is frozen and cannot be rewritten")
path = self.store.write_requirements_document(task_id, str(arguments.get("markdown") or ""))
state["completion_checklist_required"] = True
state["completion_checklist_written"] = False
state["completion_ledger"] = {}
self._record(state, "requirements", {"message": "requirements.md written and frozen", "path": path.name})
events.append(("requirements_document", {"taskId": task_id, "status": "frozen", "path": "requirements.md"}))
return events, True
if not self.store.read_requirements_document(task_id):
raise AutonomousGenerationError("write_requirements_document must be the first tool call")
if name == "write_completion_checklist":
if self.store.read_completion_checklist(task_id):
raise AutonomousGenerationError("completion.md is frozen and cannot be rewritten")
markdown = str(arguments.get("markdown") or "")
items = parse_completion_checklist(markdown, require_unchecked=True)
path = self.store.write_completion_checklist(task_id, markdown)
state["completion_checklist_required"] = False
state["completion_checklist_written"] = True
state["completion_ledger"] = {
"items": [
{"item": item, "status": "missing", "evidence": "", "updated_at": now_iso()}
for item in items
],
"verified_revision": "",
"audited_at": "",
}
self._record(state, "completion_checklist", {
"message": "completion.md written and frozen",
"item_count": len(items),
})
events.append(("completion_checklist", {"taskId": task_id, "status": "frozen", "itemCount": len(items), "path": path.name}))
return events, True
if state.get("completion_checklist_required") and not state.get("completion_checklist_written"):
raise AutonomousGenerationError("write_completion_checklist must be completed before modelling")
if name == "inspect_model":
result = self._inspect_model_payload(task_id, task)
evidence_ref = self._record_geometry_diagnostic(task_id, task, state, "inspect")
if evidence_ref:
# This is the one diagnosis-time escape hatch for a later
# selector-based repair. It exposes only opaque tokens,
# never runtime selector records or stable identifiers.
_, topology, _, _ = self._artifact_data(task_id, task)
result["selector_tokens"] = autonomous_candidate_prompt_tokens(autonomous_selector_tokens(topology))[:24]
result["evidence_ref"] = evidence_ref
self._record(state, "inspect_model", {"message": "Current model inspected", "result": result})
observed = self._record_repair_observation(task, state, name, arguments)
events.append(("tool_call", {"taskId": task_id, "tool": name, "status": "success", "message": "Current checkpoint structure loaded."}))
if evidence_ref:
events.append(("geometry_diagnostic", {"taskId": task_id, "kind": "inspect", "evidenceRef": evidence_ref, "status": "success"}))
return events, bool(evidence_ref) or observed
if name == "read_cdsl_slice":
cdsl, _, _, _ = self._artifact_data(task_id, task)
if cdsl is None:
raise AutonomousGenerationError("There is no committed CDSL slice at the root head")
feature_ids = {str(value) for value in arguments.get("feature_ids") or []}
sketch_ids = {str(value) for value in arguments.get("sketch_ids") or []}
sketches = [item for item in ((cdsl.get("geometry") or {}).get("sketches") or []) if isinstance(item, dict) and (not sketch_ids or str(item.get("id")) in sketch_ids)]
features = [item for item in cdsl.get("features") or [] if isinstance(item, dict) and (not feature_ids or str(item.get("id")) in feature_ids)]
result = {"sketches": sketches[:12], "features": features[:12]}
self._record(state, "read_cdsl_slice", {"message": "Requested CDSL slice loaded", "result": result})
observed = self._record_repair_observation(task, state, name, arguments)
events.append(("tool_call", {"taskId": task_id, "tool": name, "status": "success", "message": "CDSL slice returned to author."}))
return events, observed
if name == "measure_model":
cdsl, topology, report, identifier = self._artifact_data(task_id, task)
if cdsl is None:
raise AutonomousGenerationError("There is no model to measure")
feature_id = str(arguments.get("feature_id") or "")
engine_result = report.get("engine_result") if isinstance(report, dict) and isinstance(report.get("engine_result"), dict) else {}
records = [record for record in (topology or {}).get("records") or [] if isinstance(record, dict)]
if feature_id:
records = [record for record in records if feature_id == str(record.get("feature_id") or "") or feature_id in {str(owner) for owner in record.get("owner_feature_ids") or []}]
topology_counts, topology_sample = _compact_measurement_records(records)
result = {
"working_head": identifier,
"bbox_mm": engine_result.get("bbox_mm"),
"volume_mm3": engine_result.get("volume_mm3"),
"solid_count": engine_result.get("solid_count"),
"feature_id": feature_id or None,
"topology_counts": topology_counts,
"topology_sample": topology_sample,
}
evidence_ref = self._record_geometry_diagnostic(task_id, task, state, "measure")
if evidence_ref:
result["evidence_ref"] = evidence_ref
self._record(state, "measure_model", {"message": "Deterministic measurement returned", "result": result})
observed = self._record_repair_observation(task, state, name, arguments)
events.append(("tool_call", {"taskId": task_id, "tool": name, "status": "success", "message": "Measurements returned to author."}))
if evidence_ref:
events.append(("geometry_diagnostic", {"taskId": task_id, "kind": "measure", "evidenceRef": evidence_ref, "status": "success"}))
return events, bool(evidence_ref) or observed
if name == "inspect_topology":
_, topology, _, identifier = self._artifact_data(task_id, task)
tokens = autonomous_selector_tokens(topology)
kind = str(arguments.get("kind") or "")
limit = int(arguments.get("limit") or 24)
action_required = state.get("candidate_action_required")
selector_recovery = (
isinstance(action_required, dict)
and str(action_required.get("working_head") or "") == self._head_key(task)
and str(action_required.get("reason") or "") == "edge_selector_recovery"
)
if selector_recovery and kind != "edge":
message = "EDGE_SELECTOR_RECOVERY: inspect_topology must use kind=edge before retrying a chamfer or fillet"
self._record(state, "selector_recovery_error", {"message": message, "kind": kind})
events.append(("tool_call", {"taskId": task_id, "tool": name, "status": "error", "message": message}))
return events, False
# A broad edge listing can exceed the author context budget
# and erase the very token bank needed for the finish. The
# recovery path deliberately exposes a compact, durable list.
effective_limit = min(limit, 12) if selector_recovery else limit
result = {"working_head": identifier, "tokens": autonomous_candidate_prompt_tokens(tokens, kind=kind)[:effective_limit]}
if selector_recovery:
if not result["tokens"]:
state["edge_selector_recovery"] = {}
state["candidate_action_required"] = {
"working_head": self._head_key(task),
"reason": "edge_selector_recovery_no_edges",
"instruction": "No current edge selector tokens are available for the requested finish. Roll back rather than retrying the same finish.",
}
self._record(state, "selector_recovery_no_edges", {"message": "No edge token is available for the requested finish."})
else:
state["edge_selector_recovery"] = {
"working_head": self._head_key(task),
"geometry_fingerprint": self._active_geometry_fingerprint(task_id, task),
"tokens": result["tokens"],
}
state["candidate_action_required"] = {
"working_head": self._head_key(task),
"reason": "edge_selector_recovery_submit",
"instruction": "Use one or more returned edge selector tokens in the next chamfer or fillet fragment. Do not inspect topology again until the geometry changes.",
}
self._record(state, "selector_recovery_complete", {"message": "Fresh edge selector tokens returned; submit a corrected finish or roll back.", "token_count": len(result["tokens"])})
self._record(state, "inspect_topology", {"message": "Current topology tokens returned", "result": result})
final_repair = state.get("final_repair")
if isinstance(final_repair, dict) and str(final_repair.get("working_head") or "") == self._head_key(task):
final_repair["topology_observed"] = True
state["final_repair"] = final_repair
observed = self._record_repair_observation(task, state, name, arguments)
events.append(("tool_call", {"taskId": task_id, "tool": name, "status": "success", "message": f"Returned {len(result['tokens'])} current topology token(s)."}))
return events, observed
if name == "get_cdsl_operation_contract":
atomic_id = str(arguments.get("atomic_id") or "").strip()
if not atomic_id:
raise AutonomousGenerationError("atomic_id is required for get_cdsl_operation_contract")
result = _operation_contract_payload(engine, atomic_id)
self._record(state, "get_cdsl_operation_contract", {"message": f"Runtime contract returned for {atomic_id}.", "result": result})
events.append(("tool_call", {"taskId": task_id, "tool": name, "status": "success", "message": f"Operation contract loaded for {atomic_id}."}))
return events, True
if name in {"render_views", "render_section"}:
_, _, step_path, identifier = _model_paths(self.store, task_id, task)
if step_path is None or not step_path.is_file():
raise AutonomousGenerationError("There is no successfully built model to render")
if identifier.startswith("candidate_"):
render_dir = self.store.candidate_dir(task_id, identifier) / "renders"
else:
render_dir = self.store.revision_dir(task_id, identifier) / "author-renders"
if name == "render_views":
manifest_path = render_dir / "render-manifest.json"
if self.settings.agent_render_cache and manifest_path.is_file():
manifest = read_json(manifest_path)
else:
manifest = await asyncio.to_thread(render_checkpoint, self.settings, step_path=step_path, output_dir=render_dir)
if not isinstance(manifest, dict):
raise AutonomousGenerationError("Render manifest is invalid")
# Archive all seven views, but attach only two compact
# author-facing images. The contact sheet gives global
# orientation and the isometric frame gives readable edge
# detail without repeatedly spending seven image budgets.
paths = []
contact_sheet = Path(str(manifest.get("contact_sheet_path") or ""))
if contact_sheet.is_file():
paths.append(str(contact_sheet))
isometric = next(
(Path(str(item.get("path") or "")) for item in manifest.get("views") or () if isinstance(item, dict) and item.get("id") == "isometric"),
None,
)
if isometric and isometric.is_file():
paths.append(str(isometric))
if not paths:
raise AutonomousGenerationError("Render manifest has no usable author-facing views")
self._record(state, "render_views", {"message": "Seven technical views rendered", "working_head": identifier, "manifest": str(manifest_path)})
else:
section_dir = render_dir / f"section_{secrets.token_hex(4)}"
section = await asyncio.to_thread(render_section, self.settings, step_path=step_path, output_dir=section_dir, origin_mm=arguments.get("origin_mm"), normal=arguments.get("normal"))
paths = [str(section["path"])]
self._record(state, "render_section", {"message": "OpenCascade section rendered", "working_head": identifier, "section": {key: section.get(key) for key in ("plane", "contour_count", "bounds_mm")}})
state["pending_images"] = paths
evidence_ref = self._record_geometry_diagnostic(task_id, task, state, "render") if name == "render_views" else None
observed = self._record_repair_observation(task, state, name, arguments)
events.append(("tool_call", {"taskId": task_id, "tool": name, "status": "success", "message": "Render evidence will be attached to the next author turn."}))
if evidence_ref:
events.append(("geometry_diagnostic", {"taskId": task_id, "kind": "render", "evidenceRef": evidence_ref, "status": "success"}))
return events, bool(evidence_ref) or observed
if name == "record_geometry_conclusion":
rejection = self._active_noop_rejection(task_id, task, state)
if rejection is None:
raise AutonomousGenerationError("GEOMETRY_CONCLUSION_NOT_REQUIRED: no active unchanged-geometry candidate requires a conclusion")
fingerprint = str(rejection.get("geometry_fingerprint") or self._active_geometry_fingerprint(task_id, task))
diagnosis = self._diagnosis_state(state, fingerprint)
observations = diagnosis.get("observations") if isinstance(diagnosis.get("observations"), dict) else {}
refs = [str(item).strip() for item in arguments.get("evidence_refs") or [] if str(item).strip()]
if not refs:
raise AutonomousGenerationError("GEOMETRY_CONCLUSION_EVIDENCE_REQUIRED: cite at least one current diagnostic evidence_ref")
if len(refs) != len(set(refs)):
raise AutonomousGenerationError("GEOMETRY_CONCLUSION_EVIDENCE_DUPLICATE: evidence_refs must be unique")
valid_refs = {str(item.get("ref") or "") for item in observations.values() if isinstance(item, dict)}
unknown = [ref for ref in refs if ref not in valid_refs]
if unknown:
raise AutonomousGenerationError("GEOMETRY_CONCLUSION_EVIDENCE_UNKNOWN: " + ", ".join(unknown))
decision = str(arguments.get("decision") or "")
plan = arguments.get("optimization_plan")
if decision == "modify":
if not isinstance(plan, dict) or not str(plan.get("action") or plan.get("next_action") or "").strip():
raise AutonomousGenerationError("GEOMETRY_CONCLUSION_PLAN_REQUIRED: modify requires a non-empty optimization_plan.action (next_action is also accepted)")
# This is audit metadata, not CAD input. Accept the
# common next_action spelling used by ordinary tool-call
# models while persisting one canonical action for later
# author context and human review.
plan = {**plan, "action": str(plan.get("action") or plan.get("next_action") or "").strip()}
if decision == "complete" and self._completion_has_known_incomplete(task, state):
raise AutonomousGenerationError(
"GEOMETRY_CONCLUSION_COMPLETE_BLOCKED: the current completion audit has unresolved requirements; choose modify or rollback"
)
conclusion = {
"root_cause": str(arguments.get("root_cause") or "unknown"),
"evidence_refs": refs,
"decision": decision,
"optimization_plan": deepcopy(plan) if isinstance(plan, dict) else {},
"geometry_fingerprint": fingerprint,
"working_head": self._head_key(task),
"recorded_at": now_iso(),
}
diagnosis["conclusion"] = conclusion
state["geometry_rejection"] = {**rejection, "conclusion_required": False, "conclusion_recorded_at": conclusion["recorded_at"]}
self._record(state, "geometry_conclusion", {
"message": f"Author chose {decision} after unchanged-geometry diagnosis.",
"decision": decision,
"root_cause": conclusion["root_cause"],
"evidence_refs": refs,
})
events.append(("geometry_conclusion", {"taskId": task_id, "decision": decision, "rootCause": conclusion["root_cause"], "evidenceRefs": refs, "status": "success"}))
return events, True
if name == "submit_cdsl_fragment":
if str(task.get("active_candidate_id") or ""):
raise AutonomousGenerationError("Commit or discard the active candidate before submitting another fragment")
head = self._head_key(task)
batch_goal = str(arguments.get("batch_goal") or "").strip()
if not batch_goal:
raise AutonomousGenerationError("BATCH_GOAL_REQUIRED: describe the coherent geometry this batch must achieve")
attempts = state.setdefault("candidate_attempts_by_head", {})
raw = str(arguments.get("fragment_json") or "")
shared_revolve_axis = arguments.get("shared_revolve_axis")
self.store.append_agent_audit(task_id, "fragment", {
"raw_fragment_json": raw,
"batch_goal": batch_goal,
"shared_revolve_axis": shared_revolve_axis,
"working_head": head,
})
try:
fragment = json.loads(raw)
except json.JSONDecodeError as error:
raise AutonomousGenerationError(f"FRAGMENT_JSON_INVALID at line {error.lineno}, column {error.colno}: {error.msg}") from error
if not isinstance(fragment, dict):
raise AutonomousGenerationError("FRAGMENT_JSON_INVALID: fragment_json must decode to an object")
if shared_revolve_axis is not None:
if "revolve_axis" in fragment and fragment["revolve_axis"] != shared_revolve_axis:
raise AutonomousGenerationError(
"CONFLICTING_REVOLVE_AXIS: shared_revolve_axis conflicts with fragment_json.revolve_axis"
)
fragment["revolve_axis"] = deepcopy(shared_revolve_axis)
action_required = state.get("candidate_action_required")
recovery = state.get("edge_selector_recovery")
if (
isinstance(action_required, dict)
and str(action_required.get("working_head") or "") == head
and str(action_required.get("reason") or "") == "edge_selector_recovery_submit"
and isinstance(recovery, dict)
and str(recovery.get("working_head") or "") == head
):
allowed_tokens = {
str(item.get("token") or "")
for item in recovery.get("tokens") or ()
if isinstance(item, dict) and str(item.get("token") or "")
}
supplied_tokens = _fragment_selector_tokens(fragment)
if not supplied_tokens or any(token not in allowed_tokens for token in supplied_tokens):
raise AutonomousGenerationError(
"EDGE_SELECTOR_RECOVERY_TOKEN_REQUIRED: copy one or more selector_tokens exactly from edge_selector_recovery.tokens or rollback"
)
geometry_fingerprint = self._active_geometry_fingerprint(task_id, task)
fragment_fingerprint = hashlib.sha256(
json.dumps(fragment, ensure_ascii=True, sort_keys=True, separators=(",", ":")).encode("utf-8")
).hexdigest()
rejected = {
str(item) for item in (state.setdefault("rejected_fragment_fingerprints_by_geometry", {}).get(geometry_fingerprint) or [])
if str(item)
}
if fragment_fingerprint in rejected:
raise AutonomousGenerationError(
"CANDIDATE_FRAGMENT_DUPLICATE: this fragment was already rejected against the unchanged geometry; "
"record a new conclusion and choose a materially different fragment or rollback."
)
base_path = self.store.current_cdsl_path(task_id)
base_cdsl = read_json(base_path) if base_path and base_path.is_file() else None
_, topology, _, _ = self._artifact_data(task_id, task)
materialized, fragment_audit = materialize_autonomous_fragment(base_cdsl if isinstance(base_cdsl, dict) else None, fragment, engine=engine, selector_tokens=autonomous_selector_tokens(topology), max_features=self.settings.agent_max_features_per_fragment)
# A materialised document that cannot pass the static CDSL
# contract never reached the CAD engine. Treat it as author
# format feedback, not one of the scarce rebuild attempts for
# this geometric working head. build_candidate validates a
# second time immediately before execution as a defense in
# depth check.
preflight = deepcopy(materialized)
preflight["part_id"] = task_id
validate_cdsl(preflight, engine)
used = int(attempts.get(head) or 0)
if used >= self.settings.agent_candidate_attempts_per_head:
raise AutonomousGenerationError(f"CANDIDATE_ATTEMPT_LIMIT: head {head} exhausted {self.settings.agent_candidate_attempts_per_head} candidate build attempts")
attempts[head] = used + 1
candidate = await asyncio.to_thread(build_candidate, settings=self.settings, store=self.store, task_id=task_id, cdsl=materialized, fragment_audit=fragment_audit, parent_revision_id=str(task.get("active_revision") or ""))
state["format_correction"] = {}
state["geometry_rejection"] = {}
state["candidate_action_required"] = {}
state["edge_selector_recovery"] = {}
self._record(state, "candidate_built", {"message": "Candidate rebuilt successfully; independent visual review is running", "batch_goal": batch_goal, "health": candidate.get("health")})
fixes = candidate.get("compatibility_fixes") or []
if fixes:
self._record(state, "compatibility_normalized", {
"message": "Equivalent CDSL field aliases were normalized before validation.",
"fixes": fixes,
})
events.append(("candidate_result", self._candidate_result_payload(task_id, candidate, status="success")))
review = await self._review_staged_candidate(task_id, task, state, candidate, batch_goal)
state["last_candidate_review"] = {
"candidate_id": str(candidate.get("candidate_id") or ""),
"verdict": review["verdict"],
"batch_goal": batch_goal,
"batch_goal_status": review["batch_goal_status"],
"evidence": review["evidence"],
"coverage": review["coverage"],
"recorded_at": now_iso(),
}
self._record(state, "candidate_review", {
"message": "; ".join(review["evidence"]) or review["verdict"],
"candidate_id": candidate.get("candidate_id"),
"verdict": review["verdict"],
"batch_goal": batch_goal,
"batch_goal_status": review["batch_goal_status"],
})
events.append(("candidate_review", {
"taskId": task_id,
"candidateId": candidate.get("candidate_id") or "",
"status": "success" if review["verdict"] == "accept" else "error",
"review": review,
"message": "; ".join(review["evidence"]),
}))
if review["verdict"] == "accept":
_, commit_events = await self._commit_reviewed_candidate(
task_id, task, state, str(candidate["candidate_id"]), candidate_review=review,
)
events.extend(commit_events)
return events, True
candidate_dir = self.store.candidate_dir(task_id, str(candidate["candidate_id"]))
rejected_candidate = read_json(candidate_dir / "candidate.json", candidate)
if isinstance(rejected_candidate, dict):
rejected_candidate["status"] = "rejected"
rejected_candidate["rejected_at"] = now_iso()
rejected_candidate["reject_reason"] = "; ".join(review["evidence"])
write_json(candidate_dir / "candidate.json", rejected_candidate)
self.store.clear_active_candidate(task_id, str(candidate["candidate_id"]))
rejected_by_geometry = state.setdefault("rejected_fragment_fingerprints_by_geometry", {})
prior_rejected = {str(item) for item in rejected_by_geometry.get(geometry_fingerprint, ()) if str(item)}
prior_rejected.add(fragment_fingerprint)
rejected_by_geometry[geometry_fingerprint] = sorted(prior_rejected)
state["candidate_action_required"] = {
"working_head": self._head_key(task),
"reason": "candidate_review_rejected",
"candidate_id": str(candidate.get("candidate_id") or ""),
"evidence": list(review.get("evidence") or ())[:12],
"instruction": (
"The independent reviewer rejected the rendered candidate. Its exact evidence is retained in "
"last_candidate_review. Submit a materially different corrective fragment or rollback now; "
"do not repeat inspect_model or other observations on this unchanged checkpoint."
),
}
self._record(state, "candidate_rejected", {
"message": "Independent reviewer rejected the batch; the working checkpoint is unchanged.",
"candidate_id": candidate.get("candidate_id"),
"batch_goal": batch_goal,
})
events.append(("candidate_result", self._candidate_result_payload(
task_id, candidate, status="rejected", message="; ".join(review["evidence"]),
)))
return events, True
if name == "record_step_review":
candidate_id = str(task.get("active_candidate_id") or "")
if not candidate_id:
raise AutonomousGenerationError("A successful candidate is required before recording a step review")
review_dir = self.store.candidate_dir(task_id, candidate_id) / "reviews"
review_dir.mkdir(parents=True, exist_ok=True)
review_path = review_dir / f"review_{secrets.token_hex(6)}.md"
markdown = str(arguments.get("markdown") or "").strip()
if not markdown:
raise AutonomousGenerationError("Step review markdown must not be empty")
decision = str(arguments.get("decision") or "").strip()
if decision not in {"continue", "commit", "discard", "rollback"}:
raise AutonomousGenerationError("Step review decision must be continue, commit, discard, or rollback")
# A built candidate cannot be extended without first being
# committed. In ordinary author language "continue" means
# the candidate is accepted and modelling should continue, so
# promote it atomically with this review instead of spending a
# separate commit-only model round trip.
committed = decision in {"continue", "commit"}
persisted_decision = "commit" if committed else decision
review_path.write_text(markdown + "\n", encoding="utf-8")
review = {"candidate_id": candidate_id, "path": review_path.relative_to(self.store.task_dir(task_id)).as_posix(), "decision": persisted_decision, "recorded_at": now_iso()}
candidate = read_json(self.store.candidate_dir(task_id, candidate_id) / "candidate.json", {})
if isinstance(candidate, dict):
candidate["step_review_path"] = review["path"]
candidate["step_review_decision"] = review["decision"]
candidate["step_review_recorded_at"] = review["recorded_at"]
write_json(self.store.candidate_dir(task_id, candidate_id) / "candidate.json", candidate)
state["last_review"] = review
self._record(state, "step_review", {"message": "Step review recorded", **review})
events.append(("step_review", {"taskId": task_id, **review}))
if committed:
_, commit_events = await self._commit_reviewed_candidate(task_id, task, state, candidate_id)
events.extend(commit_events)
return events, True
if name == "audit_completion_checklist":
checklist = self._completion_items(task_id)
if not checklist:
raise AutonomousGenerationError("write_completion_checklist must be completed before the final audit")
audit = parse_completion_audit(str(arguments.get("markdown") or ""), checklist)
ledger = {
"items": [{**item, "updated_at": now_iso()} for item in audit],
"verified_revision": str(task.get("active_revision") or ""),
"audited_at": now_iso(),
}
state["completion_ledger"] = ledger
complete_count = sum(item["status"] == "complete" for item in audit)
if complete_count != len(audit):
rejection = self._active_noop_rejection(task_id, task, state)
if rejection is None:
fingerprint = self._active_geometry_fingerprint(task_id, task)
diagnosis = self._diagnosis_state(state, fingerprint)
# A failed completion audit is a rejection of the
# current geometry, even when no candidate rebuild
# failed. Preserve prior observations for this exact
# solid, but require a fresh conclusion before any
# modification or rollback.
diagnosis["conclusion"] = None
state["geometry_rejection"] = {
"signature": f"completion_audit:{self._head_key(task)}:{fingerprint}",
"working_head": self._head_key(task),
"geometry_fingerprint": fingerprint,
"source": "completion_audit",
"message": "The completion checklist has unresolved requirements.",
"instruction": "Collect at most one inspect, measure, and render diagnostic for this unchanged geometry, then record a modify or rollback conclusion.",
"conclusion_required": True,
}
self._record(state, "completion_repair_required", {
"message": "Completion audit found unresolved requirements; a bounded diagnosis and conclusion are now required.",
"complete_count": complete_count,
"item_count": len(audit),
"geometry_fingerprint": fingerprint[:12],
})
else:
fingerprint = str(rejection.get("geometry_fingerprint") or self._active_geometry_fingerprint(task_id, task))
diagnosis = self._diagnosis_state(state, fingerprint)
conclusion = diagnosis.get("conclusion")
if isinstance(conclusion, dict) and str(conclusion.get("decision") or "") == "complete":
diagnosis["conclusion"] = None
rejection["conclusion_required"] = True
rejection["instruction"] = "The completion audit has unresolved items. Record a modify or rollback conclusion; do not repeat this audit until the geometry changes."
state["geometry_rejection"] = rejection
self._record(state, "geometry_conclusion_reopened", {
"message": "Completion conclusion was invalidated by unresolved checklist items.",
"complete_count": complete_count,
"item_count": len(audit),
})
self._record(state, "completion_audit", {
"message": f"Completion checklist audited: {complete_count}/{len(audit)} item(s) complete.",
"complete_count": complete_count,
"item_count": len(audit),
})
events.append(("completion_audit", {
"taskId": task_id,
"revisionId": str(task.get("active_revision") or ""),
"completeCount": complete_count,
"itemCount": len(audit),
"ready": complete_count == len(audit),
}))
return events, True
if name == "commit_candidate":
candidate_id = str(task.get("active_candidate_id") or "")
review = state.get("last_review") if isinstance(state.get("last_review"), dict) else {}
if not candidate_id or str(review.get("candidate_id") or "") != candidate_id:
raise AutonomousGenerationError("record_step_review is required for the active candidate before commit")
_, commit_events = await self._commit_reviewed_candidate(task_id, task, state, candidate_id)
events.extend(commit_events)
return events, True
if name == "discard_candidate":
candidate_id = str(task.get("active_candidate_id") or "")
if not candidate_id:
raise AutonomousGenerationError("There is no active staged candidate to discard")
review = state.get("last_review") if isinstance(state.get("last_review"), dict) else {}
if str(review.get("candidate_id") or "") != candidate_id:
raise AutonomousGenerationError("record_step_review is required for the active candidate before discard")
candidate_dir = self.store.candidate_dir(task_id, candidate_id)
candidate = read_json(candidate_dir / "candidate.json", {})
if isinstance(candidate, dict):
candidate["status"] = "discarded"
candidate["discard_reason"] = str(arguments.get("reason") or "")
candidate["discarded_at"] = now_iso()
write_json(candidate_dir / "candidate.json", candidate)
self.store.clear_active_candidate(task_id, candidate_id)
state["pending_images"] = []
state["candidate_action_required"] = {}
state["edge_selector_recovery"] = {}
self._record(state, "candidate_discarded", {"message": "Candidate discarded", "candidate_id": candidate_id})
events.append(("candidate_result", {"taskId": task_id, "candidateId": candidate_id, "status": "discarded", "message": str(arguments.get("reason") or "")}))
return events, True
if name == "rollback_checkpoint":
requested_token = str(arguments.get("checkpoint_token") or "").strip()
requested = _rollback_tokens(task).get(requested_token)
if requested is None:
raise AutonomousGenerationError("Rollback target must be a checkpoint token returned by inspect_model")
current = str(task.get("active_revision") or "")
by_id = {str(item.get("revision_id") or ""): item for item in task.get("revisions") or [] if isinstance(item, dict)}
lineage = {""}
pointer = current
while pointer:
lineage.add(pointer)
pointer = str((by_id.get(pointer) or {}).get("parent_revision_id") or "")
if requested not in lineage:
raise AutonomousGenerationError("Rollback target must be an ancestor of the active checkpoint")
if requested == current:
# Branching at the current checkpoint neither removes a
# feature nor changes the geometry. Treating it as a
# rollback would let an author erase final-repair state
# without repairing the failed requirements.
raise AutonomousGenerationError("Rollback target must be a strict ancestor of the active checkpoint")
candidate_id = str(task.get("active_candidate_id") or "")
if candidate_id:
candidate_path = self.store.candidate_dir(task_id, candidate_id) / "candidate.json"
candidate = read_json(candidate_path, {})
if isinstance(candidate, dict):
candidate["status"] = "abandoned"
candidate["abandon_reason"] = "rollback_checkpoint"
candidate["abandoned_at"] = now_iso()
write_json(candidate_path, candidate)
self.store.clear_active_candidate(task_id, candidate_id)
# Candidate render paths belong to the abandoned branch/head;
# never attach them as evidence for the next author decision.
state["pending_images"] = []
state["format_correction"] = {}
state["geometry_rejection"] = {}
state["candidate_action_required"] = {}
state["edge_selector_recovery"] = {}
carried_repair = state.get("final_repair") if isinstance(state.get("final_repair"), dict) else None
branch_id = f"branch_{secrets.token_hex(4)}"
self.store.rollback_to_revision(task_id, requested, branch_id=branch_id)
self._invalidate_completion_audit(task_id, state, reason="rollback")
if carried_repair is not None and str(carried_repair.get("working_head") or "") == self._head_key(task):
# A rollback removes an approach, not the unresolved
# completion requirements. Retain the reviewer evidence
# on the new working head and force fresh topology before
# any next corrective action.
carried_repair["working_head"] = f"{branch_id}:{requested or 'root'}"
carried_repair["topology_observed"] = False
carried_repair["evidence_stale_after_checkpoint"] = True
carried_repair["latest_checkpoint_revision"] = requested
state["final_repair"] = carried_repair
else:
state["final_repair"] = {}
self._record(state, "rollback", {"message": str(arguments.get("reason") or "Rollback requested by author")})
events.append(("rollback", {"taskId": task_id, "reason": str(arguments.get("reason") or "")}))
return events, True
if name == "complete_task":
if str(task.get("active_candidate_id") or ""):
raise AutonomousGenerationError("An independently reviewed candidate must resolve before final completion")
coverage_error = self._completion_gate_error(task_id, task, state)
if coverage_error:
raise AutonomousGenerationError(coverage_error)
review, health = await self._final_review(task_id, state, str(arguments.get("self_review") or ""))
self._record(state, "final_review", {"message": "; ".join(review.get("evidence") or []) or review.get("verdict") or "", "review": review})
events.append(("final_review", {"taskId": task_id, "review": review, "health": health}))
if review.get("verdict") != "pass":
state["final_repair"] = _final_repair_card(task, review)
self._record(state, "final_repair", {
"message": "Final review requires an author rollback or corrective fragment before completion.",
"review": state["final_repair"],
})
events.append(("agent_thinking", {
"taskId": task_id,
"message": "最终复核未通过。请根据复核证据回滚错误特征或提交修复片段;所有 warning 和 repair 都会阻止发布。",
}))
return events, True
self.store.finish_generation(task_id, lifecycle="completed")
final_task = self.store.read_task(task_id) or {}
active_revision = str(final_task.get("published_revision") or final_task.get("active_revision") or "")
revision = next((item for item in final_task.get("revisions") or [] if isinstance(item, dict) and str(item.get("revision_id") or "") == active_revision), None)
if isinstance(revision, dict):
events.append(("cad_result", self._result_payload({"task_id": task_id, **revision}, "completed", False)))
events.append(("task_terminal", {"taskId": task_id, "lifecycle": "completed", "revisionId": active_revision}))
return events, True
raise AutonomousGenerationError(f"Unknown autonomous authoring tool: {name}")
except Exception as error:
message = str(error)
if name == "submit_cdsl_fragment":
if message == "Commit or discard the active candidate before submitting another fragment":
state["format_correction"] = {}
state["candidate_action_required"] = {
"working_head": self._head_key(task),
"instruction": "A staged candidate is awaiting its required review and decision before further modelling.",
}
self._record(state, "candidate_action_required", {
"message": "A staged candidate must be reviewed and committed or discarded before another fragment.",
})
elif message.startswith("EDGE_SELECTOR_RECOVERY_TOKEN_REQUIRED:"):
# Do not refresh topology: the current model is unchanged
# and the durable token bank remains authoritative.
state["format_correction"] = {}
self._record(state, "selector_recovery_token_rejected", {
"message": "The finish fragment did not reuse a token from the durable edge selector bank.",
})
elif message.startswith("CANDIDATE_GEOMETRY_UNCHANGED:"):
prior = state.get("geometry_rejection") if isinstance(state.get("geometry_rejection"), dict) else {}
signature = f"{self._head_key(task)}|{message}"
repeats = int(prior.get("repeat_count") or 0) + 1 if prior.get("signature") == signature else 1
geometry_fingerprint = self._active_geometry_fingerprint(task_id, task)
try:
attempted_fragment = json.loads(str(arguments.get("fragment_json") or ""))
except json.JSONDecodeError:
attempted_fragment = None
if isinstance(attempted_fragment, dict):
fragment_fingerprint = hashlib.sha256(
json.dumps(attempted_fragment, ensure_ascii=True, sort_keys=True, separators=(",", ":")).encode("utf-8")
).hexdigest()
rejected = state.setdefault("rejected_fragment_fingerprints_by_geometry", {}).setdefault(geometry_fingerprint, [])
if fragment_fingerprint not in rejected:
rejected.append(fragment_fingerprint)
state["format_correction"] = {}
state["geometry_rejection"] = {
"signature": signature,
"working_head": self._head_key(task),
"geometry_fingerprint": geometry_fingerprint,
"repeat_count": repeats,
"message": message,
"instruction": "Collect at most one inspect, measure, and render diagnostic for this unchanged geometry, then record a conclusion before modifying, rolling back, or completing.",
"conclusion_required": True,
}
diagnosis = self._diagnosis_state(state, geometry_fingerprint)
diagnosis["conclusion"] = None
self._record(state, "geometry_rejection", {
"message": "Candidate was rejected because its rebuilt solid did not change.",
"repeat_count": repeats,
"geometry_fingerprint": geometry_fingerprint[:12],
})
elif message.startswith("CANDIDATE_REVIEW_FAILED:"):
state["format_correction"] = {}
self._record(state, "candidate_review_failed", {
"message": "Independent review did not produce a usable verdict; the candidate was retained for audit and the checkpoint was not advanced.",
})
elif message.startswith("CANDIDATE_FRAGMENT_DUPLICATE:"):
rejection = self._active_noop_rejection(task_id, task, state)
if rejection is not None:
fingerprint = str(rejection.get("geometry_fingerprint") or self._active_geometry_fingerprint(task_id, task))
try:
duplicate_fragment = json.loads(str(arguments.get("fragment_json") or ""))
duplicate_key = hashlib.sha256(
json.dumps(duplicate_fragment, ensure_ascii=True, sort_keys=True, separators=(",", ":")).encode("utf-8")
).hexdigest()
except (TypeError, ValueError, json.JSONDecodeError):
duplicate_key = "unknown"
duplicate_counts = state.setdefault("duplicate_fragment_rejections_by_geometry", {}).setdefault(fingerprint, {})
repeat_count = int(duplicate_counts.get(duplicate_key) or 0) + 1
duplicate_counts[duplicate_key] = repeat_count
self._diagnosis_state(state, fingerprint)["conclusion"] = None
rejection["conclusion_required"] = True
rejection["instruction"] = "The same rejected fragment cannot be rebuilt. Reuse existing evidence to record a new conclusion, then choose a different fragment or rollback."
state["geometry_rejection"] = rejection
if repeat_count >= self.settings.agent_candidate_attempts_per_head:
state["candidate_action_required"] = {
"working_head": self._head_key(task),
"reason": "duplicate_fragment_limit",
"instruction": "The same rejected fragment was submitted repeatedly. Roll back to a strict ancestor instead of retrying it again.",
}
self._record(state, "duplicate_fragment_limit", {
"message": "Repeated structurally identical rejected fragments now require rollback.",
"repeat_count": repeat_count,
})
elif message.startswith("CANDIDATE_ATTEMPT_LIMIT:"):
# This is not a malformed CDSL fragment. Repeating the
# same call cannot improve the model, so give the author
# only evidence/rollback choices instead of a spurious
# exact-format correction card.
state["format_correction"] = {}
state["candidate_action_required"] = {
"working_head": self._head_key(task),
"reason": "candidate_attempt_limit",
"instruction": "This working head exhausted its material candidate attempts. Inspect the model and choose a rollback checkpoint; do not resubmit a fragment on this head.",
}
self._record(state, "candidate_attempt_limit", {
"message": "The current head exhausted its candidate attempts and now requires reassessment or rollback.",
"head": self._head_key(task),
})
else:
correction = _format_correction_card(
engine,
fragment_json=str(arguments.get("fragment_json") or ""),
error_message=message,
head=self._head_key(task),
previous=state.get("format_correction") if isinstance(state.get("format_correction"), dict) else None,
)
action_required = state.get("candidate_action_required")
already_recovered = (
isinstance(action_required, dict)
and str(action_required.get("working_head") or "") == self._head_key(task)
and str(action_required.get("reason") or "") == "edge_selector_recovery_submit"
)
if _requires_edge_selector_recovery(correction) and not already_recovered:
state["format_correction"] = {}
state["candidate_action_required"] = {
"working_head": self._head_key(task),
"reason": "edge_selector_recovery",
"instruction": "This chamfer or fillet needs opaque edge selector tokens. Call inspect_topology exactly once with kind=edge, then submit a corrected fragment using the returned token(s).",
}
self._record(state, "selector_recovery_required", {
"message": "Finish operation needs a fresh edge selector lookup before it can be corrected.",
"atomic_id": correction.get("atomic_id"),
"error": message,
})
else:
state["format_correction"] = correction
self._record(state, "format_correction", {
"message": "Author fragment requires an exact contract correction.",
"atomic_id": correction.get("atomic_id"),
"repeat_count": correction.get("repeat_count"),
"error": message,
})
self.store.append_agent_audit(task_id, "tool-error", {"tool": name, "message": message})
self._record(state, "tool_error", {"tool": name, "message": message})
if name == "submit_cdsl_fragment":
events.append(("candidate_result", {"taskId": task_id, "status": "error", "message": message}))
else:
events.append(("tool_call", {"taskId": task_id, "tool": name, "status": "error", "message": message}))
return events, False