Files
cdsl-cad/backend/app/services/autonomous_cdsl_generation.py
T
2026-08-31 14:16:08 +08:00

5181 lines
315 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Autonomous, append-only CDSL authoring loop.
The author model observes a frozen requirements document and real CAD state,
then submits one small operation-specific fragment at a time. The active
Runtime contract supplies the provider tool schema; validation still happens
at the trust boundary where the complete materialised CDSL is rebuilt by the
CDSL-only engine in an isolated 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.cdsl_authoring_schema import (
build_operation_fragment_schema,
canonical_fragment_example,
operation_contract_hash,
)
from app.services.engine_service import (
feature_atomic_contract,
load_engine,
step_to_glb,
topology_sidecars,
topology_snapshot,
validate_cdsl,
)
from app.services.library import CdslLibrary
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, review_modeling_plan
from app.settings import ProviderConfig, ProviderModel, Settings
Completion = Callable[..., 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 _response_language(text: str) -> str:
"""Choose the language for visible Agent prose from the user's request."""
cjk = sum(1 for char in text if "\u4e00" <= char <= "\u9fff")
latin = sum(1 for char in text if char.isascii() and char.isalpha())
return "Chinese" if cjk >= 2 and cjk >= latin * 0.15 else "English"
def _visible_error(message: str, request: str) -> str:
if _response_language(request) != "Chinese":
return message
translations = {
"Each fragment feature must declare a runtime atomic_id": "候选片段无效:每个特征都必须声明运行时 atomic_id。",
"write_requirements_document must be the first tool call": "必须先写入并冻结需求文档,之后才能调用其他建模工具。",
"write_completion_checklist must be completed before modelling": "必须先写完并冻结完成清单,之后才能开始建模。",
}
return translations.get(message, message)
def _fragment_argument(arguments: dict[str, Any]) -> tuple[dict[str, Any], bool]:
"""Return a structured fragment, with a read-only legacy compatibility path.
The public tool schema uses ``fragment`` as an object. Older persisted
tests/tasks may still call the retired ``fragment_json`` string spelling;
accepting it here avoids breaking recovery while keeping it out of the
model-facing contract.
"""
value = arguments.get("fragment")
if isinstance(value, dict):
return deepcopy(value), False
legacy = arguments.get("fragment_json")
if isinstance(legacy, str) and legacy.strip():
try:
parsed = json.loads(legacy)
except json.JSONDecodeError as error:
raise AutonomousGenerationError(
f"FRAGMENT_JSON_INVALID at line {error.lineno}, column {error.colno}: {error.msg}"
) from error
if isinstance(parsed, dict):
return parsed, True
raise AutonomousGenerationError("FRAGMENT_OBJECT_REQUIRED: fragment must be a JSON object")
def _best_effort_fragment(arguments: dict[str, Any]) -> dict[str, Any] | None:
"""Read a fragment for diagnostics without raising a second parse error."""
value = arguments.get("fragment")
if isinstance(value, dict):
return value
legacy = arguments.get("fragment_json")
if isinstance(legacy, str):
try:
value = json.loads(legacy)
except json.JSONDecodeError:
return None
return value if isinstance(value, dict) else None
return None
def _fragment_diagnostic(message: str, *, arguments: dict[str, Any] | None = None) -> dict[str, Any]:
"""Normalize authoring/runtime failures for audit, UI, and future repair agents."""
text = str(message or "")
known_codes = (
"FRAGMENT_JSON_INVALID", "FRAGMENT_OBJECT_REQUIRED", "BATCH_GOAL_REQUIRED",
"BATCH_RELATIONSHIP_REQUIRED", "OPERATION_CONTRACT_REQUIRED",
"OPERATION_CONTRACT_MISMATCH", "CDSL_SCHEMA_INVALID", "CDSL_CANONICAL_FORMAT_REQUIRED",
"CDSL_PROFILE_INVALID", "CDSL_PROFILE_MULTIPLE_OUTERS", "CDSL_PROFILE_INNER_WITHOUT_OUTER", "CDSL_PROFILE_SELF_INTERSECTING", "OPERATION_PARAM_INVALID", "OPERATION_PARAM_UNSUPPORTED",
"Unsupported runtime atomic_id:", "UNSUPPORTED_OPERATION",
"TOPOLOGY_SNAPSHOT_OBSERVATION_REQUIRED", "TOPOLOGY_TOKEN_UNOBSERVED",
"TOPOLOGY_TOKEN_INVALID", "EDGE_SELECTOR_RECOVERY_TOKEN_REQUIRED",
"CANDIDATE_GEOMETRY_UNCHANGED", "CANDIDATE_FRAGMENT_DUPLICATE",
"CANDIDATE_ATTEMPT_LIMIT", "CANDIDATE_REVIEW_FAILED",
"GLOBAL_INVARIANT_VIOLATION", "SOLID_COUNT_MISMATCH", "DIMENSION_OUT_OF_TOLERANCE",
"REVIEW_SERVICE_ERROR", "REVIEW_RESPONSE_INVALID", "REVIEW_RESPONSE_INCOMPLETE",
"ROLLBACK_TARGET_INVALID", "ROLLBACK_TARGET_CURRENT", "ROLLBACK_WOULD_DISCARD_VALID_PROGRESS", "BEST_CHECKPOINT_REQUIRED",
"STALE_EVIDENCE", "EVIDENCE_REVISION_MISMATCH", "TOPOLOGY_TOKEN_STALE",
"NO_PROGRESS_LIMIT", "REPEATED_CDSL_ATTEMPT", "STEP_ATTEMPT_LIMIT", "COMPLETION_ALREADY_REACHED",
"PLAN_REQUIRED", "PLAN_DISABLED", "PLAN_OBJECT_INVALID", "PLAN_REFERENCE_INVALID",
"PLAN_OPERATION_UNSUPPORTED", "PLAN_DEPENDENCY_CYCLE", "PLAN_COVERAGE_INCOMPLETE",
"PLAN_REVIEW_FAILED", "PLAN_REVIEW_REVISE_LIMIT", "PLAN_STEP_REQUIRED",
"PLAN_STEP_MISMATCH", "PLAN_FEATURE_UNPLANNED", "PLAN_PREREQUISITE_UNSATISFIED", "PLAN_INCOMPLETE",
"PLAN_ACTION_REQUIRED", "PLAN_ACTION_MISMATCH", "PLAN_ACTION_ALREADY_COMPLETE", "PLAN_ACTION_UNPLANNED",
"PLAN_ACTION_PREREQUISITE_UNSATISFIED", "PLAN_ACTION_EXECUTION_SHAPE_INVALID",
"PLAN_ACTIONS_REQUIRED", "PLAN_ACTION_GRANULARITY_INVALID",
"PLAN_SKIP_UNAVAILABLE", "PLAN_SKIP_INVALID",
"GEOMETRY_CONCLUSION_NOT_REQUIRED", "GEOMETRY_CONCLUSION_EVIDENCE_REQUIRED",
"GEOMETRY_CONCLUSION_EVIDENCE_DUPLICATE", "GEOMETRY_CONCLUSION_EVIDENCE_UNKNOWN",
"GEOMETRY_CONCLUSION_PLAN_REQUIRED", "GEOMETRY_CONCLUSION_COMPLETE_BLOCKED",
)
code = next((item for item in known_codes if text.startswith(item)), None)
if text.startswith("Unsupported runtime atomic_id:"):
code = "UNSUPPORTED_OPERATION"
if code is None and _is_engine_geometry_failure(text):
code = "ENGINE_GEOMETRY_INVALID"
if code is None:
code = "CDSL_AUTHORING_ERROR"
stage = "planning" if code.startswith("PLAN_") else ("geometry_diagnosis" if code.startswith("GEOMETRY_CONCLUSION_") else ("candidate_review" if code.startswith(("GLOBAL_", "SOLID_", "DIMENSION_", "REVIEW_")) or code == "CANDIDATE_REVIEW_FAILED" else ("candidate_build" if code.startswith("CANDIDATE_") or code == "ENGINE_GEOMETRY_INVALID" else "fragment_validation")))
path = "fragment"
if code == "BATCH_GOAL_REQUIRED":
path = "batch_goal"
elif code == "BATCH_RELATIONSHIP_REQUIRED":
path = "batch_relationship"
elif code in {"OPERATION_CONTRACT_REQUIRED", "OPERATION_CONTRACT_MISMATCH"}:
path = "fragment.feature.atomic_id"
elif code in {"CDSL_SCHEMA_INVALID", "CDSL_CANONICAL_FORMAT_REQUIRED", "CDSL_PROFILE_INVALID", "CDSL_PROFILE_MULTIPLE_OUTERS", "CDSL_PROFILE_INNER_WITHOUT_OUTER", "CDSL_PROFILE_SELF_INTERSECTING", "OPERATION_PARAM_INVALID", "OPERATION_PARAM_UNSUPPORTED"}:
match = re.search(r"(?:at )?(fragment(?:\.[A-Za-z0-9_\[\].]+)?)", text)
path = match.group(1) if match else "fragment"
elif code.startswith("TOPOLOGY_") or code == "EDGE_SELECTOR_RECOVERY_TOKEN_REQUIRED":
path = "fragment.selector_tokens"
elif code.startswith("PLAN_ACTION"):
path = "plan_action_id"
elif code.startswith("PLAN_STEP"):
path = "plan_step_id"
elif code.startswith("PLAN_"):
path = "modeling_plan"
elif code.startswith("GEOMETRY_CONCLUSION_EVIDENCE"):
path = "evidence_refs"
elif code.startswith("GEOMETRY_CONCLUSION_"):
path = "decision"
evidence: dict[str, Any] = {}
if isinstance(arguments, dict):
value = arguments.get("fragment")
if isinstance(value, dict):
evidence["feature_count"] = _fragment_feature_count(value)
evidence["atomic_ids"] = _fragment_atomic_ids(value)
elif isinstance(arguments.get("fragment_json"), str):
evidence["legacy_fragment_json"] = True
return {
"schema_version": "cad.cdsl-diagnostic.v1",
"code": code,
"stage": stage,
"path": path,
"message": text,
"evidence": evidence,
}
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]
_PLAN_MARKER = re.compile(r"^\s*\[(STRUCTURE|FEATURE|STEP|ACTION)\s+([A-Za-z0-9][A-Za-z0-9_.-]*)\]\s*$", re.IGNORECASE)
_PLAN_KEY = re.compile(r"^\s*([a-z_][a-z0-9_]*)\s*:\s*(.*?)\s*$", re.IGNORECASE)
def _plan_value_list(value: Any) -> list[str]:
if isinstance(value, list):
return [str(item).strip() for item in value if str(item).strip()]
text = str(value or "").strip()
if not text or text.casefold() in {"none", "null", "-"}:
return []
# This helper is only for optional legacy metadata. Accept common prose
# punctuation and JSON-like arrays without making any of them mandatory.
if text.startswith("[") and text.endswith("]"):
try:
parsed = json.loads(text)
except (TypeError, json.JSONDecodeError):
parsed = None
if isinstance(parsed, list):
return [str(item).strip() for item in parsed if str(item).strip()]
text = text[1:-1]
return [item.strip().strip('"\'') for item in re.split(r"[,;,;]", text) if item.strip().strip('"\'')]
def _plan_bool(value: Any, *, field: str) -> bool:
text = str(value or "").strip().casefold()
if text in {"yes", "true", "1"}:
return True
if text in {"no", "false", "0"}:
return False
raise AutonomousGenerationError(f"PLAN_OBJECT_INVALID: {field} must be yes or no")
def _looks_like_operation_id(value: str) -> bool:
"""Recognize capability-shaped names without treating ordinary prose as IDs."""
token = str(value or "").strip().casefold()
if not token or "_" not in token:
return token in {"fillet", "chamfer"}
prefixes = ("extrude_", "revolve_", "hole_", "pattern_", "sphere_", "reference_", "cylinder_", "sweep_")
suffixes = ("_add", "_cut", "_blind", "_wizard", "_linear", "_circular", "_mirror")
return token.startswith(prefixes) or token.endswith(suffixes)
def _public_modeling_plan_review(review: dict[str, Any]) -> dict[str, Any]:
"""Remove misleading unsupported operation names before author exposure.
The raw reviewer response remains persisted for audit/debugging. The
author only needs the actionable fact that the plan must be revised; a
copied unsupported identifier would otherwise become the next tool call.
"""
public = deepcopy(review)
unsupported = [str(item) for item in public.pop("unsupported_operations", []) if str(item)]
if not unsupported:
return public
def scrub(value: Any) -> Any:
if isinstance(value, str):
result = value
for name in unsupported:
result = re.sub(rf"(?<![A-Za-z0-9_]){re.escape(name)}(?![A-Za-z0-9_])", "unsupported_runtime_operation", result)
return result
if isinstance(value, list):
return [scrub(item) for item in value]
if isinstance(value, dict):
return {key: scrub(item) for key, item in value.items()}
return value
return scrub(public)
def parse_modeling_plan(
markdown: str,
*,
checklist: list[str],
runtime_atomic_ids: set[str] | None = None,
) -> dict[str, Any]:
"""Keep the modeling plan as semantic context, not as a second DSL.
The plan is deliberately tolerant: the author may write ordinary Markdown
or prose and may optionally add ``## STEP ...`` headings. Legacy
``[STRUCTURE]``/``[FEATURE]``/``[STEP]`` records are extracted when they
are easy to recognise, but malformed fields are retained as text instead
of blocking the CAD loop. Runtime validation remains the authority for
actual CDSL and geometry.
"""
text = str(markdown or "").strip()
if not text:
raise AutonomousGenerationError("PLAN_OBJECT_INVALID: plan_text must not be empty")
# Extract optional legacy records without imposing their field grammar.
records: dict[str, list[dict[str, Any]]] = {"structure": [], "feature": [], "step": [], "action": []}
current: dict[str, Any] | None = None
pending_key = ""
for raw_line in text.splitlines():
line = raw_line.strip()
marker = _PLAN_MARKER.match(line)
if marker:
kind, identifier = marker.groups()
normalized = kind.casefold()
current = {"id": identifier, "_kind": normalized}
records[normalized].append(current)
pending_key = ""
continue
if current is None:
continue
list_match = re.match(r"^-\s+(.+?)\s*$", line)
if list_match and pending_key:
current.setdefault(pending_key, []).append(list_match.group(1).strip())
continue
key_match = _PLAN_KEY.match(line)
if key_match:
key, value = key_match.groups()
current[key] = value.strip() if value.strip() else []
pending_key = key if not value.strip() else ""
structures = []
for record in records["structure"]:
structure_id = str(record.get("id") or "").strip()
if not structure_id:
continue
structures.append({
"structure_id": structure_id,
"role": str(record.get("role") or record.get("name") or record.get("purpose") or "").strip(),
"requirements": _plan_value_list(record.get("requirements") or record.get("checklist_coverage")),
"depends_on": _plan_value_list(record.get("depends_on")),
})
features = []
for record in records["feature"]:
feature_id = str(record.get("id") or "").strip()
if not feature_id:
continue
operation = str(record.get("operation") or "").strip()
# An explicitly declared feature operation is the one place where a
# semantic plan is allowed to name a machine capability. Keep prose
# tolerant, but reject a CAD-looking identifier that is not registered
# by the installed Runtime before it can reach the author context.
if runtime_atomic_ids and operation:
operation_token = re.match(r"^\s*([A-Za-z][A-Za-z0-9_]*)\b", operation)
candidate = operation_token.group(1) if operation_token else ""
if (
candidate
and candidate != "sketch_profile"
and candidate not in runtime_atomic_ids
and _looks_like_operation_id(candidate)
):
supported = ", ".join(sorted(runtime_atomic_ids))
raise AutonomousGenerationError(
f"PLAN_OPERATION_UNSUPPORTED: feature {feature_id} references unsupported operation "
f"{candidate}; use semantic intent or one of: {supported}"
)
features.append({
"feature_id": feature_id,
"structure_id": str(record.get("structure") or "").strip(),
"operation": operation,
"purpose": str(record.get("purpose") or "").strip(),
"depends_on": _plan_value_list(record.get("depends_on")),
"topology_sensitive": str(record.get("topology_sensitive") or "").strip().casefold() in {"yes", "true", "1"},
"evidence": _plan_value_list(record.get("evidence") or record.get("evidence_needed")),
})
# Also catch the same explicit labels when the author uses ordinary
# Markdown rather than the legacy [FEATURE] records. Free-form prose is
# still accepted; only operation:/atomic_id: declarations are checked.
if runtime_atomic_ids:
declared_operations = re.findall(
r"(?im)^\s*(?:[-*]\s*)?(?:operation|atomic_id)\s*[:=]\s*([A-Za-z][A-Za-z0-9_]*)\b",
text,
)
for candidate in declared_operations:
if candidate == "sketch_profile" or candidate in runtime_atomic_ids or not _looks_like_operation_id(candidate):
continue
supported = ", ".join(sorted(runtime_atomic_ids))
raise AutonomousGenerationError(
f"PLAN_OPERATION_UNSUPPORTED: declared operation {candidate} is not supported by Runtime; "
f"use semantic intent or one of: {supported}"
)
known_step_ids: set[str] = set()
saw_step_heading = False
steps = []
legacy_steps = records["step"]
if legacy_steps:
for record in legacy_steps:
step_id = str(record.get("id") or "").strip()
if step_id and step_id not in known_step_ids:
known_step_ids.add(step_id)
steps.append({
"step_id": step_id,
"title": str(record.get("goal") or "").strip(),
"goal": str(record.get("goal") or "").strip(),
"feature_ids": [item for item in _plan_value_list(record.get("features")) if item in {f["feature_id"] for f in features}],
"covers": [], "relationship": str(record.get("relationship") or "").strip(),
"prerequisites": _plan_value_list(record.get("prerequisites")), "expected_state_change": str(record.get("expected_state_change") or "").strip(),
"evidence": _plan_value_list(record.get("evidence") or record.get("evidence_needed")), "status": "pending",
})
else:
# Optional Markdown step headings. Any prose before the first heading
# stays with the first step so no author content is discarded.
lines = text.splitlines()
heading = re.compile(r"^\s{0,3}#{1,6}\s*(?:(?:step|步骤)\s*)?(?:第\s*)?(\d+)?\s*(?:步)?\s*[:.-]?\s*(.*?)\s*$", re.IGNORECASE)
sections: list[tuple[str, list[str]]] = []
saw_step_heading = False
current_title = "整体建模计划"
current_lines: list[str] = []
for line in lines:
match = heading.match(line)
if match and (match.group(1) or re.search(r"\bstep\b|步骤|第\s*\d+\s*步", line, re.IGNORECASE)):
saw_step_heading = True
if current_lines or not sections:
sections.append((current_title, current_lines))
current_title = (match.group(2) or f"步骤 {len(sections) + 1}").strip()
current_lines = []
else:
current_lines.append(line)
sections.append((current_title, current_lines))
sections = [(title, body) for title, body in sections if "\n".join(body).strip() or len(sections) == 1]
for index, (title, body) in enumerate(sections, 1):
step_id = f"step_{index}"
known_step_ids.add(step_id)
body_text = "\n".join(body).strip()
steps.append({
"step_id": step_id, "title": title or f"步骤 {index}", "goal": title or f"步骤 {index}",
"feature_ids": [], "covers": [], "relationship": "", "prerequisites": [],
"expected_state_change": body_text[:500], "body": body_text, "evidence": [], "status": "pending",
})
if not saw_step_heading and len(steps) == 1:
steps[0]["step_id"] = "semantic_plan"
mode = "indexed" if saw_step_heading or len(steps) > 1 or legacy_steps or records["structure"] or records["feature"] or records["action"] else "semantic_only"
if not steps:
steps = [{"step_id": "semantic_plan", "title": "整体建模计划", "goal": "整体建模计划", "feature_ids": [], "covers": [], "relationship": "", "prerequisites": [], "expected_state_change": text[:500], "body": text, "evidence": [], "status": "pending"}]
# A semantic step may contain several related actions (for example the
# several related targets). Each action is still submitted as one
# independent fragment. Explicit ACTION records are preferred; when a
# legacy feature graph is present, features become one action each; pure
# prose receives one compatible default action per step.
feature_by_id = {str(item.get("feature_id") or ""): item for item in features if isinstance(item, dict)}
actions: list[dict[str, Any]] = []
explicit_actions = records.get("action") or []
seen_action_ids: set[str] = set()
for record in explicit_actions:
action_id = str(record.get("id") or "").strip()
if not action_id:
continue
if action_id in seen_action_ids:
raise AutonomousGenerationError(f"PLAN_REFERENCE_INVALID: duplicate action id {action_id}")
seen_action_ids.add(action_id)
step_id = str(record.get("step") or record.get("step_id") or "").strip()
operation = str(record.get("operation") or record.get("atomic_id") or "").strip()
if runtime_atomic_ids and operation:
candidate = re.match(r"^\s*([A-Za-z][A-Za-z0-9_]*)\b", operation)
token = candidate.group(1) if candidate else ""
if token and token != "sketch_profile" and token not in runtime_atomic_ids and _looks_like_operation_id(token):
supported = ", ".join(sorted(runtime_atomic_ids))
raise AutonomousGenerationError(
f"PLAN_OPERATION_UNSUPPORTED: action {action_id} references unsupported operation {token}; "
f"use semantic intent or one of: {supported}"
)
required_raw = str(record.get("required") or "true").strip().casefold()
required = required_raw not in {"false", "no", "0", "optional"}
action = {
"action_id": action_id,
"step_id": step_id,
"title": str(record.get("title") or record.get("goal") or record.get("purpose") or action_id).strip(),
"target": str(record.get("target") or "").strip(),
"operation": operation,
"required": required,
"depends_on": _plan_value_list(record.get("depends_on") or record.get("prerequisites")),
"feature_id": str(record.get("feature_id") or "").strip(),
"relationship": str(record.get("relationship") or "").strip(),
"source": "explicit",
"status": "pending",
}
actions.append(action)
step_ids = {str(item.get("step_id") or "") for item in steps if isinstance(item, dict)}
for action in actions:
if action["step_id"] not in step_ids:
if action["step_id"]:
raise AutonomousGenerationError(
f"PLAN_REFERENCE_INVALID: action {action.get('action_id')} references unknown step {action['step_id']}"
)
action["step_id"] = "semantic_plan" if "semantic_plan" in step_ids else (next(iter(step_ids), ""))
if not actions:
for step in steps:
step_id = str(step.get("step_id") or "")
feature_ids = [str(value) for value in step.get("feature_ids") or () if str(value)]
if feature_ids:
for feature_id in feature_ids:
feature = feature_by_id.get(feature_id) or {}
actions.append({
"action_id": feature_id,
"step_id": step_id,
"title": str(feature.get("purpose") or feature_id),
"target": str(feature.get("structure_id") or ""),
"operation": str(feature.get("operation") or ""),
"required": True,
"depends_on": list(feature.get("depends_on") or []),
"feature_id": feature_id,
"relationship": str(step.get("relationship") or ""),
"source": "feature",
"status": "pending",
})
else:
body = str(step.get("body") or step.get("expected_state_change") or "")
soft_actions = re.findall(r"(?im)^\s{0,6}#{1,6}\s*(?:action|动作)\s*(?:\d+[a-z]?\s*[-:.)]?\s*)?(.*?)\s*$", body)
soft_actions = [item.strip() for item in soft_actions if item.strip()]
if not soft_actions:
soft_actions = [item.strip() for item in re.findall(r"(?im)^\s*[-*]\s*(?:action|动作)\s*[::]\s*(.+?)\s*$", body) if item.strip()]
titles = soft_actions or [str(step.get("goal") or step.get("title") or step_id)]
for action_index, title in enumerate(titles, 1):
actions.append({
"action_id": f"{step_id}_action_{action_index}",
"step_id": step_id,
"title": title,
"target": "",
"operation": "",
"required": True,
"depends_on": [f"{step_id}_action_{action_index - 1}"] if action_index > 1 else [],
"feature_id": "",
"relationship": str(step.get("relationship") or ""),
"source": "heading" if soft_actions else "default",
"status": "pending",
})
else:
# Mixed plans may explicitly describe actions for only some steps.
# Keep every step executable by adding a prose-compatible default to
# the remaining steps; this does not infer geometry.
covered_steps = {str(item.get("step_id") or "") for item in actions}
for step in steps:
step_id = str(step.get("step_id") or "")
if step_id in covered_steps:
continue
actions.append({
"action_id": f"{step_id}_action_1",
"step_id": step_id,
"title": str(step.get("goal") or step.get("title") or step_id),
"target": "",
"operation": "",
"required": True,
"depends_on": [],
"feature_id": "",
"relationship": str(step.get("relationship") or ""),
"source": "default",
"status": "pending",
})
actions_by_step: dict[str, list[dict[str, Any]]] = {}
unique_action_ids: set[str] = set()
for action in actions:
action_id = str(action.get("action_id") or "")
if action_id and action_id in unique_action_ids:
raise AutonomousGenerationError(f"PLAN_REFERENCE_INVALID: duplicate action id {action_id}")
if action_id:
unique_action_ids.add(action_id)
actions_by_step.setdefault(str(action.get("step_id") or ""), []).append(action)
action_ids = {str(item.get("action_id") or "") for item in actions if str(item.get("action_id") or "")}
for action in actions:
unknown = [item for item in action.get("depends_on") or () if str(item) not in action_ids]
if unknown:
raise AutonomousGenerationError(
f"PLAN_REFERENCE_INVALID: action {action.get('action_id')} depends on unknown action(s): {', '.join(unknown)}"
)
visiting: set[str] = set()
visited: set[str] = set()
by_action_id = {str(item.get("action_id") or ""): item for item in actions}
def visit(action_id: str) -> None:
if action_id in visiting:
raise AutonomousGenerationError(f"PLAN_DEPENDENCY_CYCLE: action dependency cycle includes {action_id}")
if action_id in visited:
return
visiting.add(action_id)
for dependency in by_action_id[action_id].get("depends_on") or ():
visit(str(dependency))
visiting.remove(action_id)
visited.add(action_id)
for action_id in action_ids:
visit(action_id)
for step in steps:
step["actions"] = deepcopy(actions_by_step.get(str(step.get("step_id") or ""), []))
step["action_ids"] = [str(item.get("action_id") or "") for item in step["actions"] if str(item.get("action_id") or "")]
return {
"schema_version": "cad.modeling-plan.v1",
"plan_version": 1,
"mode": mode,
"plan_text": text,
"structures": structures,
"features": features,
"actions": actions,
"steps": steps,
}
def _format_correction_card(
engine: Any,
*,
fragment_json: str = "",
fragment: dict[str, Any] | None = None,
error_message: str,
head: str,
shared_revolve_axis: dict[str, Any] | None = None,
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 = (
json.dumps(fragment, ensure_ascii=True, sort_keys=True)
if isinstance(fragment, dict)
else str(fragment_json or "")
)
atomic_id = ""
attempted_params: dict[str, Any] = {}
attempted_feature_count = 0
try:
parsed_fragment: Any = fragment if isinstance(fragment, dict) else json.loads(raw)
feature = parsed_fragment.get("feature") if isinstance(parsed_fragment, dict) else None
if not isinstance(feature, dict) and isinstance(parsed_fragment, dict):
features = parsed_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}
),
})
if atomic_id.startswith("revolve_") and attempted_params.get("angle_deg") is None:
shared_axis_note = (
"shared_revolve_axis was supplied and fills only params.axis; it never supplies params.angle_deg."
if isinstance(shared_revolve_axis, dict)
else "A batch revolve_axis fills only params.axis; it never supplies params.angle_deg."
)
card.update({
"missing_author_params": ["angle_deg"],
"shared_revolve_axis_note": shared_axis_note,
"instruction": (
f"{atomic_id} requires params.angle_deg. Add an explicit positive degree value (360 for a full revolution) "
"to this replacement fragment. Do not infer or omit it; partial revolutions are valid. "
+ shared_axis_note
),
})
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"]
if atomic_id.startswith("hole_") or atomic_id == "hole_wizard":
# Holes have a server-injected face and a strict position object
# shape. Preserve every reported issue together so one retry can
# correct the token, required fields, and unsupported aliases.
issue_text = error_message.partition(":")[2] if error_message.startswith("HOLE_FRAGMENT_INVALID:") else error_message
issues = [item.strip() for item in issue_text.split(";") if item.strip()]
author_allowed = set(author_required) | (set(optional) - set(operation["server_injected_params"]))
card.update({
"issues": issues,
"server_owned_attempted_params": sorted(
key for key in attempted_params if key in operation["server_injected_params"]
),
"unsupported_attempted_params": sorted(key for key in attempted_params if key not in author_allowed),
"minimal_params_shape": {
"diameter_mm": "positive number",
"depth_mm": "positive number",
"positions": [{"mm": "[x_mm, y_mm, z_mm]"}],
},
"valid_feature_shape": {
"atomic_id": atomic_id,
"params": {
"diameter_mm": "positive number",
"depth_mm": "positive number",
"positions": [{"mm": "[x_mm, y_mm, z_mm]"}],
},
"selector_tokens": ["exactly one current face token"],
},
"selector_tokens_required": True,
"selector_token_kind": "face",
"instruction": (
"Replace the hole fragment with exactly one current face token and only the listed hole params. "
"Use positions as [{\"mm\":[x_mm,y_mm,z_mm]}]; host_face is injected by the server. "
"Correct every item in issues in the same replacement."
),
})
if "analytic_contours" in raw and "not valid under any of the given schemas" in error_message:
card.update({
"profile_correction": {
"valid_profile_shape": {
"type": "analytic_contours",
"contours": [{
"role": "outer or inner",
"closed": True,
"segments": [{"type": "circle", "center": "[u_mm, v_mm]", "radius_mm": "positive number"}],
}],
},
"rules": [
"Each contour needs role, closed, and segments; a contour cannot be a bare {type: circle, center, radius_mm} object.",
"Use one outer contour per separate cut island. Use an outer plus inner contour only for a concentric annulus.",
],
},
"instruction": (
"Correct the analytic_contours profile using profile_correction.valid_profile_shape. "
"Do not submit bare circle objects in contours."
),
})
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 _is_engine_geometry_failure(message: str) -> bool:
"""Recognize runtime BRep/export failures that are not authoring syntax."""
lowered = str(message or "").lower()
return any(marker in lowered for marker in (
"step tessellation produced no renderable triangles",
"nbnodes",
"brep_api: command not done",
"command not done",
"boolean operation failed",
"boolean operation could not",
"fillet_radius_unavailable",
"fillet geometry invalid",
"chamfer geometry invalid",
"brep geometry failure",
"engine returned an invalid model bounding box",
"engine returned a non-positive model volume",
"engine returned no valid solid body",
"engine did not produce a valid step artifact",
"step preview conversion did not produce a valid glb artifact",
"topods::",
"topods_",
"brep",
"brepbuilderapi",
"cannot build face",
"invalid face",
"invalid wire",
"self-intersection",
"self intersection",
"non-manifold",
))
def _fragment_selector_tokens(fragment: dict[str, Any]) -> list[str]:
"""Read a fragment's authored selector tokens without changing it."""
values: list[str] = []
def append_tokens(raw: Any) -> None:
if isinstance(raw, list):
values.extend(item for item in raw if isinstance(item, str))
append_tokens(fragment.get("selector_tokens"))
feature = fragment.get("feature")
if isinstance(feature, dict):
append_tokens(feature.get("selector_tokens"))
features = fragment.get("features", fragment.get("add_features"))
if isinstance(features, list):
for item in features:
if not isinstance(item, dict):
continue
nested = item.get("feature")
append_tokens(nested.get("selector_tokens") if isinstance(nested, dict) else item.get("selector_tokens"))
return list(dict.fromkeys(values))
def _fragment_atomic_ids(fragment: dict[str, Any]) -> list[str]:
"""Return authored feature operations in fragment order without rewriting it."""
candidates: list[dict[str, Any]] = []
feature = fragment.get("feature")
if isinstance(feature, dict):
candidates.append(feature)
features = fragment.get("features", fragment.get("add_features"))
if isinstance(features, list):
for item in features:
if not isinstance(item, dict):
continue
nested = item.get("feature")
candidates.append(nested if isinstance(nested, dict) else item)
result: list[str] = []
for feature in candidates:
atomic_id = str(feature.get("atomic_id") or "").strip()
if atomic_id and atomic_id not in result:
result.append(atomic_id)
return result
def _fragment_feature_count(fragment: dict[str, Any]) -> int:
"""Count authored features without deduplicating repeated atomic IDs."""
feature = fragment.get("feature")
if isinstance(feature, dict):
return 1
features = fragment.get("features", fragment.get("add_features"))
return len(features) if isinstance(features, list) else 0
def _unread_operation_contract_ids(
state: dict[str, Any],
fragment: dict[str, Any],
base_cdsl: dict[str, Any] | None,
) -> list[str]:
"""Require a contract lookup before a task first authors a new operation."""
read_ids = {
str(item)
for item in state.get("read_operation_contract_ids") or ()
if isinstance(item, str) and str(item)
}
existing_ids = {
str(feature.get("atomic_id") or "")
for feature in ((base_cdsl or {}).get("features") or ())
if isinstance(feature, dict) and str(feature.get("atomic_id") or "")
}
return [atomic_id for atomic_id in _fragment_atomic_ids(fragment) if atomic_id not in existing_ids and atomic_id not in read_ids]
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": "write_modeling_plan",
"description": "Write a semantic modeling plan after completion.md and before CAD observation or CDSL. Use ordinary Markdown or prose with ## Step headings. When one step contains independently located profiles or Runtime features, keep the related targets in that step but add one ### Action heading per target. One action must be executable as one Runtime feature. A supported multi-position hole or pattern may remain one action when it shares one host and operation contract. No JSON is required. This is context for another LLM, not CDSL.",
"parameters": {
"type": "object",
"properties": {"plan_text": {"type": "string", "minLength": 40, "maxLength": 16000}},
"required": ["plan_text"],
"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 exactly one operation for the active plan action as a structured CDSL fragment object. A semantic step may contain multiple related actions, but each action requires its own fragment and checkpoint. The active operation contract and dynamic tool schema are authoritative. Legacy aliases, feature arrays, batch_relationship, shared_revolve_axis, ids, dependencies, sketch_id, and raw selectors are invalid.",
"parameters": {"type": "object", "properties": {"plan_step_id": {"type": "string", "minLength": 1}, "plan_action_id": {"type": "string", "minLength": 1}, "batch_goal": {"type": "string", "minLength": 8, "maxLength": 800}, "batch_relationship": {"type": "string", "minLength": 12, "maxLength": 1000}, "fragment": {"type": "object", "minProperties": 1}, "shared_revolve_axis": {"type": "object", "properties": {"origin_mm": point3, "direction": point3}, "required": ["origin_mm", "direction"], "additionalProperties": False}}, "required": ["batch_goal", "fragment"], "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, skip, or complete. Choose skip only when current evidence proves this plan step was already satisfied by an earlier feature; skip advances the plan without writing CDSL. For modify, supply optimization_plan.action with the next materially different modelling action, for example {\"optimization_plan\":{\"action\":\"move the hole cut to the observed top face\"}}. next_action is accepted as a legacy spelling. 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", "skip", "complete"]},
"optimization_plan": {
"type": "object",
"properties": {
"action": {"type": "string", "minLength": 1},
"next_action": {"type": "string", "minLength": 1},
"reason": {"type": "string", "minLength": 1},
},
"anyOf": [{"required": ["action"]}, {"required": ["next_action"]}, {"required": ["reason"]}],
"additionalProperties": True,
},
},
"required": ["root_cause", "evidence_refs", "decision"],
"allOf": [{
"if": {"properties": {"decision": {"const": "modify"}}, "required": ["decision"]},
"then": {"required": ["optimization_plan"]},
}],
"additionalProperties": False,
},
},
},
{
"type": "function",
"function": {
"name": "skip_satisfied_plan_step",
"description": "Advance the active modeling-plan step without CDSL only when the server has already verified every frozen completion item at the current revision. This tool is exposed only while that proof is current.",
"parameters": {
"type": "object",
"properties": {
"reason": {"type": "string", "minLength": 1, "maxLength": 800},
},
"required": ["reason"],
"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 _planning_reference_context(settings: Settings, requirements: str) -> dict[str, Any]:
"""Return a bounded, advisory skill/library context for plan authoring."""
skill_root = settings.engine_root.parent.parent / "agent" / "skills" / "cad-engine" / "references" / "part-skills"
catalog = read_json(skill_root / "catalog.json", {})
text = str(requirements or "").casefold()
matched: list[dict[str, Any]] = []
for item in ((catalog.get("skills") or ()) if isinstance(catalog, dict) else ()):
if not isinstance(item, dict):
continue
triggers = [str(value).casefold() for value in item.get("triggers") or () if str(value)]
excludes = [str(value).casefold() for value in item.get("exclude") or () if str(value)]
if not any(trigger in text for trigger in triggers) or any(value in text for value in excludes):
continue
matched.append(item)
matched.sort(key=lambda item: (-int(item.get("priority") or 0), str(item.get("id") or "")))
selected: list[dict[str, Any]] = []
planning_count = 0
support_count = 0
for item in matched:
kind = str(item.get("kind") or "")
if kind == "planning":
if planning_count >= 1:
continue
planning_count += 1
else:
if support_count >= 3:
continue
support_count += 1
bridge = skill_root / str(item.get("bridge") or "")
selected.append({
"id": str(item.get("id") or ""),
"kind": kind,
"title": str(item.get("title") or ""),
"guidance": bridge.read_text(encoding="utf-8")[:2500] if bridge.is_file() else "",
})
try:
samples = CdslLibrary(settings).search(requirements, limit=3)
except (OSError, ValueError, json.JSONDecodeError):
samples = []
return {
"precedence": "Runtime operation contracts and user requirements override every skill and library example.",
"skills": selected,
"library_examples": samples,
"library_rule": "Examples are expression-pattern references only. Never copy their CDSL ids or treat them as executable requirements.",
}
def _operation_authoring_rule(atomic_id: str, selector_rule: dict[str, Any] | None) -> str:
"""Give the author a usable fragment shape, not just a parameter list."""
if atomic_id.startswith("revolve_"):
return (
"Use {sketch:{workplane,profile}, feature:{atomic_id,params}}. "
"params must contain explicit angle_deg (degrees, not radians) and axis "
"{origin_mm:[x,y,z], direction:[dx,dy,dz]}. Shared axes and top-level axis fields are not accepted. "
"The axis direction must be parallel to the sketch plane and its origin must lie in that plane. "
"Never use selector_tokens for an axis."
)
if atomic_id.startswith("extrude_"):
return (
"Use {sketch:{workplane,profile}, feature:{atomic_id,params}}. "
"Put only distance_mm (and optional reverse) in params. Extrusion "
"direction comes from sketch.workplane.normal; do not put workplane, "
"profile, or direction in params."
)
if atomic_id == "sphere_add":
return "Use {feature:{atomic_id:'sphere_add',params:{radius_mm,center_mm}}}; sphere_add has no sketch."
if atomic_id.startswith("hole_") or atomic_id == "hole_wizard":
return (
"Use a sketchless feature. First inspect_topology(kind='face'), then place "
"exactly one returned face token in selector_tokens. positions uses "
"[{\"mm\":[x_mm,y_mm,z_mm]}]. The server injects host_face through "
"server_injected_params. One hole feature cannot span different host faces."
)
if selector_rule:
return (
"Use a sketchless feature. First inspect_topology for the required token kind, "
"then put only returned opaque tokens in selector_tokens."
)
return "Use only the listed params. Do not add selector_tokens or a sketch unless requires_sketch is true."
def _operation_contract_payload(engine: Any, atomic_id: str) -> dict[str, Any]:
"""Return runtime documentation and a schema-valid authoring template."""
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]
payload = {
"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": _operation_authoring_rule(atomic_id, selector_rule),
}
canonical_schema = build_operation_fragment_schema(engine, atomic_id)
payload.update({
"canonical_fragment_schema": canonical_schema,
"canonical_fragment_example": canonical_fragment_example(engine, atomic_id),
"canonical_example_note": "Example is schema-only. Replace every dimension, coordinate, and selector token with values from current requirements/topology; placeholder token strings are not executable.",
"contract_hash": operation_contract_hash(atomic_id, canonical_schema),
"canonical_only": True,
"legacy_aliases_allowed": False,
})
if atomic_id.startswith("revolve_"):
sketch = {
"workplane": {
"origin_mm": [0, 0, 0],
"x_dir": [1, 0, 0],
"normal": [0, -1, 0],
},
"profile": {
"type": "polygon",
"vertices": [[10, -5], [20, -5], [20, 5], [10, 5]],
},
}
axis = {"origin_mm": [0, 0, 0], "direction": [0, 0, 1]}
payload["fragment_template"] = {
"sketch": sketch,
"feature": {
"atomic_id": atomic_id,
"params": {"angle_deg": 360, "axis": axis},
},
}
return payload
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 _rollback_token_details(task: dict[str, Any]) -> list[dict[str, Any]]:
"""Describe rollback handles without making the current head selectable."""
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)
}
current = str(task.get("active_revision") or "")
details: list[dict[str, Any]] = [{
"token": "root",
"revision_id": "",
"current": not bool(current),
"rollback_allowed": bool(current),
"recommended_for_rollback": False,
"parent_token": None,
}]
pointer = current
child = ""
while pointer:
record = revisions.get(pointer) or {}
parent = str(record.get("parent_revision_id") or "")
details.append({
"token": _checkpoint_token(branch_id, pointer),
"revision_id": pointer,
"current": pointer == current,
"rollback_allowed": pointer != current,
"recommended_for_rollback": bool(child and pointer == parent),
"parent_token": _checkpoint_token(branch_id, parent) if parent else "root",
"quality_score": record.get("quality_score"),
"plan_step_id": record.get("plan_step_id") or "",
"plan_action_id": record.get("plan_action_id") or "",
"completed_plan_actions": list(record.get("completed_plan_actions") or []),
})
child = pointer
pointer = parent
# The immediate parent is the least destructive default target.
if details and current:
parent_revision = str((revisions.get(current) or {}).get("parent_revision_id") or "")
for item in details:
if str(item.get("revision_id") or "") == parent_revision:
item["recommended_for_rollback"] = True
return details
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 _global_geometry_violations(
health: dict[str, Any],
*,
requirements: str,
checklist: list[str],
check_dimensions: bool = True,
) -> list[dict[str, Any]]:
"""Return cheap, observable blockers that apply to every candidate.
This deliberately does not infer CAD operations. It only protects
invariants that are already observable in the deterministic rebuild
report, so a local batch reviewer cannot accept a globally invalid model.
"""
violations: list[dict[str, Any]] = []
solid_count = health.get("solid_count")
requires_single = any(
marker in (requirements + "\n" + "\n".join(checklist))
for marker in ("单个机械零件", "单一实体", "所有主要实体相互连接", "one coherent solid", "single connected")
)
if requires_single and isinstance(solid_count, (int, float)) and int(solid_count) != 1:
violations.append({
"code": "SOLID_COUNT_MISMATCH",
"message": f"要求单一连通实体,但当前 solid_count={int(solid_count)}",
"evidence": {"solid_count": int(solid_count)},
})
dimensions = ((health.get("bbox_mm") or {}).get("dimensions") if isinstance(health.get("bbox_mm"), dict) else None) or []
# Frozen image-derived requirements commonly state the expected total
# height as "总高度约 82 mm". Apply a conservative absolute tolerance;
# absent that explicit assumption, do not invent a dimension constraint.
match = re.search(r"总高度[^0-9]{0,16}(\d+(?:\.\d+)?)[^0-9]{0,8}mm", requirements or "", re.IGNORECASE) if check_dimensions else None
if match and len(dimensions) >= 3:
expected = float(match.group(1))
actual = float(dimensions[2])
tolerance = max(2.0, expected * 0.05)
if abs(actual - expected) > tolerance:
violations.append({
"code": "DIMENSION_OUT_OF_TOLERANCE",
"message": f"模型总高度 {actual:.3f} mm 超出冻结值 {expected:.3f} mm ± {tolerance:.3f} mm",
"evidence": {"axis": "Z", "actual_mm": actual, "expected_mm": expected, "tolerance_mm": tolerance},
})
return violations
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 [],
"compatibility_fix_count": int(fragment_audit.get("compatibility_fix_count") or 0),
"legacy_input": bool(fragment_audit.get("legacy_input")),
}
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,
"plan_step_id": str(candidate.get("plan_step_id") or ""),
"plan_step_goal": str(candidate.get("plan_step_goal") or ""),
"plan_action_id": str(candidate.get("plan_action_id") or ""),
"plan_action_title": str(candidate.get("plan_action_title") or ""),
"completed_plan_actions": list(candidate.get("completed_plan_actions") or []),
"atomic_ids": candidate.get("atomic_ids") or [],
"legacy_input": bool(candidate.get("legacy_input")),
"compatibility_fix_count": int(candidate.get("compatibility_fix_count") or 0),
# 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, on_text_delta: Callable[[str], Awaitable[None]] | None = None) -> None:
self.settings = settings
self.store = store
self.complete = complete
self.on_text_delta = on_text_delta
self.tools = autonomous_tools()
@staticmethod
def _clear_active_operation_contract(state: dict[str, Any]) -> None:
state["active_operation_contract"] = {}
state["pending_operation"] = ""
state["pending_operation_contract_hash"] = ""
state["pending_operation_revision"] = ""
def _active_contract(self, task: dict[str, Any], state: dict[str, Any] | None, engine: Any | None = None) -> dict[str, Any] | None:
contract = (state or {}).get("active_operation_contract")
if not isinstance(contract, dict) or not str(contract.get("atomic_id") or ""):
return None
if str((state or {}).get("pending_operation_revision") or "") != str(task.get("active_revision") or ""):
return None
schema = contract.get("canonical_fragment_schema")
atomic_id = str(contract.get("atomic_id") or "")
if not isinstance(schema, dict):
return None
expected_hash = operation_contract_hash(atomic_id, schema)
if str((state or {}).get("pending_operation_contract_hash") or "") != expected_hash:
return None
if engine is not None:
try:
runtime_schema = build_operation_fragment_schema(engine, atomic_id)
except (ValueError, KeyError):
return None
if operation_contract_hash(atomic_id, runtime_schema) != expected_hash:
return None
return contract
def _canonical_submit_tool(self, task: dict[str, Any], state: dict[str, Any] | None) -> dict[str, Any] | None:
contract = self._active_contract(task, state)
if contract is None:
return None
base = next(
tool for tool in self.tools
if str((tool.get("function") or {}).get("name") or "") == "submit_cdsl_fragment"
)
tool = deepcopy(base)
function = tool["function"]
fragment_schema = deepcopy(contract["canonical_fragment_schema"])
function["description"] = (
f"Submit exactly one canonical {contract['atomic_id']} operation using the enforced fragment schema. "
"Legacy aliases, feature arrays, ids, dependencies and server-owned selectors are forbidden."
)
plan_mode = str((state or {}).get("modeling_plan_mode") or "semantic_only")
required = ["batch_goal", "fragment"]
if (state or {}).get("modeling_plan_enforced") and plan_mode == "indexed":
required.insert(0, "plan_step_id")
if (state or {}).get("modeling_plan_enforced"):
required.insert(1 if "plan_step_id" in required else 0, "plan_action_id")
function["parameters"] = {
"type": "object",
"properties": {
"plan_step_id": {"type": "string", "minLength": 1},
"plan_action_id": {"type": "string", "minLength": 1},
"batch_goal": {"type": "string", "minLength": 8, "maxLength": 800},
"fragment": fragment_schema,
},
"required": required,
"additionalProperties": False,
}
return tool
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
}
conclusion_tool = deepcopy(by_name["record_geometry_conclusion"])
rejection_state = (state or {}).get("geometry_rejection")
rejection_fingerprint = str((rejection_state or {}).get("geometry_fingerprint") or "") if isinstance(rejection_state, dict) else ""
diagnosis_state = ((state or {}).get("geometry_diagnoses_by_fingerprint") or {}).get(rejection_fingerprint) or {}
diagnosis_observations = diagnosis_state.get("observations") if isinstance(diagnosis_state, dict) else {}
current_evidence_refs = sorted({
str(item.get("ref") or "")
for item in (diagnosis_observations or {}).values()
if isinstance(item, dict) and str(item.get("ref") or "")
})
if current_evidence_refs:
evidence_schema = conclusion_tool["function"]["parameters"]["properties"]["evidence_refs"]["items"]
evidence_schema.pop("minLength", None)
evidence_schema["enum"] = current_evidence_refs
conclusion_tool["function"]["description"] += " Copy evidence_refs exactly from the enum; arbitrary text and paths are invalid."
by_name["record_geometry_conclusion"] = conclusion_tool
# Keep operation selection closed over the installed Runtime. A
# free-form string here lets the model repeatedly ask for aliases
# such as ``extrude_cut`` even though the backend rejects them. An
# operation-specific enum makes unsupported IDs unselectable at the
# tool boundary while the backend remains the final authority.
contract_tool = deepcopy(by_name["get_cdsl_operation_contract"])
try:
runtime_ids = sorted(
str(item) for item in getattr(load_engine(self.settings), "SUPPORTED_ATOMIC_IDS", ()) if str(item)
)
except Exception:
runtime_ids = []
if runtime_ids:
active_contract = self._active_contract(task, state)
active_atomic_id = str((active_contract or {}).get("atomic_id") or "")
selectable_runtime_ids = [item for item in runtime_ids if item != active_atomic_id] or runtime_ids
contract_parameters = contract_tool["function"].setdefault("parameters", {})
atomic_schema = contract_parameters.setdefault("properties", {}).setdefault("atomic_id", {})
atomic_schema.pop("minLength", None)
atomic_schema["enum"] = selectable_runtime_ids
contract_tool["function"]["description"] = (
"Read the exact Runtime contract for one supported atomic operation. "
"atomic_id must be copied exactly from the enum; aliases and prefixes are invalid."
)
by_name["get_cdsl_operation_contract"] = contract_tool
canonical_submit = self._canonical_submit_tool(task, state)
def tools_for(*names: str) -> list[dict[str, Any]]:
selected: list[dict[str, Any]] = []
seen: set[str] = set()
for name in names:
tool = canonical_submit if name == "submit_cdsl_fragment" else by_name[name]
if tool is None:
tool = by_name["get_cdsl_operation_contract"]
actual = str((tool.get("function") or {}).get("name") or "")
if actual and actual not in seen:
selected.append(tool)
seen.add(actual)
return selected
if (state or {}).get("completion_checklist_required") and not (state or {}).get("completion_checklist_written"):
return [by_name["write_completion_checklist"]]
# New autonomous runs must establish and independently review a
# modeling plan before exposing any geometry action. Legacy tests and
# persisted tasks without this marker retain the original protocol.
if (state or {}).get("modeling_plan_enforced"):
plan_status = str((state or {}).get("modeling_plan_status") or "missing")
if plan_status in {"missing", "revise"}:
return [by_name["write_modeling_plan"]]
if plan_status != "approved":
return []
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))
)
if (state or {}).get("modeling_plan_enforced"):
plan_status = str((state or {}).get("modeling_plan_status") or "")
step_status = (state or {}).get("plan_step_status") if isinstance((state or {}).get("plan_step_status"), dict) else {}
ready = ready and plan_status == "approved" and bool(step_status) and all(value in {"complete", "skipped"} for value in step_status.values())
# 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 tools_for("submit_cdsl_fragment", "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"]]
completion_proof = self._current_completion_proof(task, state or {})
current_plan_step = self._plan_current_step(state or {})
if (
completion_proof is not None
and (state or {}).get("modeling_plan_enforced")
and isinstance(current_plan_step, dict)
and str(current_plan_step.get("status") or "pending") not in {"complete", "skipped"}
):
# The frozen completion contract has already been independently
# verified on this exact revision. Do not force a redundant CDSL
# candidate merely to discover that the active plan step is a no-op.
return [by_name["skip_satisfied_plan_step"]]
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 "") == "operation_contract_required":
return [by_name["get_cdsl_operation_contract"]]
if str(action_required.get("reason") or "") == "unsupported_operation":
return [by_name["get_cdsl_operation_contract"], by_name["inspect_model"]]
if str(action_required.get("reason") or "") == "unsupported_operation_limit":
return [by_name["rollback_checkpoint"], by_name["inspect_model"]]
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 tools_for("submit_cdsl_fragment", "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 "") == "topology_selector_recovery":
return [by_name["inspect_topology"], by_name["rollback_checkpoint"]]
if str(action_required.get("reason") or "") == "topology_selector_recovery_submit":
return tools_for("submit_cdsl_fragment", "rollback_checkpoint")
if str(action_required.get("reason") or "") == "topology_selector_recovery_no_tokens":
return [by_name["rollback_checkpoint"]]
if str(action_required.get("reason") or "") == "engine_geometry_invalid":
return tools_for("inspect_topology", "measure_model", "render_views", "render_section", "get_cdsl_operation_contract", "submit_cdsl_fragment", "rollback_checkpoint")
if str(action_required.get("reason") or "") == "duplicate_repair_observation":
return tools_for("submit_cdsl_fragment", "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 tools_for("submit_cdsl_fragment", "rollback_checkpoint")
if str(action_required.get("reason") or "") == "review_service_failure":
# A malformed reviewer response is not evidence that the
# geometry is wrong. Prevent an authoring loop; the retained
# candidate must be retried by the review service or abandoned
# through rollback.
return [by_name["rollback_checkpoint"]]
if str(action_required.get("reason") or "") == "global_invariant_violation":
return tools_for("submit_cdsl_fragment", "rollback_checkpoint")
if str(action_required.get("reason") or "") == "optional_finish_failed":
# A cosmetic edge finish is deliberately non-blocking once
# the core checklist is independently complete. Keep the
# evidence tools available, but do not reopen a CDSL repair
# loop for the failed optional operation.
return [*repair_observation_tools(), *completion_action_tools(), 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 "") == "canonical_format_limit":
self._clear_active_operation_contract(state)
return [by_name["get_cdsl_operation_contract"], by_name["rollback_checkpoint"]]
if str(action_required.get("reason") or "") == "duplicate_fragment_limit":
return [by_name["rollback_checkpoint"]]
if str(action_required.get("reason") or "") == "repeated_cdsl_attempt":
return tools_for("submit_cdsl_fragment", "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 tools_for("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(), *tools_for("get_cdsl_operation_contract", "submit_cdsl_fragment", "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. The contract lookup remains available
# so the first additive operation can satisfy the same first-use
# protocol as every later operation.
if not str(task.get("active_revision") or "") and not str(task.get("active_candidate_id") or ""):
if canonical_submit is not None:
return tools_for("submit_cdsl_fragment")
recent = (state or {}).get("recent_events") or []
last_kind = str((recent[-1] or {}).get("kind") or "") if recent and isinstance(recent[-1], dict) else ""
if last_kind == "get_cdsl_operation_contract":
return tools_for("submit_cdsl_fragment")
return tools_for("get_cdsl_operation_contract")
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 tools_for("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 tools_for("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 tools_for("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 tools_for("inspect_topology", "get_cdsl_operation_contract", "measure_model", "render_views", "render_section", "submit_cdsl_fragment", "rollback_checkpoint") + completion_action_tools()
return tools_for(
"inspect_model", "read_cdsl_slice", "measure_model", "inspect_topology",
"get_cdsl_operation_contract", "render_views", "render_section",
"submit_cdsl_fragment", "record_geometry_conclusion", "rollback_checkpoint",
) + 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("modeling_plan_enforced", False)
current.setdefault("modeling_plan_written", bool(self.store.read_modeling_plan(task_id)))
current.setdefault("modeling_plan_status", "approved" if current.get("modeling_plan_written") else "missing")
current.setdefault("modeling_plan_version", int((task.get("modeling_plan_version") or 0)))
current.setdefault("modeling_plan_review_attempts", 0)
current.setdefault("modeling_plan", {})
current.setdefault("modeling_plan_mode", "semantic_only")
current.setdefault("plan_source_hash", "")
current.setdefault("active_plan_step_id", "")
current.setdefault("active_plan_action_id", "")
current.setdefault("plan_action_status", {})
current.setdefault("completed_plan_actions", [])
current.setdefault("plan_feature_status", {})
current.setdefault("plan_step_status", {})
current.setdefault("best_known_checkpoint", str(task.get("active_revision") or ""))
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("topology_observation", {})
current.setdefault("engine_geometry_failure", {})
current.setdefault("last_candidate_review", {})
current.setdefault("read_operation_contract_ids", [])
current.setdefault("active_operation_contract", {})
current.setdefault("pending_operation", "")
current.setdefault("pending_operation_contract_hash", "")
current.setdefault("pending_operation_revision", "")
current.setdefault("schema_retry_counts", {})
current.setdefault("selector_recovery_counts", {})
current.setdefault("geometry_retry_counts", {})
current.setdefault("unsupported_operation_counts", {})
current.setdefault("optional_finish", {})
active_contract = current.get("active_operation_contract")
if isinstance(active_contract, dict) and active_contract.get("atomic_id"):
try:
atomic_id = str(active_contract.get("atomic_id") or "")
runtime_schema = build_operation_fragment_schema(load_engine(self.settings), atomic_id)
valid_hash = operation_contract_hash(atomic_id, runtime_schema)
if (
str(current.get("pending_operation_revision") or "") != str(task.get("active_revision") or "")
or str(current.get("pending_operation_contract_hash") or "") != valid_hash
):
self._clear_active_operation_contract(current)
except (ValueError, KeyError, RuntimeError):
self._clear_active_operation_contract(current)
if not current.get("modeling_plan"):
plan_text = self.store.read_modeling_plan(task_id)
if plan_text:
try:
current["modeling_plan"] = parse_modeling_plan(plan_text, checklist=self._completion_items(task_id))
current["modeling_plan_mode"] = str(current["modeling_plan"].get("mode") or "semantic_only")
current["modeling_plan"]["plan_version"] = int(current.get("modeling_plan_version") or task.get("modeling_plan_version") or 1)
self._initialize_plan_actions(current)
except AutonomousGenerationError:
current["modeling_plan_status"] = "stale"
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": {},
"topology_observation": {},
"engine_geometry_failure": {},
"read_operation_contract_ids": [],
"active_operation_contract": {},
"pending_operation": "",
"pending_operation_contract_hash": "",
"pending_operation_revision": "",
"schema_retry_counts": {},
"selector_recovery_counts": {},
"geometry_retry_counts": {},
"unsupported_operation_counts": {},
"optional_finish": {},
"completion_checklist_required": False,
"completion_checklist_written": False,
"completion_ledger": {},
"modeling_plan_enforced": False,
"modeling_plan_written": False,
"modeling_plan_status": "missing",
"modeling_plan_version": 0,
"modeling_plan_review_attempts": 0,
"modeling_plan": {},
"modeling_plan_mode": "semantic_only",
"plan_source_hash": "",
"active_plan_step_id": "",
"active_plan_action_id": "",
"plan_action_status": {},
"completed_plan_actions": [],
"plan_feature_status": {},
"plan_step_status": {},
"best_known_checkpoint": "",
"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)
diagnostic = payload.get("diagnostic")
if isinstance(diagnostic, dict):
state["last_diagnostic"] = diagnostic
else:
previous = state.get("last_diagnostic")
state["last_diagnostic"] = str(payload.get("message") or payload.get("error") or (previous if isinstance(previous, str) else ""))
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"}
# Compatibility migrations and raw fragment audit records are useful
# for operators, but are dangerous few-shot examples for an author:
# they make legacy aliases look like successful canonical syntax.
hidden_kinds = {"compatibility_normalized", "fragment"}
entries = [
raw for raw in state.get("recent_events") or ()
if isinstance(raw, dict) and str(raw.get("kind") or "") not in hidden_kinds
]
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 _current_topology_observation(
self,
task_id: str,
task: dict[str, Any],
state: dict[str, Any],
*,
topology: dict[str, Any] | None = None,
) -> dict[str, Any] | None:
"""Return the one durable token bank valid for the active snapshot."""
observation = state.get("topology_observation")
if not isinstance(observation, dict):
return None
if str(observation.get("working_head") or "") != self._head_key(task):
return None
if topology is None:
_, topology, _, _ = self._artifact_data(task_id, task)
snapshot_id = str((topology or {}).get("snapshot_id") or "")
if not snapshot_id or str(observation.get("snapshot_id") or "") != snapshot_id:
return None
raw_tokens = observation.get("tokens")
if not isinstance(raw_tokens, list):
return None
tokens: list[dict[str, Any]] = []
for item in raw_tokens:
if not isinstance(item, dict):
return None
token = str(item.get("token") or "")
kind = str(item.get("kind") or "")
geometry = item.get("geometry")
if not token or kind not in {"face", "edge", "vertex", "plane", "axis", "body"} or not isinstance(geometry, dict):
return None
tokens.append({"token": token, "kind": kind, "geometry": deepcopy(geometry)})
return {"working_head": self._head_key(task), "snapshot_id": snapshot_id, "tokens": tokens}
def _plan_current_step(self, state: dict[str, Any]) -> dict[str, Any] | None:
plan = state.get("modeling_plan") if isinstance(state.get("modeling_plan"), dict) else {}
steps = [item for item in plan.get("steps") or () if isinstance(item, dict)]
active_id = str(state.get("active_plan_step_id") or "")
if active_id:
found = next((item for item in steps if str(item.get("step_id") or "") == active_id), None)
if found is not None:
return found
for step in steps:
if str(step.get("status") or "pending") not in {"complete", "skipped"}:
return step
return steps[-1] if steps else None
def _plan_current_action(self, state: dict[str, Any]) -> dict[str, Any] | None:
"""Return the next required action inside the active semantic step."""
step = self._plan_current_step(state)
if not isinstance(step, dict):
return None
actions = [item for item in step.get("actions") or () if isinstance(item, dict)]
if not actions:
return None
active_id = str(state.get("active_plan_action_id") or "")
statuses = state.get("plan_action_status") if isinstance(state.get("plan_action_status"), dict) else {}
if active_id:
found = next((item for item in actions if str(item.get("action_id") or "") == active_id), None)
if found is not None and statuses.get(active_id, found.get("status", "pending")) not in {"complete", "skipped", "satisfied_by_prior_step"}:
return found
return next(
(item for item in actions
if statuses.get(str(item.get("action_id") or ""), item.get("status", "pending")) not in {"complete", "skipped", "satisfied_by_prior_step"}
and bool(item.get("required", True))),
next((item for item in actions if statuses.get(str(item.get("action_id") or ""), item.get("status", "pending")) not in {"complete", "skipped", "satisfied_by_prior_step"}), None),
)
def _initialize_plan_actions(self, state: dict[str, Any]) -> None:
plan = state.get("modeling_plan") if isinstance(state.get("modeling_plan"), dict) else {}
actions = [item for item in plan.get("actions") or () if isinstance(item, dict)]
statuses = state.setdefault("plan_action_status", {})
for action in actions:
action_id = str(action.get("action_id") or "")
if action_id:
statuses.setdefault(action_id, str(action.get("status") or "pending"))
if not str(state.get("active_plan_action_id") or ""):
action = self._plan_current_action(state)
state["active_plan_action_id"] = str((action or {}).get("action_id") or "")
state["completed_plan_actions"] = [key for key, value in statuses.items() if value in {"complete", "satisfied_by_prior_step"}]
def _plan_prompt_context(self, state: dict[str, Any]) -> dict[str, Any] | None:
if not state.get("modeling_plan_enforced") or str(state.get("modeling_plan_status") or "") != "approved":
return None
plan = state.get("modeling_plan") if isinstance(state.get("modeling_plan"), dict) else {}
step = self._plan_current_step(state)
if step is None:
return None
feature_by_id = {
str(item.get("feature_id") or ""): item
for item in plan.get("features") or () if isinstance(item, dict)
}
features = [feature_by_id[item] for item in step.get("feature_ids") or () if item in feature_by_id]
self._initialize_plan_actions(state)
current_action = self._plan_current_action(state)
actions = [item for item in step.get("actions") or () if isinstance(item, dict)]
completed_actions = [key for key, value in (state.get("plan_action_status") or {}).items() if value in {"complete", "satisfied_by_prior_step"}]
step_ids = [str(item.get("step_id") or "") for item in plan.get("steps") or () if isinstance(item, dict)]
current_index = step_ids.index(str(step.get("step_id") or "")) if str(step.get("step_id") or "") in step_ids else -1
return {
"plan_version": plan.get("plan_version"),
"plan_mode": str(plan.get("mode") or state.get("modeling_plan_mode") or "semantic_only"),
"current_step": deepcopy(step),
"current_step_features": deepcopy(features),
"current_action": deepcopy(current_action),
"current_step_actions": deepcopy(actions),
"completed_actions": completed_actions,
"remaining_actions": [str(item.get("action_id") or "") for item in actions if str(item.get("action_id") or "") not in completed_actions],
"completed_feature_ids": [key for key, value in (state.get("plan_feature_status") or {}).items() if value == "complete"],
"satisfied_by_prior_feature_ids": [key for key, value in (state.get("plan_feature_status") or {}).items() if value == "satisfied_by_prior_step"],
"step_status": state.get("plan_step_status") or {},
"future_steps": [
deepcopy(item)
for index, item in enumerate(plan.get("steps") or ())
if isinstance(item, dict)
and index > current_index
and str(item.get("status") or "pending") not in {"complete", "skipped"}
],
"instruction": "A semantic modeling step may contain multiple related actions. Generate only the current action and submit exactly one feature per fragment with plan_step_id and plan_action_id. Left and right related slots may stay in one step, but must use separate fragments and checkpoints. Do not implement features belonging to future_steps early. If the current model already satisfies a future step, collect evidence and record a skip conclusion.",
}
def _plan_gate_error(self, state: dict[str, Any]) -> str | None:
if not state.get("modeling_plan_enforced"):
return None
if str(state.get("modeling_plan_status") or "") != "approved":
return "PLAN_REQUIRED: modeling plan must be approved before final publication"
steps = state.get("plan_step_status") if isinstance(state.get("plan_step_status"), dict) else {}
incomplete = [key for key, value in steps.items() if value not in {"complete", "skipped"}]
action_status = state.get("plan_action_status") if isinstance(state.get("plan_action_status"), dict) else {}
plan = state.get("modeling_plan") if isinstance(state.get("modeling_plan"), dict) else {}
incomplete_actions = [str(item.get("action_id") or "") for item in plan.get("actions") or () if isinstance(item, dict) and item.get("required", True) and action_status.get(str(item.get("action_id") or ""), "pending") not in {"complete", "skipped", "satisfied_by_prior_step"}]
if incomplete:
return "PLAN_INCOMPLETE: unresolved modeling-plan steps: " + ", ".join(incomplete)
if incomplete_actions:
return "PLAN_INCOMPLETE: unresolved modeling-plan actions: " + ", ".join(incomplete_actions)
return None
def _current_completion_proof(self, task: dict[str, Any], state: dict[str, Any]) -> dict[str, Any] | None:
"""Return independent completion evidence bound to the active revision."""
revision_id = str(task.get("active_revision") or "")
ledger = state.get("completion_ledger") if isinstance(state.get("completion_ledger"), dict) else {}
raw_items = ledger.get("items")
if not isinstance(raw_items, list) or not raw_items or any(not isinstance(item, dict) for item in raw_items):
return None
items = list(raw_items)
task_id = str(task.get("task_id") or "")
checklist = self._completion_items(task_id) if task_id else []
if checklist:
expected = [_checklist_key(item) for item in checklist]
actual = [_checklist_key(str(item.get("item") or "")) for item in items]
if actual != expected:
return None
if (
not revision_id
or str(ledger.get("verified_revision") or "") != revision_id
or any(str(item.get("status") or "") != "complete" for item in items)
):
return None
return {
"revision_id": revision_id,
"evidence_ref": f"completion_ledger:{revision_id}",
"item_count": len(items),
"items": [
{"item": str(item.get("item") or ""), "evidence": str(item.get("evidence") or "")}
for item in items
],
}
def _mark_plan_progress(self, state: dict[str, Any], candidate: dict[str, Any]) -> None:
if not state.get("modeling_plan_enforced"):
return
plan = state.get("modeling_plan") if isinstance(state.get("modeling_plan"), dict) else {}
step_id = str(candidate.get("plan_step_id") or "")
if not step_id:
return
step = next((item for item in plan.get("steps") or () if isinstance(item, dict) and str(item.get("step_id") or "") == step_id), None)
if not isinstance(step, dict):
return
self._initialize_plan_actions(state)
feature_status = state.setdefault("plan_feature_status", {})
action_status = state.setdefault("plan_action_status", {})
feature_ids = {str(value) for value in step.get("feature_ids") or () if str(value)}
planned_features = [
item for item in plan.get("features") or ()
if isinstance(item, dict) and str(item.get("feature_id") or "") in feature_ids
]
action_id = str(candidate.get("plan_action_id") or "")
actions = [item for item in step.get("actions") or () if isinstance(item, dict)]
action = next((item for item in actions if str(item.get("action_id") or "") == action_id), None) if action_id else None
if action is not None:
action_status[action_id] = "complete"
action["status"] = "complete"
linked_feature = str(action.get("feature_id") or "")
if linked_feature:
feature_status[linked_feature] = "complete"
elif str(action.get("operation") or ""):
match = next(
(item for item in planned_features
if str(item.get("operation") or "") == str(action.get("operation") or "")
and feature_status.get(str(item.get("feature_id") or "")) != "complete"),
None,
)
if isinstance(match, dict) and str(match.get("feature_id") or ""):
feature_status[str(match.get("feature_id"))] = "complete"
operations = [str(value) for value in candidate.get("atomic_ids") or () if str(value)]
for operation in operations:
if action is not None:
break
match = next(
(item for item in planned_features
if str(item.get("operation") or "") == operation
and feature_status.get(str(item.get("feature_id") or "")) != "complete"),
None,
)
if isinstance(match, dict):
feature_status[str(match.get("feature_id") or "")] = "complete"
step_status = state.setdefault("plan_step_status", {})
required_actions = [item for item in actions if item.get("required", True)]
actions_complete = all(action_status.get(str(item.get("action_id") or "")) in {"complete", "skipped", "satisfied_by_prior_step"} for item in required_actions)
features_complete = all(feature_status.get(str(item.get("feature_id") or "")) == "complete" for item in planned_features) if planned_features else True
step_status[step_id] = "complete" if actions_complete and features_complete else "pending"
for item in plan.get("steps") or ():
if isinstance(item, dict) and str(item.get("step_id") or "") == step_id:
item["status"] = step_status[step_id]
next_step = next((item for item in plan.get("steps") or () if isinstance(item, dict) and str(item.get("status") or "pending") not in {"complete", "skipped"}), None)
state["active_plan_step_id"] = str(next_step.get("step_id") or "") if isinstance(next_step, dict) else ""
state["active_plan_action_id"] = ""
self._initialize_plan_actions(state)
def _mark_plan_step_skipped(
self,
state: dict[str, Any],
*,
evidence_refs: list[str],
reason: str,
) -> dict[str, Any] | None:
"""Advance a plan step already satisfied by the current geometry."""
if not state.get("modeling_plan_enforced"):
return None
plan = state.get("modeling_plan") if isinstance(state.get("modeling_plan"), dict) else {}
step = self._plan_current_step(state)
if not isinstance(step, dict):
return None
step_id = str(step.get("step_id") or state.get("active_plan_step_id") or "")
if not step_id:
return None
state.setdefault("plan_step_status", {})[step_id] = "skipped"
feature_status = state.setdefault("plan_feature_status", {})
action_status = state.setdefault("plan_action_status", {})
for feature_id in step.get("feature_ids") or ():
key = str(feature_id or "")
if key:
feature_status[key] = "satisfied_by_prior_step"
for action in step.get("actions") or ():
if isinstance(action, dict) and str(action.get("action_id") or ""):
action_id = str(action["action_id"])
action_status[action_id] = "satisfied_by_prior_step"
action["status"] = "satisfied_by_prior_step"
step["status"] = "skipped"
step["satisfied_by_prior_step"] = True
step["skip_reason"] = str(reason).strip()
step["skip_evidence_refs"] = list(dict.fromkeys(str(item) for item in evidence_refs if str(item)))
next_step = next(
(item for item in plan.get("steps") or ()
if isinstance(item, dict) and str(item.get("status") or "pending") not in {"complete", "skipped"}),
None,
)
state["active_plan_step_id"] = str(next_step.get("step_id") or "") if isinstance(next_step, dict) else ""
state["active_plan_action_id"] = ""
self._initialize_plan_actions(state)
return step
def _restore_plan_after_rollback(self, state: dict[str, Any], task: dict[str, Any], revision_id: str) -> None:
if not state.get("modeling_plan_enforced"):
return
plan = state.get("modeling_plan") if isinstance(state.get("modeling_plan"), dict) else {}
steps = [item for item in plan.get("steps") or () if isinstance(item, dict)]
target_step_id = ""
if revision_id:
revision = next((item for item in task.get("revisions") or () if isinstance(item, dict) and str(item.get("revision_id") or "") == revision_id), None)
target_step_id = str((revision or {}).get("plan_step_id") or "")
target_index = next((index for index, item in enumerate(steps) if str(item.get("step_id") or "") == target_step_id), -1)
self._initialize_plan_actions(state)
feature_status = state.setdefault("plan_feature_status", {})
action_status = state.setdefault("plan_action_status", {})
step_status = state.setdefault("plan_step_status", {})
target_revision = next((item for item in task.get("revisions") or () if isinstance(item, dict) and str(item.get("revision_id") or "") == revision_id), {}) if revision_id else {}
target_action_id = str(target_revision.get("plan_action_id") or "")
for index, step in enumerate(steps):
step_id = str(step.get("step_id") or "")
prior_step = target_index >= 0 and index < target_index
target_step = target_index >= 0 and index == target_index
for feature_id in step.get("feature_ids") or ():
feature_status[str(feature_id)] = "complete" if prior_step else "pending"
actions = [item for item in step.get("actions") or () if isinstance(item, dict)]
target_action_index = next((i for i, item in enumerate(actions) if str(item.get("action_id") or "") == target_action_id), -1)
for action_index, action in enumerate(actions):
if not isinstance(action, dict):
continue
action_id = str(action.get("action_id") or "")
if not action_id:
continue
action_complete = prior_step or (target_step and target_action_index >= 0 and action_index <= target_action_index)
action_status[action_id] = "complete" if action_complete else "pending"
action["status"] = action_status[action_id]
if action_complete and str(action.get("feature_id") or ""):
feature_status[str(action.get("feature_id"))] = "complete"
required_actions = [item for item in actions if item.get("required", True)]
step_complete = bool(actions) and all(action_status.get(str(item.get("action_id") or "")) == "complete" for item in required_actions)
if not actions:
step_complete = prior_step
step["status"] = "complete" if step_complete else "pending"
step_status[step_id] = step["status"]
next_step = next((item for item in steps if str(item.get("status") or "pending") not in {"complete", "skipped"}), None)
state["active_plan_step_id"] = str(next_step.get("step_id") or "") if isinstance(next_step, dict) else ""
state["active_plan_action_id"] = ""
self._initialize_plan_actions(state)
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 = {
"visible_response_language": _response_language(str(state.get("request") or source_requirements or "")),
"visible_response_language_rule": "Every visible word outside code, identifiers, and units must use visible_response_language.",
"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),
"modeling_plan": self._plan_prompt_context(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,
"engine_geometry_failure": state.get("engine_geometry_failure") or None,
# This survives event trimming. It is the only token bank any
# topology-sensitive fragment may reuse, and disappears as soon
# as its checkpoint or runtime snapshot changes.
"current_topology_tokens": self._current_topology_observation(task_id, task, state),
"edge_selector_recovery": state.get("edge_selector_recovery") or None,
"final_repair": state.get("final_repair") or None,
# Keep the active contract in durable context. It must not be
# hidden in recent events where context trimming can remove it.
"active_operation_contract": state.get("active_operation_contract") 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": "required only when the active contract says requires_sketch", "feature": "exactly one {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": [
"Every submission contains exactly one operation and exactly one feature. The active operation contract is authoritative; legacy aliases, feature arrays, wrappers and unknown fields are invalid.",
"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, 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 explicit params.angle_deg and params.axis={origin_mm:[x,y,z],direction:[dx,dy,dz]}. Never use shared_revolve_axis, top-level revolve_axis, or 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 state.get("modeling_plan_enforced") and str(state.get("modeling_plan_status") or "missing") in {"missing", "revise"}:
context["planning_references"] = _planning_reference_context(self.settings, requirements or source_requirements)
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 a structured 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)
if len(encoded) > limit:
# Keep the non-negotiable author contracts and current CAD
# state, while dropping verbose prose/evidence before any
# contract can be trimmed. The provider may receive a larger
# message than the configured soft budget, but it will never
# receive a turn without the active operation schema.
for key in ("last_candidate_review", "last_step_review", "engine_geometry_failure", "geometry_diagnosis", "geometry_rejection", "final_repair", "runtime_operations", "cdsl_authoring_basics"):
context[key] = None
encoded = json.dumps(context, ensure_ascii=False)
if len(encoded) <= limit:
break
requirements_frozen = bool(requirements)
response_language = _response_language(str(state.get("request") or source_requirements or ""))
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. "
f"All visible Agent prose, explanations, requirement markdown, checklist text, and tool status messages must be written in {response_language}, matching the user's request language. Keep CAD identifiers, JSON keys, units, and code syntax unchanged. "
+ 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. "
)
+ (
"Now call write_modeling_plan before any CAD observation or CDSL. Write a clear semantic plan in ordinary Markdown or prose; use readable headings such as ## Step 1 - Base and ## Step 2 - Support so execution can keep related work together. When a step contains multiple independently located host regions, profiles, or Runtime features, it MUST enumerate one action per target using headings such as ### Action 3a - Target A and ### Action 3b - Target B, or [ACTION target_a] and [ACTION target_b]. Keep related actions in the same semantic step rather than turning each target into an unrelated step. Each action must be executable as exactly one Runtime feature. An operation that natively supports several positions or a pattern may remain one action when all targets share the operation's required host and parameters. Include target, exact Runtime operation when known, and dependencies. Do not force JSON or checklist text copying. Explain what each step builds, which requirements it addresses, how operations relate geometrically, prerequisites, and how to verify the result. "
if state.get("modeling_plan_enforced") and str(state.get("modeling_plan_status") or "missing") in {"missing", "revise"} else ""
)
+ "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. "
"Every submit_cdsl_fragment call must contain exactly one feature for the currently loaded operation contract and the active plan action. A semantic step may contain multiple related actions, but submit each action separately with its own plan_action_id and checkpoint. Mixed batches and features arrays are disabled in this run; do not send batch_relationship, shared_revolve_axis, legacy aliases, or historical fields. If the operation changes, read its contract first. "
"Only atomic_id values listed exactly in runtime_operations are supported. Treat generic names such as extrude_add, cylinder_add, or pattern_circular as invalid aliases unless that exact ID appears in runtime_operations; choose a listed operation and read its contract. "
"For sketch operations use exactly {sketch:{workplane,profile},feature:{atomic_id,params}}; for non-sketch operations use {feature:{atomic_id,params,selector_tokens when required}}. Never nest workplane or profile inside feature.params. "
"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 a structured fragment object, not a nested JSON string. 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, raw selectors, or legacy aliases in a new 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 opaque tokens in current_topology_tokens; that complete token bank includes token, kind and compact geometry and is invalid immediately after a checkpoint, rollback, or topology change. "
"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 skip_satisfied_plan_step is exposed, the independent completion ledger has already verified every frozen requirement on the current revision; call it instead of creating redundant geometry. 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 and copy evidence_refs exactly from the tool enum. Choose skip only when those current observations prove the active plan step was already satisfied by a prior feature; skip advances to the next step without creating a revision. Until a 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."
)
if state.get("modeling_plan_enforced") and str(state.get("modeling_plan_status") or "") == "approved":
system += (
" The approved modeling plan is execution guidance. Generate only the current plan step shown in context, "
"include its plan_step_id and active plan_action_id on submit_cdsl_fragment when available. Keep related actions in the same semantic step, but never combine two independent profiles in one fragment; do not invent or combine unrelated work from another step. "
"The plan is guidance for operation selection, not a request to emit a plan or an IR."
)
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", "revision_id": "", "current": True, "rollback_allowed": False, "recommended_for_rollback": False}]}
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": _rollback_token_details(task),
}
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 [],
"planStepId": candidate.get("plan_step_id") or "",
"planActionId": candidate.get("plan_action_id") 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)
detail = str(error)
review_code = "REVIEW_RESPONSE_INCOMPLETE" if "coverage" in detail.lower() or "invalid" in detail.lower() else "REVIEW_SERVICE_ERROR"
raise AutonomousGenerationError(f"CANDIDATE_REVIEW_FAILED: {review_code}: {detail}") 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 {}
plan_step = None
plan_feature_ids: list[str] = []
if state.get("modeling_plan_enforced"):
plan = state.get("modeling_plan") if isinstance(state.get("modeling_plan"), dict) else {}
plan_step_id = str(candidate.get("plan_step_id") or state.get("active_plan_step_id") or "")
plan_step = next((item for item in plan.get("steps") or () if isinstance(item, dict) and str(item.get("step_id") or "") == plan_step_id), None)
plan_feature_ids = [str(item) for item in (plan_step or {}).get("feature_ids") or () if str(item)]
# A final envelope dimension is not expected to exist during the
# initial base step. Keep solid-count validation global, but defer
# dimensions until the plan has advanced beyond its first structural
# step (or until an unplanned/legacy run is being reviewed).
plan_steps = [item for item in (state.get("modeling_plan") or {}).get("steps", []) if isinstance(item, dict)] if state.get("modeling_plan_enforced") else []
step_index = next((index for index, item in enumerate(plan_steps) if item is plan_step), 0)
check_dimensions = not state.get("modeling_plan_enforced") or step_index > 0
global_violations = _global_geometry_violations(
candidate.get("health") if isinstance(candidate.get("health"), dict) else {},
requirements=self.store.read_requirements_document(task_id),
checklist=self._completion_items(task_id),
check_dimensions=check_dimensions,
)
if global_violations:
stored = read_json(candidate_dir / "candidate.json", candidate)
if isinstance(stored, dict):
stored.update({"status": "blocked_global_invariant", "global_diagnostics": global_violations, "review_failed_at": now_iso()})
write_json(candidate_dir / "candidate.json", stored)
self.store.clear_active_candidate(task_id, candidate_id)
message = "; ".join(str(item.get("message") or item.get("code") or "") for item in global_violations)
raise AutonomousGenerationError(f"GLOBAL_INVARIANT_VIOLATION: {message}")
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,
plan_step=plan_step,
plan_feature_ids=plan_feature_ids,
plan_action=(next((item for item in (plan_step or {}).get("actions") or () if isinstance(item, dict) and str(item.get("action_id") or "") == str(candidate.get("plan_action_id") or "")), None) if plan_step else None),
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)
detail = str(error)
review_code = "REVIEW_RESPONSE_INCOMPLETE" if "coverage" in detail.lower() or "invalid" in detail.lower() else "REVIEW_SERVICE_ERROR"
raise AutonomousGenerationError(f"CANDIDATE_REVIEW_FAILED: {review_code}: {detail}") 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)
candidate_record = read_json(self.store.candidate_dir(task_id, candidate_id) / "candidate.json", {})
if isinstance(candidate_record, dict):
action_id = str(candidate_record.get("plan_action_id") or "")
if action_id:
candidate_record["completed_plan_actions"] = sorted({
*[str(key) for key, value in (state.get("plan_action_status") or {}).items() if value in {"complete", "satisfied_by_prior_step"}],
action_id,
})
write_json(self.store.candidate_dir(task_id, candidate_id) / "candidate.json", candidate_record)
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._clear_active_operation_contract(state)
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"]))
if isinstance(candidate_record, dict):
self._mark_plan_progress(state, candidate_record)
# Keep the latest accepted checkpoint as the best recoverable state.
# A later repair must not silently discard all valid upstream work by
# choosing root when a usable checkpoint exists.
state["best_known_checkpoint"] = str(built.get("revision_id") or "")
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,
)
if global_violations:
review = {
**review,
"verdict": "repair",
"confidence": 1.0,
"evidence": list(review.get("evidence") or []) + [str(item.get("message") or item.get("code") or "") for item in global_violations],
"global_diagnostics": global_violations,
}
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."""
if self.on_text_delta is None:
return await self.complete(messages, tools, provider, model, forced_tool)
return await self.complete(messages, tools, provider, model, forced_tool, on_text_delta=self.on_text_delta)
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,
)
# Plan review requires an independently configured reviewer. Keep
# legacy/local runs without that reviewer on the original protocol;
# production runs with the feature enabled get the hard plan gate.
state["modeling_plan_enforced"] = bool(
self.settings.modeling_plan_enabled
and self.settings.review_provider_id
and self.settings.review_model_id
)
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", "markdown": requirements}
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})
preview_arguments = {
str(key): (str(value)[:800] + ("..." if len(str(value)) > 800 else ""))
for key, value in arguments.items()
if key not in {"api_key", "authorization", "image", "image_data"}
}
invocation_id = f"{task_id}_tool_{secrets.token_hex(6)}"
yield "tool_call", {"taskId": task_id, "tool": name, "arguments": preview_arguments, "status": "running", "eventId": invocation_id, "invocationId": invocation_id}
try:
events, progressed = await self._execute_tool(task_id, request, state, engine, name, arguments)
except Exception as error:
yield "tool_call", {"taskId": task_id, "tool": name, "status": "error", "message": str(error), "eventId": invocation_id, "invocationId": invocation_id}
raise
completion_event = False
terminal_event = False
for event_name, payload in events:
visible_payload = dict(payload)
visible_payload.setdefault("invocationId", invocation_id)
if event_name == "tool_call":
visible_payload.setdefault("eventId", invocation_id)
completion_event = str(visible_payload.get("status") or "").lower() in {"success", "error"}
terminal_event = terminal_event or event_name == "task_terminal"
yield event_name, visible_payload
if not completion_event and not terminal_event:
yield "tool_call", {"taskId": task_id, "tool": name, "status": "success", "message": "工具执行完成。", "eventId": invocation_id, "invocationId": invocation_id}
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", "markdown": str(arguments.get("markdown") or "").strip()}))
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, "markdown": markdown}))
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 == "write_modeling_plan":
if not state.get("modeling_plan_enforced"):
raise AutonomousGenerationError("PLAN_DISABLED: modeling plan is not enabled for this run")
plan_text = str(arguments.get("plan_text") or "").strip()
current_version = int(state.get("modeling_plan_version") or task.get("modeling_plan_version") or 0)
version = current_version + 1
runtime_ids = {str(item) for item in getattr(engine, "SUPPORTED_ATOMIC_IDS", ()) if str(item)}
checklist = self._completion_items(task_id)
previous_step_status = deepcopy(state.get("plan_step_status")) if isinstance(state.get("plan_step_status"), dict) else {}
previous_action_status = deepcopy(state.get("plan_action_status")) if isinstance(state.get("plan_action_status"), dict) else {}
previous_feature_status = deepcopy(state.get("plan_feature_status")) if isinstance(state.get("plan_feature_status"), dict) else {}
plan = parse_modeling_plan(plan_text, checklist=checklist, runtime_atomic_ids=runtime_ids)
plan["plan_version"] = version
plan["source_requirements_hash"] = hashlib.sha256(
self.store.read_source_requirements(task_id).encode("utf-8")
).hexdigest()
plan["plan_source_hash"] = hashlib.sha256(plan_text.encode("utf-8")).hexdigest()
state["modeling_plan"] = plan
state["plan_source_hash"] = plan["plan_source_hash"]
state["modeling_plan_mode"] = str(plan.get("mode") or "semantic_only")
state["modeling_plan_status"] = "pending_review"
self._record(state, "modeling_plan", {"message": "Modeling plan written; independent review is running.", "version": version})
try:
review = await review_modeling_plan(
self.settings,
source_requirements=self.store.read_source_requirements(task_id),
requirements=self.store.read_requirements_document(task_id),
checklist=checklist,
plan=plan,
runtime_operations=_runtime_summary(engine),
node_id=f"plan-v{version}",
)
except Exception as error:
state["modeling_plan_status"] = "revise" if current_version else "missing"
raise AutonomousGenerationError(f"PLAN_REVIEW_FAILED: {error}") from error
self.store.write_modeling_plan(task_id, plan_text, version=version)
state["modeling_plan_written"] = True
state["modeling_plan_version"] = version
state["modeling_plan_review_attempts"] = int(state.get("modeling_plan_review_attempts") or 0) + 1
review["plan_version"] = version
review_path = self.store.write_modeling_plan_review(task_id, review, version=version)
public_review = _public_modeling_plan_review(review)
state["modeling_plan_review_path"] = review_path.as_posix()
if public_review.get("verdict") == "pass":
state["modeling_plan_status"] = "approved"
state["plan_feature_status"] = {
str(item.get("feature_id")): previous_feature_status.get(str(item.get("feature_id")), "pending")
for item in plan.get("features") or () if isinstance(item, dict)
}
state["plan_step_status"] = {
str(item.get("step_id")): previous_step_status.get(str(item.get("step_id")), "pending")
for item in plan.get("steps") or () if isinstance(item, dict)
}
state["plan_action_status"] = {
str(item.get("action_id")): previous_action_status.get(str(item.get("action_id")), "pending")
for item in plan.get("actions") or () if isinstance(item, dict) and str(item.get("action_id") or "")
}
for step in plan.get("steps") or ():
if isinstance(step, dict):
step["status"] = state["plan_step_status"].get(str(step.get("step_id") or ""), "pending")
for action in plan.get("actions") or ():
if isinstance(action, dict):
action["status"] = state["plan_action_status"].get(str(action.get("action_id") or ""), "pending")
next_step = next(
(item for item in plan.get("steps") or () if isinstance(item, dict) and str(item.get("status") or "pending") not in {"complete", "skipped"}),
None,
)
state["active_plan_step_id"] = str((next_step or {}).get("step_id") or "")
state["active_plan_action_id"] = ""
state["completed_plan_actions"] = [key for key, value in state["plan_action_status"].items() if value in {"complete", "satisfied_by_prior_step"}]
self._initialize_plan_actions(state)
else:
state["modeling_plan_status"] = "revise"
if int(state["modeling_plan_review_attempts"]) >= self.settings.modeling_plan_max_revisions:
raise AutonomousGenerationError("PLAN_REVIEW_REVISE_LIMIT: modeling plan revision limit reached")
self._record(state, "modeling_plan_review", {
"message": f"Modeling plan review returned {public_review.get('verdict')}",
"version": version,
"verdict": public_review.get("verdict"),
"issues": public_review.get("issues") or [],
"path": review_path.as_posix(),
})
events.append(("modeling_plan", {"taskId": task_id, "version": version, "status": state["modeling_plan_status"], "plan": plan}))
events.append(("modeling_plan_review", {"taskId": task_id, "version": version, "review": public_review}))
return events, True
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]}
state["topology_observation"] = {
"working_head": self._head_key(task),
"snapshot_id": str((topology or {}).get("snapshot_id") or ""),
# Keep the compact, author-visible token object rather
# than only its opaque string. Contract reads and other
# tool results may be trimmed from conversation history.
"tokens": deepcopy(result["tokens"]),
}
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"])})
elif (
isinstance(action_required, dict)
and str(action_required.get("working_head") or "") == self._head_key(task)
and str(action_required.get("reason") or "") == "topology_selector_recovery"
):
if result["tokens"]:
state["candidate_action_required"] = {
"working_head": self._head_key(task),
"reason": "topology_selector_recovery_submit",
"instruction": "Use only selector tokens returned by this current topology observation in the corrected fragment, or roll back.",
}
self._record(state, "topology_selector_recovery_complete", {
"message": "Fresh current topology tokens returned; submit a corrected selector fragment or roll back.",
"token_count": len(result["tokens"]),
})
else:
state["candidate_action_required"] = {
"working_head": self._head_key(task),
"reason": "topology_selector_recovery_no_tokens",
"instruction": "The active snapshot has no selector tokens of the requested kind. Roll back rather than inventing a selector.",
}
self._record(state, "topology_selector_recovery_no_tokens", {
"message": "No current topology tokens were available for the requested selector recovery.",
})
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")
previous_contract = self._active_contract(task, state, engine)
repeated_current_contract = str((previous_contract or {}).get("atomic_id") or "") == atomic_id
action_required = state.get("candidate_action_required")
if (
isinstance(action_required, dict)
and str(action_required.get("working_head") or "") == self._head_key(task)
and str(action_required.get("reason") or "") == "operation_contract_required"
):
required_ids = [
str(item) for item in action_required.get("atomic_ids") or ()
if isinstance(item, str) and str(item)
]
if atomic_id not in required_ids:
raise AutonomousGenerationError(
"OPERATION_CONTRACT_REQUIRED: read the required operation contract first: "
+ ", ".join(required_ids)
)
result = _operation_contract_payload(engine, atomic_id)
read_ids = [
str(item) for item in state.setdefault("read_operation_contract_ids", [])
if isinstance(item, str) and str(item)
]
if atomic_id not in read_ids:
read_ids.append(atomic_id)
state["read_operation_contract_ids"] = read_ids
state["active_operation_contract"] = deepcopy(result)
state["pending_operation"] = atomic_id
state["pending_operation_contract_hash"] = str(result.get("contract_hash") or "")
state["pending_operation_revision"] = str(task.get("active_revision") or "")
if str((state.get("candidate_action_required") or {}).get("reason") or "") == "canonical_format_limit":
state["candidate_action_required"] = {}
if (
isinstance(action_required, dict)
and str(action_required.get("working_head") or "") == self._head_key(task)
and str(action_required.get("reason") or "") == "operation_contract_required"
):
required_ids = {
str(item) for item in action_required.get("atomic_ids") or ()
if isinstance(item, str) and str(item)
}
if required_ids.issubset(set(read_ids)):
state["candidate_action_required"] = {}
self._record(state, "operation_contract_repeated" if repeated_current_contract else "get_cdsl_operation_contract", {
"message": (
f"Runtime contract for {atomic_id} was already active on this revision."
if repeated_current_contract else
f"Runtime contract returned for {atomic_id}."
),
**({} if repeated_current_contract else {"result": result}),
})
events.append(("tool_call", {"taskId": task_id, "tool": name, "status": "success", "message": f"Operation contract loaded for {atomic_id}."}))
return events, not repeated_current_contract
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 == "skip_satisfied_plan_step":
proof = self._current_completion_proof(task, state)
if proof is None:
raise AutonomousGenerationError(
"PLAN_SKIP_INVALID: the frozen completion checklist is not fully verified on the active revision"
)
if not state.get("modeling_plan_enforced") or str(state.get("modeling_plan_status") or "") != "approved":
raise AutonomousGenerationError("PLAN_SKIP_UNAVAILABLE: skip requires an approved enforced modeling plan")
skipped_step = self._mark_plan_step_skipped(
state,
evidence_refs=[str(proof["evidence_ref"])],
reason=str(arguments.get("reason") or "satisfied by independently verified completion evidence"),
)
if skipped_step is None:
raise AutonomousGenerationError("PLAN_SKIP_INVALID: no active modeling-plan step can be skipped")
self._clear_active_operation_contract(state)
state["geometry_rejection"] = {}
state["candidate_action_required"] = {}
step_id = str(skipped_step.get("step_id") or "")
self._record(state, "plan_step_satisfied", {
"message": "Skipped a redundant plan step using completion evidence verified on the active revision.",
"plan_step_id": step_id,
"completion_revision": proof["revision_id"],
"completion_item_count": proof["item_count"],
"evidence_ref": proof["evidence_ref"],
})
events.append(("plan_step_skipped", {
"taskId": task_id,
"planStepId": step_id,
"nextPlanStepId": str(state.get("active_plan_step_id") or ""),
"evidenceRef": proof["evidence_ref"],
"message": "The active revision already satisfies every frozen completion item; the redundant plan step was skipped without creating geometry.",
"status": "success",
}))
return events, True
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:
allowed = ", ".join(sorted(valid_refs)) or "none"
raise AutonomousGenerationError(
"GEOMETRY_CONCLUSION_EVIDENCE_UNKNOWN: " + ", ".join(unknown)
+ f"; allowed_evidence_refs: {allowed}"
)
decision = str(arguments.get("decision") or "")
plan = arguments.get("optimization_plan")
concluded_plan_step_id = str((self._plan_current_step(state) or {}).get("step_id") or "")
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); minimal payload: "
"{\"optimization_plan\":{\"action\":\"describe the next materially different modelling step\"}}"
)
# 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 == "skip":
if not state.get("modeling_plan_enforced"):
raise AutonomousGenerationError("PLAN_SKIP_UNAVAILABLE: skip requires an enforced modeling plan")
if str(arguments.get("root_cause") or "") != "duplicate_feature":
raise AutonomousGenerationError("PLAN_SKIP_INVALID: skip is only valid when evidence shows the step is already satisfied")
skipped_step = self._mark_plan_step_skipped(
state,
evidence_refs=refs,
reason=str((plan or {}).get("reason") or "satisfied by prior geometry"),
)
if skipped_step is None:
raise AutonomousGenerationError("PLAN_SKIP_INVALID: no active modeling-plan step can be skipped")
concluded_plan_step_id = str(skipped_step.get("step_id") or concluded_plan_step_id)
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 {},
"plan_step_id": concluded_plan_step_id,
"geometry_fingerprint": fingerprint,
"working_head": self._head_key(task),
"recorded_at": now_iso(),
}
diagnosis["conclusion"] = conclusion
if decision == "skip":
# The no-op has been explained by current evidence. Clear
# the gate so authoring resumes at the next plan step.
state["geometry_rejection"] = {}
state["candidate_action_required"] = {}
self._clear_active_operation_contract(state)
else:
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,
"plan_step_id": concluded_plan_step_id,
})
events.append(("geometry_conclusion", {"taskId": task_id, "decision": decision, "rootCause": conclusion["root_cause"], "evidenceRefs": refs, "planStepId": concluded_plan_step_id, "nextPlanStepId": str(state.get("active_plan_step_id") or ""), "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")
plan_step_id = str(arguments.get("plan_step_id") or "").strip()
plan_action_id = str(arguments.get("plan_action_id") or "").strip()
planned_step: dict[str, Any] | None = None
planned_action: dict[str, Any] | None = None
if state.get("modeling_plan_enforced"):
if str(state.get("modeling_plan_status") or "") != "approved":
raise AutonomousGenerationError("PLAN_REQUIRED: modeling plan must be approved before CDSL generation")
planned_step = self._plan_current_step(state)
expected_step_id = str((planned_step or {}).get("step_id") or state.get("active_plan_step_id") or "")
# A semantic plan may not contain machine-readable step
# identifiers. Default an omitted ID to the active step;
# retain the mismatch guard only for explicit IDs so
# callers cannot accidentally advance a different step.
if not plan_step_id and expected_step_id:
plan_step_id = expected_step_id
if plan_step_id and expected_step_id and plan_step_id != expected_step_id:
raise AutonomousGenerationError(f"PLAN_STEP_MISMATCH: expected active plan step {expected_step_id or 'none'}, received {plan_step_id}")
step_status = state.get("plan_step_status") if isinstance(state.get("plan_step_status"), dict) else {}
missing_prerequisites = [
item for item in (planned_step or {}).get("prerequisites") or ()
if str(item) in step_status and step_status.get(str(item)) != "complete"
]
if missing_prerequisites:
raise AutonomousGenerationError("PLAN_PREREQUISITE_UNSATISFIED: active step prerequisites are incomplete: " + ", ".join(missing_prerequisites))
self._initialize_plan_actions(state)
planned_action = self._plan_current_action(state)
expected_action_id = str((planned_action or {}).get("action_id") or state.get("active_plan_action_id") or "")
if not plan_action_id and expected_action_id:
# Compatibility for persisted callers; the dynamic
# enforced schema always requires this field.
plan_action_id = expected_action_id
if not plan_action_id:
raise AutonomousGenerationError("PLAN_ACTION_REQUIRED: active plan action must be provided")
if planned_action is None:
raise AutonomousGenerationError(f"PLAN_ACTION_UNPLANNED: action {plan_action_id} is not part of the active plan step")
if expected_action_id and plan_action_id != expected_action_id:
known = {str(item.get("action_id") or "") for item in (planned_step or {}).get("actions") or () if isinstance(item, dict)}
if plan_action_id in known:
raise AutonomousGenerationError(f"PLAN_ACTION_MISMATCH: expected active plan action {expected_action_id}, received {plan_action_id}")
raise AutonomousGenerationError(f"PLAN_ACTION_UNPLANNED: action {plan_action_id} is not part of the active plan step")
action_status = state.get("plan_action_status") if isinstance(state.get("plan_action_status"), dict) else {}
if action_status.get(plan_action_id) in {"complete", "skipped", "satisfied_by_prior_step"}:
raise AutonomousGenerationError(f"PLAN_ACTION_ALREADY_COMPLETE: action {plan_action_id} is already complete")
action_records = [item for item in (planned_step or {}).get("actions") or () if isinstance(item, dict)]
planned_action = next((item for item in action_records if str(item.get("action_id") or "") == plan_action_id), planned_action)
action_prerequisites = [str(item) for item in (planned_action or {}).get("depends_on") or () if str(item)]
missing_actions = [item for item in action_prerequisites if action_status.get(item) not in {"complete", "skipped", "satisfied_by_prior_step"}]
if missing_actions:
raise AutonomousGenerationError("PLAN_ACTION_PREREQUISITE_UNSATISFIED: action prerequisites are incomplete: " + ", ".join(missing_actions))
batch_relationship = str(arguments.get("batch_relationship") or "").strip()
attempts = state.setdefault("candidate_attempts_by_head", {})
fragment: dict[str, Any] = {}
shared_revolve_axis = arguments.get("shared_revolve_axis")
self.store.append_agent_audit(task_id, "fragment", {
"fragment": arguments.get("fragment") if isinstance(arguments.get("fragment"), dict) else None,
"batch_goal": batch_goal,
"plan_step_id": plan_step_id or None,
"plan_action_id": plan_action_id or None,
"batch_relationship": batch_relationship or None,
"shared_revolve_axis": shared_revolve_axis,
"working_head": head,
})
fragment, _legacy_fragment = _fragment_argument(arguments)
feature_count = _fragment_feature_count(fragment)
active_contract = self._active_contract(task, state, engine)
if active_contract is None:
requested_operations = _fragment_atomic_ids(fragment)
# Retired persisted callers may still provide the old
# fragment_json string. Keep that restore path alive for
# recovery/tests, while every new structured tool call
# remains contract-first and canonical-only.
if _legacy_fragment and len(requested_operations) == 1 and requested_operations[0] in set(state.get("read_operation_contract_ids") or ()):
expected_atomic_id = requested_operations[0]
active_contract = _operation_contract_payload(engine, expected_atomic_id)
else:
suffix = (" first use of: " + ", ".join(requested_operations)) if requested_operations else ""
raise AutonomousGenerationError(
"OPERATION_CONTRACT_REQUIRED: call get_cdsl_operation_contract before submitting" + suffix
)
expected_atomic_id = str(active_contract.get("atomic_id") or "")
submitted_atomic_ids = _fragment_atomic_ids(fragment)
if feature_count != 1:
raise AutonomousGenerationError(
"CDSL_SCHEMA_INVALID: canonical autonomous fragments must contain exactly one feature"
)
if submitted_atomic_ids != [expected_atomic_id]:
received = ", ".join(submitted_atomic_ids) or "none"
raise AutonomousGenerationError(
f"OPERATION_CONTRACT_MISMATCH: expected {expected_atomic_id}, received {received}"
)
if planned_step is not None:
plan = state.get("modeling_plan") if isinstance(state.get("modeling_plan"), dict) else {}
planned_feature_records = [
item
for item in plan.get("features") or ()
if isinstance(item, dict) and str(item.get("feature_id") or "") in set(planned_step.get("feature_ids") or ())
]
planned_features = {str(item.get("operation") or "") for item in planned_feature_records}
submitted_operations = set(_fragment_atomic_ids(fragment))
# Only enforce operation membership when the plan actually
# contains recognisable Runtime atomic IDs. Natural
# language operation descriptions remain reviewer context.
enforce_operations = bool(planned_features) and all(
operation == "sketch_profile" or operation in getattr(engine, "SUPPORTED_ATOMIC_IDS", ())
for operation in planned_features
)
unplanned = sorted(item for item in submitted_operations if item and item not in planned_features) if enforce_operations else []
if unplanned:
raise AutonomousGenerationError("PLAN_FEATURE_UNPLANNED: fragment operation(s) are outside the active plan step: " + ", ".join(unplanned))
topology_operations = {
str(item.get("operation") or "") for item in planned_feature_records
if item.get("topology_sensitive") is True
}
if submitted_operations.intersection(topology_operations) and self._current_topology_observation(task_id, task, state) is None:
raise AutonomousGenerationError("TOPOLOGY_SNAPSHOT_OBSERVATION_REQUIRED: the active plan step contains a topology-sensitive feature and requires inspect_topology on the current checkpoint")
planned_operation = str((planned_action or {}).get("operation") or "").strip()
if planned_operation and planned_operation not in {"sketch_profile"} and planned_operation in set(getattr(engine, "SUPPORTED_ATOMIC_IDS", ())):
if expected_atomic_id != planned_operation:
raise AutonomousGenerationError(f"PLAN_ACTION_EXECUTION_SHAPE_INVALID: active action {plan_action_id} requires operation {planned_operation}, received {expected_atomic_id}")
if batch_relationship:
raise AutonomousGenerationError("CDSL_SCHEMA_INVALID: batch_relationship is not allowed for a single canonical operation")
if shared_revolve_axis is not None:
raise AutonomousGenerationError(
"CDSL_SCHEMA_INVALID: shared_revolve_axis is not accepted; put angle_deg and axis in the revolve feature params"
)
_, topology, _, _ = self._artifact_data(task_id, task)
supplied_tokens = _fragment_selector_tokens(fragment)
if supplied_tokens:
observation = self._current_topology_observation(task_id, task, state, topology=topology)
if observation is None:
raise AutonomousGenerationError(
"TOPOLOGY_SNAPSHOT_OBSERVATION_REQUIRED: selector_tokens require an inspect_topology result from the active snapshot; "
"do not reuse tokens after a checkpoint, rollback, or topology change"
)
observed_tokens = {
str(item.get("token") or "") for item in observation["tokens"]
if str(item.get("token") or "")
}
if any(token not in observed_tokens for token in supplied_tokens):
raise AutonomousGenerationError(
"TOPOLOGY_TOKEN_UNOBSERVED: selector_tokens must be copied exactly from the current inspect_topology result"
)
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
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=1,
source="tool_call",
allow_legacy_aliases=bool(_legacy_fragment),
expected_atomic_id=expected_atomic_id,
)
# 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 ""))
candidate["atomic_ids"] = submitted_atomic_ids
if plan_step_id:
candidate["plan_step_id"] = plan_step_id
candidate["plan_action_id"] = plan_action_id or None
candidate["plan_step_goal"] = str((planned_step or {}).get("goal") or "")
candidate["plan_action_title"] = str((planned_action or {}).get("title") or "")
write_json(self.store.candidate_dir(task_id, str(candidate["candidate_id"])) / "candidate.json", candidate)
state["format_correction"] = {}
state["geometry_rejection"] = {}
state["candidate_action_required"] = {}
state["edge_selector_recovery"] = {}
self._clear_active_operation_contract(state)
state["topology_observation"] = {}
state["engine_geometry_failure"] = {}
self._clear_active_operation_contract(state)
self._record(state, "candidate_built", {"message": "Candidate rebuilt successfully; independent visual review is running", "batch_goal": batch_goal, "plan_step_id": plan_step_id or None, "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 ""),
"plan_step_id": str(candidate.get("plan_step_id") or ""),
"plan_action_id": str(candidate.get("plan_action_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"),
"plan_step_id": candidate.get("plan_step_id") or "",
"plan_action_id": candidate.get("plan_action_id") or "",
"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_INVALID: rollback_checkpoint requires a token returned by inspect_model with rollback_allowed=true")
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_INVALID: rollback target must be a strict 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_CURRENT: the current checkpoint cannot be used as a rollback target; choose a strict ancestor, preferably its recommended parent")
best_known = str(state.get("best_known_checkpoint") or "")
reason = str(arguments.get("reason") or "").strip()
root_rollback_justified = any(
marker in reason.casefold()
for marker in (
"主体几何不可修复",
"主体不可修复",
"base geometry is unrepairable",
"base geometry cannot be repaired",
"unrepairable base geometry",
)
)
if not requested and current and best_known and not root_rollback_justified:
raise AutonomousGenerationError(
"ROLLBACK_WOULD_DISCARD_VALID_PROGRESS: root rollback requires an explicit主体几何不可修复 reason"
)
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._restore_plan_after_rollback(state, task, requested)
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": reason or "Rollback requested by author", "target_revision": requested or "root"})
events.append(("rollback", {"taskId": task_id, "reason": reason, "targetRevision": requested or "root"}))
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")
final_repair = state.get("final_repair")
if isinstance(final_repair, dict) and str(final_repair.get("working_head") or "") == self._head_key(task) and final_repair.get("evidence_stale_after_checkpoint"):
raise AutonomousGenerationError(
"STALE_EVIDENCE: final-repair evidence belongs to an older checkpoint; inspect the current revision before completion"
)
plan_error = self._plan_gate_error(state)
if plan_error:
raise AutonomousGenerationError(plan_error)
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
optional_finish = state.get("optional_finish") if isinstance(state.get("optional_finish"), dict) else None
if optional_finish and isinstance(optional_finish.get("warning"), dict):
self.store.update_task_fields(task_id, {"warnings": [optional_finish["warning"]]})
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 message.startswith("STALE_EVIDENCE:"):
state["last_diagnostic"] = _fragment_diagnostic(message)
self._record(state, "stale_evidence", {"message": message})
self.store.append_agent_audit(task_id, "tool-diagnostic", state["last_diagnostic"])
events.append(("tool_call", {"taskId": task_id, "tool": name, "status": "error", "message": _visible_error(message, request), "diagnostic": state["last_diagnostic"]}))
return events, False
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((
"TOPOLOGY_TOKEN_INVALID:",
"TOPOLOGY_SNAPSHOT_OBSERVATION_REQUIRED:",
"TOPOLOGY_TOKEN_UNOBSERVED:",
))
or (
message.startswith("HOLE_FRAGMENT_INVALID:")
and "requires exactly one face selector token" in message
)
):
if message.startswith("HOLE_FRAGMENT_INVALID:"):
# Retain every field-level hole correction while the
# action gate obtains a replacement face token.
state["format_correction"] = _format_correction_card(
engine,
fragment=_best_effort_fragment(arguments),
error_message=message,
head=self._head_key(task),
previous=state.get("format_correction") if isinstance(state.get("format_correction"), dict) else None,
)
else:
state["format_correction"] = {}
state["candidate_action_required"] = {
"working_head": self._head_key(task),
"reason": "topology_selector_recovery",
"instruction": "The fragment needs a current face selector token. Call inspect_topology for the active snapshot, then correct every retained field issue and copy only one returned face token into the replacement fragment or roll back.",
}
self._record(state, "topology_selector_recovery_required", {
"message": "A selector fragment must recover its token from the active topology snapshot before retrying.",
"error": message,
})
elif _is_engine_geometry_failure(message) and str((state.get("active_operation_contract") or {}).get("atomic_id") or "") in {"fillet", "chamfer"}:
# Fillet/chamfer are optional finishing operations. A
# failed BRep edge finish must not invalidate an already
# complete core model or masquerade as a schema error.
state["format_correction"] = {}
state["optional_finish"] = {
"status": "failed",
"atomic_id": str((state.get("active_operation_contract") or {}).get("atomic_id") or ""),
"error": message,
"warning": {
"code": "OPTIONAL_FINISH_SKIPPED",
"message": "Optional fillet/chamfer could not be applied; core geometry may still be completed.",
},
}
state["candidate_action_required"] = {
"working_head": self._head_key(task),
"reason": "optional_finish_failed",
"instruction": "Optional edge finishing failed. Do not retry formatting. Verify the core completion checklist; complete the task if all required items are independently complete, otherwise roll back or continue with a required operation.",
}
if state.get("modeling_plan_enforced"):
optional_step = self._plan_current_step(state)
optional_step_id = str((optional_step or {}).get("step_id") or state.get("active_plan_step_id") or "")
if optional_step_id:
state.setdefault("plan_step_status", {})[optional_step_id] = "skipped"
if isinstance(optional_step, dict):
optional_step["status"] = "skipped"
state["active_plan_step_id"] = ""
self._clear_active_operation_contract(state)
self._record(state, "optional_finish_failed", state["optional_finish"])
elif _is_engine_geometry_failure(message):
state["format_correction"] = {}
state["engine_geometry_failure"] = {
"working_head": self._head_key(task),
"error": message,
}
state["candidate_action_required"] = {
"working_head": self._head_key(task),
"reason": "engine_geometry_invalid",
"instruction": "The CDSL passed authoring validation but produced an invalid BRep or STEP artifact. Diagnose the unchanged checkpoint, then use one isolated replacement operation or roll back; do not treat this as a JSON format correction.",
}
self._record(state, "engine_geometry_invalid", {
"message": "Candidate failed in BRep/STEP execution rather than CDSL authoring validation.",
"error": message,
})
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)
attempted_fragment = _best_effort_fragment(arguments)
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"] = {}
state["candidate_action_required"] = {
"working_head": self._head_key(task),
"reason": "review_service_failure",
"instruction": "The candidate geometry was preserved, but the independent reviewer failed. Do not generate a CDSL repair. Retry review through the service or rollback only.",
"error": message,
}
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.",
"reason": "review_service_failure",
})
self.store.finish_generation(task_id, lifecycle="waiting_review", failure={
"schema_version": "cad.review-service-failure.v1",
"stage": "candidate_review",
"message": message,
})
elif message.startswith("GLOBAL_INVARIANT_VIOLATION:"):
state["format_correction"] = {}
state["candidate_action_required"] = {
"working_head": self._head_key(task),
"reason": "global_invariant_violation",
"instruction": "The candidate violates a global geometry constraint. Repair the reported invariant before adding unrelated features; do not treat this as a CDSL format correction.",
"error": message,
}
self._record(state, "global_invariant_violation", {
"message": message,
"working_head": self._head_key(task),
})
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 = _best_effort_fragment(arguments)
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,
})
else:
# A fragment rejected on this geometry is permanently
# blocked until the geometry head changes. Do not turn
# this deterministic duplicate into a generic format
# correction, which used to permit an endless loop.
state["format_correction"] = {}
state["candidate_action_required"] = {
"working_head": self._head_key(task),
"reason": "repeated_cdsl_attempt",
"instruction": "This fragment hash was already rejected on the current geometry. Submit a materially different canonical fragment or roll back to a strict ancestor; do not repeat it.",
"diagnostic_code": "REPEATED_CDSL_ATTEMPT",
}
self._record(state, "repeated_cdsl_attempt", {
"message": "A previously rejected fragment was submitted again on unchanged geometry.",
"diagnostic_code": "REPEATED_CDSL_ATTEMPT",
})
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),
})
elif message.startswith("BATCH_RELATIONSHIP_REQUIRED:"):
attempted_fragment = _best_effort_fragment(arguments) or {}
feature_count = _fragment_feature_count(attempted_fragment)
state["format_correction"] = {
"signature": f"{self._head_key(task)}|batch_relationship|{message}",
"repeat_count": 1,
"working_head": self._head_key(task),
"error": message,
"attempted_feature_count": feature_count,
"batch_relationship_required": True,
"instruction": (
"This fragment has multiple features. Add batch_relationship explaining their exact shared geometry, "
"host, sketch dependency, or ordered boolean result; split unrelated changes into separate candidates."
),
}
self._record(state, "format_correction", {
"message": "A multi-feature fragment needs an explicit relationship declaration.",
"attempted_feature_count": feature_count,
"error": message,
})
elif message.startswith("OPERATION_CONTRACT_REQUIRED:"):
raw_ids = message.partition("first use of:")[2]
required_ids = [item.strip() for item in raw_ids.split(",") if item.strip()]
state["format_correction"] = {}
state["candidate_action_required"] = {
"working_head": self._head_key(task),
"reason": "operation_contract_required",
"atomic_ids": required_ids,
"instruction": "Read the exact runtime contract for each listed operation before submitting this fragment again.",
}
self._record(state, "operation_contract_required", {
"message": "A new atomic operation must have its runtime contract read before first use.",
"atomic_ids": required_ids,
})
elif message.startswith("Unsupported runtime atomic_id:"):
unsupported = message.partition(":")[2].strip()
counts = state.setdefault("unsupported_operation_counts", {})
key = f"{self._head_key(task)}|{unsupported}"
count = int(counts.get(key) or 0) + 1
counts[key] = count
state["format_correction"] = {}
state["candidate_action_required"] = {
"working_head": self._head_key(task),
"reason": "unsupported_operation" if count < 2 else "unsupported_operation_limit",
"unsupported_atomic_id": unsupported,
"instruction": (
f"The operation {unsupported} is not supported by Runtime. Choose an atomic_id listed exactly in runtime_operations and read that contract."
if count < 2 else
"Repeated unsupported operation selection is blocked. Choose a listed Runtime operation or roll back; do not request this operation again."
),
}
self._record(state, "unsupported_operation", {
"message": "The requested atomic operation is not supported by Runtime.",
"atomic_id": unsupported,
"repeat_count": count,
})
elif message.startswith("CDSL_PROFILE_MULTIPLE_OUTERS"):
# Multiple outer loops normally describe independently
# located material targets. Retrying profile syntax cannot
# make a one-feature Runtime operation execute two actions,
# so return to planning without consuming schema retries or
# changing the current checkpoint.
state["format_correction"] = {}
state["modeling_plan_status"] = "revise"
state["candidate_action_required"] = {
"working_head": self._head_key(task),
"reason": "plan_action_split_required",
"diagnostic_code": "PLAN_ACTION_GRANULARITY_INVALID",
"plan_step_id": str(state.get("active_plan_step_id") or ""),
"plan_action_id": str(state.get("active_plan_action_id") or ""),
"error": message,
"instruction": (
"This fragment contains multiple independent outer profiles. Keep the related targets in the same "
"semantic step, revise that step to enumerate one action per target, then submit only the active "
"action as one fragment and one Runtime feature."
),
}
self._clear_active_operation_contract(state)
self._record(state, "plan_action_split_required", {
"message": "Multiple independent outer profiles require separate actions in the current semantic step.",
"diagnostic_code": "PLAN_ACTION_GRANULARITY_INVALID",
"plan_step_id": str(state.get("active_plan_step_id") or ""),
"plan_action_id": str(state.get("active_plan_action_id") or ""),
"working_head": self._head_key(task),
"candidate_preserved": False,
"checkpoint_preserved": True,
})
elif message.startswith(("CDSL_SCHEMA_INVALID:", "CDSL_SCHEMA_INVALID at ", "CDSL_CANONICAL_FORMAT_REQUIRED", "CDSL_PROFILE_INVALID", "CDSL_PROFILE_INNER_WITHOUT_OUTER", "CDSL_PROFILE_SELF_INTERSECTING", "OPERATION_CONTRACT_MISMATCH:")):
contract = self._active_contract(task, state)
atomic_id = str((contract or {}).get("atomic_id") or "")
key = f"{self._head_key(task)}|{atomic_id or 'unknown'}"
counts = state.setdefault("schema_retry_counts", {})
retry_count = int(counts.get(key) or 0) + 1
counts[key] = retry_count
state["candidate_action_required"] = {}
state["format_correction"] = {
"code": "CDSL_SCHEMA_INVALID" if not message.startswith("CDSL_CANONICAL_FORMAT_REQUIRED") else "CDSL_CANONICAL_FORMAT_REQUIRED",
"signature": f"{key}|{message}",
"repeat_count": retry_count,
"working_head": self._head_key(task),
"atomic_id": atomic_id or None,
"error": message,
"instruction": "Submit exactly one replacement fragment using canonical_fragment_schema. Do not reuse legacy aliases, feature arrays, wrappers, or fields from the invalid fragment.",
"replacement_schema": deepcopy((contract or {}).get("canonical_fragment_schema") or {}),
"canonical_example": deepcopy((contract or {}).get("canonical_fragment_example") or {}),
"retry_policy": {"max_retries": 1, "same_operation_only": True},
}
if retry_count > 1:
state["format_correction"]["instruction"] = "Canonical schema correction limit reached. Choose a different operation after rereading its contract or rollback; do not repeat this fragment."
state["candidate_action_required"] = {
"working_head": self._head_key(task),
"reason": "canonical_format_limit",
"instruction": "The canonical format retry limit is exhausted. Read a different operation contract or rollback; do not repeat the invalid fragment.",
}
self._record(state, "canonical_schema_error", {
"message": "The submitted fragment did not match the active canonical operation schema.",
"error": message,
"atomic_id": atomic_id,
"retry_count": retry_count,
})
else:
# A new authoring error supersedes a prior rejected
# candidate action on the same head. Leaving
# candidate_review_rejected here masks the exact contract
# correction on the next turn and traps the runner in the
# wrong action branch.
state["candidate_action_required"] = {}
correction = _format_correction_card(
engine,
fragment=_best_effort_fragment(arguments),
error_message=message,
head=self._head_key(task),
shared_revolve_axis=arguments.get("shared_revolve_axis") if isinstance(arguments.get("shared_revolve_axis"), dict) else None,
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})
diagnostic = _fragment_diagnostic(message, arguments=arguments)
diagnostic["tool"] = name
if name == "record_geometry_conclusion" and str(diagnostic.get("code") or "").startswith("GEOMETRY_CONCLUSION_EVIDENCE"):
rejection = state.get("geometry_rejection") if isinstance(state.get("geometry_rejection"), dict) else {}
fingerprint = str(rejection.get("geometry_fingerprint") or self._active_geometry_fingerprint(task_id, task))
diagnosis = ((state.get("geometry_diagnoses_by_fingerprint") or {}).get(fingerprint) or {})
observations = diagnosis.get("observations") if isinstance(diagnosis, dict) else {}
diagnostic.setdefault("evidence", {})["allowed_evidence_refs"] = sorted({
str(item.get("ref") or "")
for item in (observations or {}).values()
if isinstance(item, dict) and str(item.get("ref") or "")
})
state["last_diagnostic"] = diagnostic
self.store.append_agent_audit(task_id, "tool-diagnostic", diagnostic)
self._record(state, "tool_error", {"tool": name, "message": message, "diagnostic": diagnostic})
if name == "submit_cdsl_fragment":
events.append(("candidate_result", {"taskId": task_id, "status": "error", "message": _visible_error(message, request), "diagnostic": diagnostic}))
else:
events.append(("tool_call", {"taskId": task_id, "tool": name, "status": "error", "message": _visible_error(message, request), "diagnostic": diagnostic}))
return events, False