重构 改为标准化输出

This commit is contained in:
2026-09-01 14:10:23 +08:00
parent 7533b298f1
commit bc87bd6d2d
10151 changed files with 1709487 additions and 11776 deletions
@@ -0,0 +1 @@
"""Infrastructure adapters for the v3 ports."""
@@ -0,0 +1,400 @@
"""Immutable v3 file artifacts with staging manifests and atomic publication."""
from __future__ import annotations
from hashlib import sha256
import json
import os
from pathlib import Path
import re
import secrets
import shutil
from typing import Any
from app.cad_agent.ports import CandidateStage
_SAFE_RELATIVE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._/-]{0,240}$")
class FileArtifactStore:
def __init__(self, root: Path) -> None:
# Keep every externally reported artifact path in the same canonical
# form as staging and publication paths (notably /var vs /private/var
# on macOS).
self.root = root.resolve()
self.root.mkdir(parents=True, exist_ok=True)
def task_dir(self, task_id: str) -> Path:
if not re.fullmatch(r"cad_[a-z0-9]{12}", task_id):
raise ValueError("Invalid v3 task id")
return self.root / task_id
def artifact_path(self, task_id: str, relative_path: str) -> Path:
return self._path(task_id, relative_path)
def initialize_task(
self,
task_id: str,
request: str,
*,
source_blocks: list[dict[str, Any]] | None = None,
) -> None:
root = self.task_dir(task_id)
(root / "documents").mkdir(parents=True, exist_ok=True)
(root / "actions").mkdir(exist_ok=True)
(root / "revisions").mkdir(exist_ok=True)
(root / ".staging").mkdir(exist_ok=True)
source = root / "source-requirements.md"
blocks = self._source_blocks(request, source_blocks)
if not source.exists():
self._write_once(source, "\n\n".join(block["text"] for block in blocks) + "\n")
self.write_source_index(task_id, request, source_blocks=blocks)
def sync_action_ledger(self, task_id: str, events: list[dict[str, Any]]) -> str:
"""Mirror committed SQLite events into an append-only JSONL audit log.
SQLite remains authoritative. Replaying this method after an
interruption appends only missing committed sequences and rejects a
divergent line instead of rewriting audit history.
"""
path = self._path(task_id, "actions/action-ledger.jsonl")
existing: dict[int, dict[str, Any]] = {}
if path.is_file():
for raw in path.read_text(encoding="utf-8").splitlines():
if not raw.strip():
continue
value = json.loads(raw)
sequence = value.get("sequence") if isinstance(value, dict) else None
if not isinstance(sequence, int) or sequence < 1:
raise ValueError("Action ledger contains an invalid sequence")
existing[sequence] = value
missing: list[str] = []
for event in events:
sequence = event.get("sequence")
if not isinstance(sequence, int) or sequence < 1:
raise ValueError("Action ledger event has an invalid sequence")
entry = {"schema_version": "cad.action-ledger.v1", **event}
previous = existing.get(sequence)
if previous is not None:
if previous != entry:
raise ValueError("Action ledger diverges from committed SQLite event")
continue
missing.append(json.dumps(entry, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + "\n")
if missing:
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("a", encoding="utf-8") as handle:
handle.writelines(missing)
handle.flush()
os.fsync(handle.fileno())
return "actions/action-ledger.jsonl"
def write_source_index(
self,
task_id: str,
request: str,
*,
source_blocks: list[dict[str, Any]] | None = None,
) -> dict[str, str]:
root = self.task_dir(task_id)
source = root / "source-requirements.md"
if source_blocks is None:
text = source.read_text(encoding="utf-8") if source.is_file() else request
blocks = self._source_blocks(text, None)
else:
blocks = self._source_blocks(request, source_blocks)
index = {f"src_{position:03d}": block["text"] for position, block in enumerate(blocks, 1)}
attachment_blocks = {
f"src_{position:03d}": block["attachment"]
for position, block in enumerate(blocks, 1)
if isinstance(block.get("attachment"), dict)
}
payload: dict[str, Any] = {"schema_version": "cad.source-index.v1", "sources": index}
if attachment_blocks:
payload["attachment_blocks"] = attachment_blocks
self._write_json_once(root / "documents" / "source-index.json", payload)
return index
@staticmethod
def _source_blocks(request: str, source_blocks: list[dict[str, Any]] | None) -> list[dict[str, Any]]:
"""Return immutable source paragraphs and attachment blocks.
Source identifiers are assigned only after this method returns, so
callers cannot choose IDs. Attachment metadata is deliberately a
closed, server-provided projection: it lets a reviewer trace the
source without treating upload metadata as arbitrary LLM input.
"""
supplied = source_blocks
if supplied is None:
supplied = [{"text": value.strip()} for value in re.split(r"\n\s*\n", request) if value.strip()]
blocks: list[dict[str, Any]] = []
for item in supplied:
if not isinstance(item, dict):
raise ValueError("Source blocks must be objects")
text = item.get("text")
if not isinstance(text, str) or not text.strip():
raise ValueError("Every source block requires non-empty text")
if len(text) > 30_000:
raise ValueError("Source block exceeds the 30000 character limit")
block: dict[str, Any] = {"text": text.strip()}
attachment = item.get("attachment")
if attachment is not None:
if not isinstance(attachment, dict):
raise ValueError("Source attachment metadata must be an object")
allowed = {"attachment_id", "name", "kind", "mime", "sha256"}
if set(attachment) - allowed:
raise ValueError("Source attachment metadata contains unsupported fields")
attachment_id = attachment.get("attachment_id")
digest = attachment.get("sha256")
if not isinstance(attachment_id, str) or not re.fullmatch(r"[A-Za-z0-9_-]{1,128}", attachment_id):
raise ValueError("Source attachment id is invalid")
if not isinstance(digest, str) or not re.fullmatch(r"[a-f0-9]{64}", digest):
raise ValueError("Source attachment SHA-256 is invalid")
block["attachment"] = {
"attachment_id": attachment_id,
"name": str(attachment.get("name") or "attachment")[:255],
"kind": str(attachment.get("kind") or "document")[:32],
"mime": str(attachment.get("mime") or "application/octet-stream")[:128],
"sha256": digest,
}
blocks.append(block)
if not blocks:
raise ValueError("At least one source block is required")
return blocks
def read_source_index(self, task_id: str) -> dict[str, str]:
payload = self.read_json(task_id, "documents/source-index.json") or {}
sources = payload.get("sources") if isinstance(payload.get("sources"), dict) else {}
return {str(key): str(value) for key, value in sources.items()}
def read_source_requirements(self, task_id: str) -> str:
path = self.task_dir(task_id) / "source-requirements.md"
return path.read_text(encoding="utf-8") if path.is_file() else ""
def read_requirements_draft(self, task_id: str, artifact_path: str = "") -> dict[str, Any]:
if artifact_path:
value = self.read_json(task_id, artifact_path)
if value is None:
raise ValueError("Committed requirements draft artifact is unavailable")
return value
documents = self.task_dir(task_id) / "documents"
candidates = sorted(documents.glob("requirements-draft-v*.json"))
if not candidates:
return {"schema_version": "cad.requirements-draft.v1", "revision": 0, "items": []}
value = json.loads(candidates[-1].read_text(encoding="utf-8"))
return value if isinstance(value, dict) else {"schema_version": "cad.requirements-draft.v1", "revision": 0, "items": []}
def write_requirements_draft(self, task_id: str, payload: dict[str, Any], *, invocation_id: str) -> str:
revision = int(payload.get("revision") or 0)
if revision < 1:
raise ValueError("Requirements draft revision is invalid")
return self._write_invocation_json(task_id, f"requirements-draft-v{revision}", payload, invocation_id)
def read_requirements_review(self, task_id: str, artifact_path: str = "") -> dict[str, Any] | None:
return self.read_json(task_id, artifact_path) if artifact_path else None
def write_requirements_review(self, task_id: str, payload: dict[str, Any], *, invocation_id: str) -> str:
return self._write_invocation_json(task_id, "requirements-review", payload, invocation_id)
def read_requirements_contract(self, task_id: str, artifact_path: str = "") -> dict[str, Any] | None:
# State points to the immutable artifact used as program input. The
# fixed name is a read-only convenience view for terminal tasks.
return self.read_json(task_id, artifact_path or "requirements-contract.json")
def write_requirements_contract(self, task_id: str, payload: dict[str, Any], *, invocation_id: str = "") -> str:
if not invocation_id:
return self.write_json_once(task_id, "requirements-contract.json", payload)
return self._write_invocation_json(task_id, "requirements-contract", payload, invocation_id)
def read_json(self, task_id: str, relative_path: str) -> dict[str, Any] | None:
path = self._path(task_id, relative_path)
if not path.is_file():
return None
value = json.loads(path.read_text(encoding="utf-8"))
return value if isinstance(value, dict) else None
def write_json_once(self, task_id: str, relative_path: str, payload: dict[str, Any]) -> str:
path = self._path(task_id, relative_path)
self._write_json_once(path, payload)
return relative_path
def write_text_once(self, task_id: str, relative_path: str, text: str) -> str:
path = self._path(task_id, relative_path)
self._write_once(path, text)
return relative_path
def read_active_cdsl(self, task_id: str, revision_id: str) -> dict[str, Any] | None:
return self.read_json(task_id, f"revisions/{revision_id}/model.cdsl.json") if revision_id else None
def read_topology(self, task_id: str, revision_id: str) -> dict[str, Any] | None:
return self.read_json(task_id, f"revisions/{revision_id}/model.topology.json") if revision_id else None
def start_candidate_stage(self, task_id: str, idempotency_key: str, payload: dict[str, Any]) -> CandidateStage:
root = self.task_dir(task_id) / ".staging"
stable_id = "stage_" + sha256(idempotency_key.encode("utf-8")).hexdigest()[:20]
directory = root / stable_id
directory.mkdir(parents=True, exist_ok=True)
self._write_json_once(directory / "input.json", payload)
return CandidateStage(stable_id, str(directory.resolve()))
def stage_output_dir(self, task_id: str, stage_id: str) -> str:
return str(self._stage_path(task_id, stage_id, ""))
def write_stage_json(self, task_id: str, stage_id: str, relative_path: str, payload: dict[str, Any]) -> str:
path = self._stage_path(task_id, stage_id, relative_path)
self._write_json_once(path, payload)
return relative_path
def read_stage_json(self, task_id: str, stage_id: str, relative_path: str) -> dict[str, Any] | None:
path = self._stage_path(task_id, stage_id, relative_path)
if not path.is_file():
return None
value = json.loads(path.read_text(encoding="utf-8"))
return value if isinstance(value, dict) else None
def publish_candidate(self, task_id: str, stage_id: str, revision_id: str) -> dict[str, str]:
source = self._stage_path(task_id, stage_id, "")
target = self._path(task_id, f"revisions/{revision_id}")
if target.exists():
manifest = self._manifest(target)
if manifest is None:
raise RuntimeError("Published revision has no valid manifest")
return {key: f"revisions/{revision_id}/{key}" for key in manifest["files"]}
# Render/rebuild reports contain paths for the reviewer. Rebase those
# paths while artifacts are still mutable staging output, so they point
# at the revision after the atomic directory rename.
self._rebase_staged_paths(source, target)
manifest = self._create_manifest(source)
self._write_json_once(source / "manifest.json", manifest)
target.parent.mkdir(parents=True, exist_ok=True)
os.replace(source, target)
return {key: f"revisions/{revision_id}/{key}" for key in manifest["files"]}
def find_published_candidate(self, task_id: str, stage_id: str) -> tuple[str, dict[str, Any]] | None:
"""Find a manifest-verified candidate already renamed before its DB CAS.
Publishing artifacts and advancing SQLite cannot share a transaction.
The stage id persisted inside ``candidate.json`` makes a post-rename
recovery deterministic and prevents another build or revision.
"""
revisions = self.task_dir(task_id) / "revisions"
if not revisions.is_dir():
return None
for revision in sorted(revisions.iterdir()):
if not revision.is_dir() or self._manifest(revision) is None:
continue
candidate_path = revision / "candidate.json"
if not candidate_path.is_file():
continue
try:
candidate = json.loads(candidate_path.read_text(encoding="utf-8"))
except json.JSONDecodeError:
continue
if isinstance(candidate, dict) and candidate.get("stage_id") == stage_id:
return revision.name, candidate
return None
def recover_staged_candidates(self, task_id: str, referenced_stage_ids: set[str]) -> None:
root = self.task_dir(task_id) / ".staging"
if not root.is_dir():
return
for directory in root.iterdir():
if not directory.is_dir() or directory.name in referenced_stage_ids:
continue
shutil.rmtree(directory)
def _path(self, task_id: str, relative_path: str) -> Path:
if relative_path and (not _SAFE_RELATIVE.fullmatch(relative_path) or ".." in Path(relative_path).parts):
raise ValueError("Invalid artifact relative path")
root = self.task_dir(task_id).resolve()
target = (root / relative_path).resolve()
if target != root and root not in target.parents:
raise ValueError("Artifact path escapes its task")
return target
def _stage_path(self, task_id: str, stage_id: str, relative_path: str) -> Path:
if not re.fullmatch(r"stage_[a-f0-9]{20}", stage_id):
raise ValueError("Invalid candidate stage id")
root = self.task_dir(task_id).resolve()
stage = (root / ".staging" / stage_id).resolve()
if root not in stage.parents:
raise ValueError("Stage path escapes task directory")
if relative_path and (not _SAFE_RELATIVE.fullmatch(relative_path) or ".." in Path(relative_path).parts):
raise ValueError("Invalid stage artifact path")
path = (stage / relative_path).resolve()
if path != stage and stage not in path.parents:
raise ValueError("Stage artifact path escapes staging directory")
return path
@staticmethod
def _write_once(path: Path, text: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
encoded = text.encode("utf-8")
if path.exists():
if path.read_bytes() != encoded:
raise ValueError(f"Immutable artifact already exists: {path.name}")
return
temporary = path.with_name(path.name + ".tmp-" + secrets.token_hex(4))
temporary.write_bytes(encoded)
os.replace(temporary, path)
def _write_invocation_json(self, task_id: str, stem: str, payload: dict[str, Any], invocation_id: str) -> str:
digest = sha256(json.dumps(payload, ensure_ascii=True, sort_keys=True, separators=(",", ":")).encode("utf-8")).hexdigest()[:12]
safe_invocation = re.sub(r"[^A-Za-z0-9_-]", "", invocation_id)[:32]
if not safe_invocation:
raise ValueError("Artifact invocation ID is invalid")
return self.write_json_once(task_id, f"documents/{stem}-{digest}-{safe_invocation}.json", payload)
@classmethod
def _write_json_once(cls, path: Path, payload: dict[str, Any]) -> None:
cls._write_once(path, json.dumps(payload, ensure_ascii=False, sort_keys=True, indent=2) + "\n")
@staticmethod
def _create_manifest(directory: Path) -> dict[str, Any]:
files: dict[str, str] = {}
for path in sorted(directory.rglob("*")):
if not path.is_file() or path.name == "manifest.json":
continue
relative = path.relative_to(directory).as_posix()
files[relative] = sha256(path.read_bytes()).hexdigest()
if not files:
raise RuntimeError("Candidate staging directory has no artifacts")
return {"schema_version": "cad.v3.artifact-manifest.v1", "files": files}
@staticmethod
def _manifest(directory: Path) -> dict[str, Any] | None:
path = directory / "manifest.json"
if not path.is_file():
return None
value = json.loads(path.read_text(encoding="utf-8"))
files = value.get("files") if isinstance(value, dict) else None
if not isinstance(files, dict):
return None
for relative, digest in files.items():
candidate = directory / str(relative)
if not candidate.is_file() or sha256(candidate.read_bytes()).hexdigest() != digest:
return None
return value
@classmethod
def _rebase_staged_paths(cls, source: Path, target: Path) -> None:
source_text = str(source)
target_text = str(target)
def rebase(value: Any) -> Any:
if isinstance(value, str):
return target_text + value.removeprefix(source_text) if value.startswith(source_text) else value
if isinstance(value, list):
return [rebase(item) for item in value]
if isinstance(value, dict):
return {key: rebase(item) for key, item in value.items()}
return value
for path in source.rglob("*.json"):
try:
value = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
continue
rebased = rebase(value)
if rebased != value:
path.write_text(json.dumps(rebased, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
@@ -0,0 +1,28 @@
"""In-process idempotent event delivery for the v3 delivery boundary."""
from __future__ import annotations
from copy import deepcopy
from typing import Any
class IdempotentInProcessPublisher:
"""A small delivery adapter with durable-outbox compatible semantics.
The process-local sink is intentionally only a delivery mechanism: SQLite
remains the durable event source. The event ID is retained so a later
durable broker/websocket adapter can make the same idempotency guarantee.
"""
def __init__(self) -> None:
self._seen_event_ids: set[int] = set()
self.delivered: list[dict[str, Any]] = []
async def publish(self, event: dict[str, Any]) -> None:
event_id = event.get("event_id")
if not isinstance(event_id, int):
raise ValueError("Published outbox event has no integer event_id")
if event_id in self._seen_event_ids:
return
self._seen_event_ids.add(event_id)
self.delivered.append(deepcopy(event))
@@ -0,0 +1,58 @@
"""Independent rendered-review adapter for protocol v3."""
from __future__ import annotations
import base64
import json
from pathlib import Path
from typing import Any
from app.cad_agent.adapters.structured_llm import StructuredModelGateway
from app.cad_agent.ports import AdapterUnavailable
class RenderedReviewGateway:
def __init__(self, models: StructuredModelGateway) -> None:
self.models = models
async def review(self, *, kind: str, payload: dict[str, Any], tool: dict[str, Any], provider_id: str, model_id: str) -> dict[str, Any]:
name = str((tool.get("function") or {}).get("name") or "")
if not name:
raise RuntimeError("Review tool is missing a name")
content: list[dict[str, Any]] = [{"type": "text", "text": json.dumps(payload, ensure_ascii=False)}]
if kind in {"candidate", "final"}:
manifest = payload.get("render_manifest") if isinstance(payload.get("render_manifest"), dict) else {}
for path in self._evidence_paths(manifest):
content.append(self._image_part(path))
return await self.models.call_tool(
messages=[
{"role": "system", "content": "You are an independent CAD reviewer. Inspect supplied deterministic facts and rendered images. Return only the specified structured tool call."},
{"role": "user", "content": content},
],
tool=tool,
provider_id=provider_id,
model_id=model_id,
required_tool_name=name,
)
@staticmethod
def _evidence_paths(manifest: dict[str, Any]) -> list[Path]:
selected: list[Path] = []
contact = Path(str(manifest.get("contact_sheet_path") or ""))
if contact.is_file():
selected.append(contact)
wanted = {"top", "front", "right", "isometric"}
for item in manifest.get("views") or ():
if not isinstance(item, dict) or str(item.get("id") or "") not in wanted:
continue
path = Path(str(item.get("path") or ""))
if path.is_file():
selected.append(path)
if not selected:
raise AdapterUnavailable("RENDER_SERVICE_UNAVAILABLE: review render evidence is unavailable")
return selected[:5]
@staticmethod
def _image_part(path: Path) -> dict[str, Any]:
media_type = "image/jpeg" if path.suffix.lower() in {".jpg", ".jpeg"} else "image/png"
return {"type": "image_url", "image_url": {"url": f"data:{media_type};base64,{base64.b64encode(path.read_bytes()).decode('ascii')}"}}
+690
View File
@@ -0,0 +1,690 @@
"""CDSL runtime adapter driven exclusively by profile operation contracts."""
from __future__ import annotations
from copy import deepcopy
from hashlib import sha256
import json
import math
from pathlib import Path
from typing import Any
from app.cad_agent.domain.operation_contract import (
SEMANTIC_PREFLIGHT_NAMES,
OperationContractError,
canonical_hash,
validate_fragment,
validate_operation_contract,
)
from app.cad_agent.domain.verifier_registry import default_registry
from app.services.engine_service import load_engine, topology_snapshot, validate_cdsl
from app.services.review_renderer import ReviewRenderError, render_checkpoint
from app.settings import Settings
from vendor.cdsl_preview_runtime import step_to_glb
class RuntimeAdapterError(RuntimeError):
pass
class ProfileCadRuntime:
"""Adapter that owns engine imports; application code sees only its port."""
def __init__(self, settings: Settings) -> None:
self.settings = settings
self.engine = load_engine(settings)
self._semantic_preflight_handlers = {
"sketch_workplane": self._preflight_sketch_workplane,
"profile_non_self_intersecting": self._preflight_profile_non_self_intersecting,
"host_face_exists": self._preflight_host_face_exists,
"hole_positions_on_host_plane": self._preflight_hole_positions_on_host_plane,
"cut_exit_distance": self._preflight_cut_exit_distance,
"requires_active_solid": self._preflight_requires_active_solid,
"revolve_axis_on_sketch": self._preflight_revolve_axis_on_sketch,
"reference_plane_nonzero_normal": self._preflight_reference_plane_nonzero_normal,
"reference_axis_nonzero_direction": self._preflight_reference_axis_nonzero_direction,
"selected_edges_exist": self._preflight_selected_edges_exist,
"source_features_exist": self._preflight_source_features_exist,
"mirror_plane_exists": self._preflight_mirror_plane_exists,
}
schema_path = Path(str(self.engine.__file__)).with_name("profile_schema.json")
try:
self._profile = json.loads(schema_path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as error:
raise RuntimeAdapterError("RUNTIME_CONTRACT_INVALID: profile schema is unavailable") from error
self._contracts = self._profile.get("operation_contracts")
if not isinstance(self._contracts, dict):
raise RuntimeAdapterError("RUNTIME_CONTRACT_INVALID: profile has no v3 operation contracts")
declared = {str(item) for item in self._contracts}
registered = {str(item) for item in getattr(self.engine, "SUPPORTED_ATOMIC_IDS", ())}
if declared != registered:
raise RuntimeAdapterError("RUNTIME_CONTRACT_INVALID: runtime and profile operation registries disagree")
verifier_kinds = set(default_registry().claim_kinds)
for contract in self._contracts.values():
try:
validate_operation_contract(contract)
except OperationContractError as error:
raise RuntimeAdapterError(f"RUNTIME_CONTRACT_INVALID: {error}") from error
unknown_preflights = set(contract["semantic_preflight"]) - set(self._semantic_preflight_handlers)
unknown_verifiers = set(contract["candidate_verifiers"]) - verifier_kinds
if unknown_preflights or unknown_verifiers:
raise RuntimeAdapterError(
"RUNTIME_CONTRACT_INVALID: operation contract references an unavailable "
f"{'preflight' if unknown_preflights else 'candidate verifier'}"
)
if set(self._semantic_preflight_handlers) != SEMANTIC_PREFLIGHT_NAMES:
raise RuntimeAdapterError("RUNTIME_CONTRACT_INVALID: runtime preflight registry is incomplete")
def supported_atomic_ids(self) -> tuple[str, ...]:
return tuple(sorted(self._contracts))
def operation_contract(self, atomic_id: str) -> dict[str, Any]:
contract = self._contracts.get(atomic_id)
if not isinstance(contract, dict):
raise RuntimeAdapterError(f"RUNTIME_CONTRACT_INVALID: unknown operation {atomic_id}")
result = deepcopy(contract)
result["contract_hash"] = canonical_hash(contract)
result["registry_revision"] = str(self._profile.get("schema_version") or "")
return result
def selector_tokens(self, topology: dict[str, Any] | None) -> dict[str, dict[str, Any]]:
if not isinstance(topology, dict):
return {}
snapshot_id = str(topology.get("snapshot_id") or "")
if not snapshot_id:
return {}
result: dict[str, dict[str, Any]] = {}
for record in topology.get("records") or ():
if not isinstance(record, dict) or not record.get("executable"):
continue
record_id = str(record.get("record_id") or "")
kind = str(record.get("kind") or "")
if not record_id or kind not in {"face", "edge", "plane", "axis", "body", "vertex"}:
continue
token = "sel_" + sha256(f"{snapshot_id}|{record_id}".encode("utf-8")).hexdigest()[:16]
geometry = deepcopy(record.get("geometry") or {})
owners = record.get("owner_feature_ids") or [record.get("feature_id") or ""]
result[token] = {
"token": token,
"kind": kind,
"snapshot_id": snapshot_id,
"selector": {"kind": kind, "stable_id": record_id, "owner_feature_id": str(owners[0] or ""), "geometry": geometry, "source": "runtime_snapshot", "snapshot_id": snapshot_id, "confidence": 1.0},
"geometry": geometry,
}
return result
def reference_tokens(self, cdsl: dict[str, Any] | None) -> dict[str, str]:
"""Return opaque, head-scoped feature references for pattern contracts."""
result: dict[str, str] = {}
document_hash = canonical_hash(cdsl) if isinstance(cdsl, dict) else "root"
for feature in (cdsl or {}).get("features") or ():
if not isinstance(feature, dict) or not isinstance(feature.get("id"), str) or not feature["id"]:
continue
feature_id = feature["id"]
result["ref_" + sha256(f"{document_hash}|{feature_id}".encode("utf-8")).hexdigest()[:16]] = feature_id
return result
def materialize_fragment(self, base_cdsl: dict[str, Any] | None, fragment: dict[str, Any], contract: dict[str, Any], selector_tokens: dict[str, dict[str, Any]], reference_tokens: dict[str, str], *, require_through: bool = False) -> tuple[dict[str, Any], dict[str, Any]]:
# ActionCommandHandler validates the exposed schema first, but this
# adapter is also used during crash recovery. Keep the runtime boundary
# self-contained so a corrupted/replayed staged payload cannot produce
# a candidate merely by bypassing that handler-level validation.
try:
validate_operation_contract(contract)
except OperationContractError as error:
raise RuntimeAdapterError(f"RUNTIME_CONTRACT_INVALID: {error}") from error
current = self.operation_contract(str(contract.get("atomic_id") or ""))
if (
contract.get("contract_hash") != current["contract_hash"]
or contract.get("registry_revision") != current["registry_revision"]
):
raise RuntimeAdapterError("RUNTIME_CONTRACT_INVALID: operation contract is not the current verified registry entry")
selector_shape = str((contract.get("fragment_shape") or {}).get("selector_tokens") or "forbidden")
selector_kind = str((contract.get("selector_policy") or {}).get("token_kind") or "")
allowed_selectors = [
token
for token, value in selector_tokens.items()
if selector_shape == "required"
and isinstance(value, dict)
and value.get("kind") == selector_kind
]
errors = validate_fragment(
contract,
fragment,
selector_tokens=allowed_selectors,
reference_tokens=list(reference_tokens),
)
if errors:
raise RuntimeAdapterError(
"RUNTIME_PRECONDITION_FAILED: fragment no longer matches the active operation schema: "
+ errors[0]["message"]
)
materialized = deepcopy(fragment)
reference = contract["reference_policy"]
if reference["mode"] == "snapshot_bound":
slot = str(reference["slot"]).removeprefix("params.")
supplied = materialized.get("feature", {}).get("params", {}).get(slot, [])
if not isinstance(supplied, list) or not all(token in reference_tokens for token in supplied):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: reference token is absent or stale")
materialized["feature"]["params"][slot] = [reference_tokens[token] for token in supplied]
self._semantic_preflight(materialized, contract, selector_tokens, base_cdsl, require_through=require_through)
document = deepcopy(base_cdsl) if isinstance(base_cdsl, dict) else {
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "agent_preflight", "geometry": {"sketches": []}, "features": [],
}
geometry = document.setdefault("geometry", {})
sketches = geometry.setdefault("sketches", []) if isinstance(geometry, dict) else None
features = document.setdefault("features", [])
if not isinstance(sketches, list) or not isinstance(features, list):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: base CDSL collections are invalid")
index = len(features) + 1
feature = materialized["feature"]
output = {"id": f"feature_{index:03d}", "atomic_id": contract["atomic_id"], "params": deepcopy(feature["params"]), "depends_on": [str(features[-1].get("id"))] if features else []}
if contract["fragment_shape"]["sketch"] == "required":
sketch_id = f"sketch_{len(sketches) + 1:03d}"
sketch = materialized["sketch"]
sketches.append({"id": sketch_id, "workplane": deepcopy(sketch["workplane"]), "profile": deepcopy(sketch["profile"])})
output["sketch_id"] = sketch_id
selected = [selector_tokens[token]["selector"] for token in feature.get("selector_tokens", [])]
slot = contract["selector_policy"]["slot"]
if slot == "params.host_face":
# A topology selector is authoritative while accepting the
# action, but a later pattern replays this feature after its own
# cut may have split the selected B-rep face. Lower the accepted
# planar selector to a concrete, world-aligned host frame and
# transform the author-supplied world coordinates into that
# frame. The token remains in the fragment audit for provenance;
# the materialized CDSL is stable under replay.
output["params"]["host_face"], output["params"]["positions"] = self._materialized_hole_host_frame(
selector_tokens[str(feature["selector_tokens"][0])],
output["params"].get("positions"),
)
elif slot == "params.mirror_plane":
output["params"]["mirror_plane"] = selected[0]
elif slot == "feature.selectors":
output["selectors"] = selected
features.append(output)
try:
self._validate_finite_tree(document)
self._validate_materialized_runtime_types(output)
validate_cdsl(document, self.engine)
except Exception as error:
message = str(error)
# A JSON-schema failure after server materialization is a registry
# drift: the author could not have supplied the missing server
# field. Engine capability analysis, however, can reject a
# schema-valid author sketch (for example an unresolvable analytic
# contour). That is a recoverable action precondition failure,
# not a deployment defect.
if "CDSL engine runtime preflight failed:" in message:
raise RuntimeAdapterError(
"RUNTIME_PRECONDITION_FAILED: materialized fragment is not executable by the engine: "
+ message
) from error
raise RuntimeAdapterError(
"RUNTIME_CONTRACT_INVALID: materialized fragment violates the engine CDSL schema: "
+ message
) from error
return document, {"schema_version": "cad.v3.fragment-audit.v1", "atomic_id": contract["atomic_id"], "fragment_hash": canonical_hash(fragment), "contract_hash": contract["contract_hash"], "assigned_feature_ids": [output["id"]], "assigned_sketch_ids": [output["sketch_id"]] if output.get("sketch_id") else [], "selector_snapshot_id": next(iter(selector_tokens.values()), {}).get("snapshot_id", ""), "selector_tokens": list(feature.get("selector_tokens", [])), "reference_snapshot_id": canonical_hash(reference_tokens), "reference_tokens": list(fragment.get("feature", {}).get("params", {}).get(str(reference.get("slot") or "").removeprefix("params."), [])) if reference["mode"] == "snapshot_bound" else []}
def rebuild(self, cdsl: dict[str, Any], output_dir: str, task_id: str, revision_id: str) -> dict[str, Any]:
root = Path(output_dir)
root.mkdir(parents=True, exist_ok=True)
cdsl_copy = deepcopy(cdsl)
cdsl_copy["part_id"] = task_id
step_path = root / "model.step"
glb_path = root / "model.glb"
self._write_json(root / "model.cdsl.json", cdsl_copy)
try:
self._validate_finite_tree(cdsl_copy)
validate_cdsl(cdsl_copy, self.engine)
except RuntimeAdapterError:
raise
except Exception as error:
raise RuntimeAdapterError(f"RUNTIME_PRECONDITION_FAILED: persisted CDSL failed rebuild preflight: {error}") from error
try:
engine_result = self.engine.run_cdsl_only(cdsl_copy, step_path)
if str(engine_result.get("engine") or "") != "cdsl_only":
raise RuntimeAdapterError("engine did not execute CDSL-only rebuild")
preview = step_to_glb(step_path, glb_path)
health = self._health(engine_result, step_path, glb_path)
topology = topology_snapshot(engine_result, task_id=task_id, revision_id=revision_id, preview=preview)
self._write_json(root / "model.topology.json", topology)
manifest = render_checkpoint(self.settings, step_path=step_path, output_dir=root / "renders")
report = {"engine_result": engine_result, "preview": preview, "health": health, "render_manifest": manifest}
self._write_json(root / "rebuild-report.json", report)
return {"health": health, "topology": topology, "report": report, "render_manifest": manifest, "paths": {"cdsl": "model.cdsl.json", "step": "model.step", "glb": "model.glb", "topology": "model.topology.json", "report": "rebuild-report.json", "render_manifest": "renders/render-manifest.json"}}
except OSError:
# Artifact writes are a recoverable infrastructure outage. Let the
# application handler park the same candidate stage for replay.
raise
except ReviewRenderError as error:
raise RuntimeAdapterError(f"RENDER_SERVICE_UNAVAILABLE: {error}") from error
except Exception as error:
raise RuntimeAdapterError(f"RUNTIME_EXECUTION_FAILURE: {error}") from error
def _semantic_preflight(self, fragment: dict[str, Any], contract: dict[str, Any], selector_tokens: dict[str, dict[str, Any]], base_cdsl: dict[str, Any] | None, *, require_through: bool) -> None:
if fragment.get("feature", {}).get("atomic_id") != contract.get("atomic_id"):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: atomic_id does not match active contract")
policy = contract["selector_policy"]
supplied = fragment.get("feature", {}).get("selector_tokens", [])
if contract["fragment_shape"]["selector_tokens"] == "required":
if not all(token in selector_tokens and selector_tokens[token]["kind"] == policy["token_kind"] for token in supplied):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: selector token is absent, stale, or has the wrong kind")
for name in contract["semantic_preflight"]:
self._semantic_preflight_handlers[name](fragment, selector_tokens, base_cdsl, require_through)
def _preflight_sketch_workplane(self, fragment: dict[str, Any], _selectors: dict[str, dict[str, Any]], _base: dict[str, Any] | None, _require_through: bool) -> None:
sketch = fragment.get("sketch")
workplane = sketch.get("workplane") if isinstance(sketch, dict) else None
if not isinstance(workplane, dict):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: sketch workplane is unavailable")
normal, x_dir = workplane.get("normal"), workplane.get("x_dir")
if not isinstance(normal, list) or not isinstance(x_dir, list) or self._norm(normal) <= 1e-9 or self._norm(x_dir) <= 1e-9 or self._norm(self._cross(normal, x_dir)) <= 1e-9:
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: sketch workplane vectors are degenerate")
def _preflight_profile_non_self_intersecting(self, fragment: dict[str, Any], _selectors: dict[str, dict[str, Any]], _base: dict[str, Any] | None, _require_through: bool) -> None:
sketch = fragment.get("sketch")
profile = sketch.get("profile") if isinstance(sketch, dict) else None
if not isinstance(profile, dict):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: sketch profile is unavailable")
if profile.get("type") == "polygon":
vertices = profile.get("vertices")
if not isinstance(vertices, list) or len(vertices) < 3 or self._polygon_self_intersects(vertices):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: polygon profile self-intersects")
elif profile.get("type") == "analytic_contours":
self._preflight_analytic_contours(profile)
def _preflight_host_face_exists(self, fragment: dict[str, Any], selectors: dict[str, dict[str, Any]], _base: dict[str, Any] | None, _require_through: bool) -> None:
supplied = fragment.get("feature", {}).get("selector_tokens", [])
if len(supplied) != 1 or not isinstance(selectors.get(supplied[0]), dict) or selectors[supplied[0]].get("kind") != "face":
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: host face is absent or stale")
def _materialized_hole_host_frame(
self,
selected: dict[str, Any],
positions: Any,
) -> tuple[dict[str, Any], list[dict[str, list[float]]]]:
"""Lower a verified planar host selector into replay-stable CDSL data.
Hole position inputs are world coordinates at the author boundary.
The engine's frame form uses local coordinates, so both values must
be converted together. Keeping only the selector makes a later
pattern replay depend on a face that the source cut has already
subdivided, which is neither stable nor geometrically meaningful.
"""
geometry = selected.get("geometry") if isinstance(selected, dict) else None
center = geometry.get("center_mm") if isinstance(geometry, dict) else None
normal = geometry.get("normal") if isinstance(geometry, dict) else None
if not self._valid_vector3(center) or not self._valid_vector3(normal, require_nonzero=True):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: selected host face has no usable plane frame")
if not isinstance(positions, list) or not positions:
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: hole positions are unavailable for host-frame materialization")
origin = [float(value) for value in center]
unit_normal = [float(value) / self._norm(normal) for value in normal]
seed = [1.0, 0.0, 0.0] if abs(unit_normal[0]) < 0.9 else [0.0, 1.0, 0.0]
x_raw = [seed[index] - self._dot(seed, unit_normal) * unit_normal[index] for index in range(3)]
x_length = self._norm(x_raw)
if x_length <= 1e-9:
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: selected host face cannot define a stable x direction")
x_dir = [value / x_length for value in x_raw]
y_dir = self._cross(unit_normal, x_dir)
local_positions: list[dict[str, list[float]]] = []
for position in positions:
point = position.get("mm") if isinstance(position, dict) else None
if not self._valid_vector3(point):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: hole position is not a finite 3D point")
offset = [float(point[index]) - origin[index] for index in range(3)]
local_positions.append({"mm": [
self._dot(offset, x_dir),
self._dot(offset, y_dir),
self._dot(offset, unit_normal),
]})
return (
{"frame": {"origin_mm": origin, "x_dir": x_dir, "y_dir": y_dir, "normal": unit_normal}},
local_positions,
)
def _preflight_hole_positions_on_host_plane(self, fragment: dict[str, Any], selectors: dict[str, dict[str, Any]], _base: dict[str, Any] | None, _require_through: bool) -> None:
positions = fragment.get("feature", {}).get("params", {}).get("positions")
if not isinstance(positions, list) or not positions or not all(isinstance(item, dict) and isinstance(item.get("mm"), list) and len(item["mm"]) == 3 for item in positions):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: hole positions are invalid for the host plane")
supplied = fragment.get("feature", {}).get("selector_tokens", [])
host = selectors.get(supplied[0]) if len(supplied) == 1 else None
geometry = host.get("geometry") if isinstance(host, dict) else None
normal = geometry.get("normal") if isinstance(geometry, dict) else None
center = geometry.get("center_mm") if isinstance(geometry, dict) else None
bbox = geometry.get("bbox_mm") if isinstance(geometry, dict) else None
if not isinstance(geometry, dict) or geometry.get("surface_type") != "plane" or not self._valid_vector3(normal, require_nonzero=True) or not self._valid_vector3(center):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: hole host must be an observable planar face")
unit_normal = [float(component) / self._norm(normal) for component in normal]
tolerance = 1e-5
if not self._valid_bbox(bbox):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: host face has no usable topology bounds")
for position in positions:
point = position["mm"]
if not self._valid_vector3(point):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: hole position is not a finite 3D point")
offset = [float(point[index]) - float(center[index]) for index in range(3)]
if abs(self._dot(offset, unit_normal)) > tolerance:
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: hole position is not on the selected host plane")
if any(float(point[index]) < float(bbox[index]) - tolerance or float(point[index]) > float(bbox[index + 3]) + tolerance for index in range(3)):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: hole position is outside the selected host-face bounds")
boundary_loops = geometry.get("boundary_loops_mm")
if isinstance(boundary_loops, list) and boundary_loops and not self._point_in_planar_face(point, boundary_loops, unit_normal, tolerance):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: hole position is outside the selected host face's effective boundary")
def _preflight_analytic_contours(self, profile: dict[str, Any]) -> None:
contours = profile.get("contours")
if not isinstance(contours, list) or not contours:
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: analytic profile requires at least one contour")
tolerance = 1e-7
for contour_index, contour in enumerate(contours):
segments = contour.get("segments") if isinstance(contour, dict) else None
if not isinstance(segments, list) or not segments:
raise RuntimeAdapterError(f"RUNTIME_PRECONDITION_FAILED: analytic contour {contour_index} is empty")
if any(segment.get("type") == "circle" for segment in segments if isinstance(segment, dict)):
if len(segments) != 1 or segments[0].get("type") != "circle":
raise RuntimeAdapterError(f"RUNTIME_PRECONDITION_FAILED: analytic contour {contour_index} cannot mix a full circle with other segments")
continue
endpoints: list[tuple[tuple[float, float], tuple[float, float]]] = []
for segment_index, segment in enumerate(segments):
if not isinstance(segment, dict) or segment.get("type") not in {"line", "arc"}:
raise RuntimeAdapterError(f"RUNTIME_PRECONDITION_FAILED: analytic contour {contour_index} has an unsupported segment")
start = self._point2(segment.get("start"))
end = self._point2(segment.get("end"))
if start is None or end is None:
raise RuntimeAdapterError(f"RUNTIME_PRECONDITION_FAILED: analytic contour {contour_index} segment {segment_index} has non-finite endpoints")
if self._distance2(start, end) <= tolerance:
raise RuntimeAdapterError(f"RUNTIME_PRECONDITION_FAILED: analytic contour {contour_index} segment {segment_index} is degenerate")
if segment.get("type") == "arc":
center = self._point2(segment.get("center"))
radius = segment.get("radius_mm")
if center is None or not isinstance(radius, (int, float)) or isinstance(radius, bool) or not math.isfinite(float(radius)) or float(radius) <= 0:
raise RuntimeAdapterError(f"RUNTIME_PRECONDITION_FAILED: analytic contour {contour_index} arc {segment_index} has an invalid circle")
arc_tolerance = max(tolerance, float(radius) * 1e-7)
if abs(self._distance2(start, center) - float(radius)) > arc_tolerance or abs(self._distance2(end, center) - float(radius)) > arc_tolerance:
raise RuntimeAdapterError(f"RUNTIME_PRECONDITION_FAILED: analytic contour {contour_index} arc {segment_index} endpoints are not on the declared circle")
endpoints.append((start, end))
for segment_index in range(1, len(endpoints)):
if self._distance2(endpoints[segment_index - 1][1], endpoints[segment_index][0]) > tolerance:
raise RuntimeAdapterError(f"RUNTIME_PRECONDITION_FAILED: analytic contour {contour_index} is discontinuous before segment {segment_index}")
if contour.get("closed") is True and self._distance2(endpoints[-1][1], endpoints[0][0]) > tolerance:
raise RuntimeAdapterError(f"RUNTIME_PRECONDITION_FAILED: analytic contour {contour_index} is not closed")
line_segments = [segment for segment, raw in zip(endpoints, segments) if raw.get("type") == "line"]
for first, (a, b) in enumerate(line_segments):
for second, (c, d) in enumerate(line_segments):
if second <= first + 1 or (first == 0 and second == len(line_segments) - 1 and contour.get("closed") is True):
continue
if self._segments_intersect(a, b, c, d):
raise RuntimeAdapterError(f"RUNTIME_PRECONDITION_FAILED: analytic contour {contour_index} has an obvious self-intersection")
@classmethod
def _validate_materialized_runtime_types(cls, feature: dict[str, Any]) -> None:
atomic_id = str(feature.get("atomic_id") or "")
if not atomic_id.startswith("hole_"):
return
params = feature.get("params") if isinstance(feature.get("params"), dict) else {}
host = params.get("host_face") if isinstance(params.get("host_face"), dict) else None
frame = host.get("frame") if isinstance(host, dict) and isinstance(host.get("frame"), dict) else None
if not isinstance(frame, dict):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: materialized hole host_face.frame is unavailable")
for field in ("origin_mm", "x_dir", "y_dir", "normal"):
if not cls._valid_vector3(frame.get(field), require_nonzero=field != "origin_mm"):
raise RuntimeAdapterError(f"RUNTIME_PRECONDITION_FAILED: materialized hole host_face.frame.{field} is invalid")
x_dir, y_dir, normal = frame["x_dir"], frame["y_dir"], frame["normal"]
if abs(cls._dot(x_dir, y_dir)) > 1e-6 or abs(cls._dot(x_dir, normal)) > 1e-6 or abs(cls._dot(y_dir, normal)) > 1e-6:
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: materialized hole host frame is not orthogonal")
positions = params.get("positions")
if not isinstance(positions, list) or not positions:
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: materialized hole positions are unavailable")
for index, position in enumerate(positions):
point = position.get("mm") if isinstance(position, dict) else None
if not cls._valid_vector3(point) or abs(float(point[2])) > 1e-5:
raise RuntimeAdapterError(f"RUNTIME_PRECONDITION_FAILED: materialized hole position {index} is invalid in the host frame")
@staticmethod
def _validate_finite_tree(value: Any, path: str = "$") -> None:
if value is None:
raise RuntimeAdapterError(f"RUNTIME_PRECONDITION_FAILED: {path} must not be null")
if isinstance(value, float) and not math.isfinite(value):
raise RuntimeAdapterError(f"RUNTIME_PRECONDITION_FAILED: {path} must be finite")
if isinstance(value, dict):
for key, child in value.items():
ProfileCadRuntime._validate_finite_tree(child, f"{path}.{key}")
elif isinstance(value, list):
for index, child in enumerate(value):
ProfileCadRuntime._validate_finite_tree(child, f"{path}[{index}]")
@staticmethod
def _point2(value: Any) -> tuple[float, float] | None:
if not isinstance(value, list) or len(value) != 2:
return None
if not all(isinstance(item, (int, float)) and not isinstance(item, bool) and math.isfinite(float(item)) for item in value):
return None
return float(value[0]), float(value[1])
@staticmethod
def _distance2(left: tuple[float, float], right: tuple[float, float]) -> float:
return math.hypot(left[0] - right[0], left[1] - right[1])
@classmethod
def _point_in_planar_face(cls, point: list[float], loops: list[Any], normal: list[float], tolerance: float) -> bool:
drop_axis = max(range(3), key=lambda index: abs(float(normal[index])))
def project(value: Any) -> tuple[float, float] | None:
if not cls._valid_vector3(value):
return None
coordinates = [float(value[index]) for index in range(3) if index != drop_axis]
return coordinates[0], coordinates[1]
projected_point = project(point)
if projected_point is None:
return False
polygons: list[list[tuple[float, float]]] = []
for loop in loops:
polygon = [project(vertex) for vertex in loop] if isinstance(loop, list) else []
if len(polygon) >= 3 and all(vertex is not None for vertex in polygon):
polygons.append([vertex for vertex in polygon if vertex is not None])
if not polygons:
return False
def area(polygon: list[tuple[float, float]]) -> float:
return abs(sum(
polygon[index][0] * polygon[(index + 1) % len(polygon)][1]
- polygon[(index + 1) % len(polygon)][0] * polygon[index][1]
for index in range(len(polygon))
)) / 2
def contains(polygon: list[tuple[float, float]]) -> bool:
x, y = projected_point
inside = False
for index, first in enumerate(polygon):
second = polygon[(index + 1) % len(polygon)]
if cls._distance_to_segment_2d(projected_point, first, second) <= tolerance:
return True
if (first[1] > y) != (second[1] > y):
crossing_x = (second[0] - first[0]) * (y - first[1]) / (second[1] - first[1]) + first[0]
if x < crossing_x:
inside = not inside
return inside
outer = max(polygons, key=area)
return contains(outer) and not any(contains(inner) for inner in polygons if inner is not outer)
@staticmethod
def _distance_to_segment_2d(point: tuple[float, float], start: tuple[float, float], end: tuple[float, float]) -> float:
dx, dy = end[0] - start[0], end[1] - start[1]
length_squared = dx * dx + dy * dy
if length_squared <= 1e-18:
return math.hypot(point[0] - start[0], point[1] - start[1])
fraction = max(0.0, min(1.0, ((point[0] - start[0]) * dx + (point[1] - start[1]) * dy) / length_squared))
return math.hypot(point[0] - (start[0] + fraction * dx), point[1] - (start[1] + fraction * dy))
def _preflight_cut_exit_distance(self, fragment: dict[str, Any], selectors: dict[str, dict[str, Any]], _base: dict[str, Any] | None, require_through: bool) -> None:
if not require_through:
return
params = fragment.get("feature", {}).get("params", {})
depth = params.get("depth_mm", params.get("distance_mm"))
supplied = fragment.get("feature", {}).get("selector_tokens", [])
if not isinstance(depth, (int, float)):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: cut depth is unavailable")
if supplied:
host = selectors[supplied[0]]["geometry"]
bbox = host.get("bbox_mm") if isinstance(host, dict) else None
normal = host.get("normal") or host.get("plane_normal") if isinstance(host, dict) else None
thickness = self._span_from_bbox(bbox, normal)
else:
bbox = self._active_body_bbox(selectors)
normal = (fragment.get("sketch") or {}).get("workplane", {}).get("normal")
thickness = self._span_from_bbox(bbox, normal)
if thickness is None:
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: cannot prove through-cut exit distance from the active topology")
if float(depth) <= thickness + 1e-6:
raise RuntimeAdapterError(
"RUNTIME_PRECONDITION_FAILED: "
f"cut depth {float(depth):g} mm must exceed measured host-body thickness {thickness:g} mm"
)
def _preflight_requires_active_solid(self, _fragment: dict[str, Any], _selectors: dict[str, dict[str, Any]], base: dict[str, Any] | None, _require_through: bool) -> None:
# A committed active CDSL document exists only after a candidate with
# a solid has been accepted. Reference and subtraction operations
# therefore cannot be the first feature of a part.
if not isinstance(base, dict) or not base.get("features"):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: operation requires an active solid checkpoint")
def _preflight_revolve_axis_on_sketch(self, fragment: dict[str, Any], _selectors: dict[str, dict[str, Any]], _base: dict[str, Any] | None, _require_through: bool) -> None:
axis = fragment.get("feature", {}).get("params", {}).get("axis")
direction = axis.get("direction") if isinstance(axis, dict) else None
origin = axis.get("origin_mm") if isinstance(axis, dict) else None
workplane = fragment.get("sketch", {}).get("workplane") if isinstance(fragment.get("sketch"), dict) else None
plane_origin = workplane.get("origin_mm") if isinstance(workplane, dict) else None
plane_normal = workplane.get("normal") if isinstance(workplane, dict) else None
if not self._valid_vector3(direction, require_nonzero=True) or not self._valid_vector3(origin) or not self._valid_vector3(plane_origin) or not self._valid_vector3(plane_normal, require_nonzero=True):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: revolve axis or sketch plane is invalid")
unit_direction = [float(component) / self._norm(direction) for component in direction]
unit_normal = [float(component) / self._norm(plane_normal) for component in plane_normal]
if abs(self._dot(unit_direction, unit_normal)) > 1e-6:
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: revolve axis is not parallel to the sketch plane")
axis_offset = [float(origin[index]) - float(plane_origin[index]) for index in range(3)]
if abs(self._dot(axis_offset, unit_normal)) > 1e-5:
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: revolve axis is not on the sketch plane")
def _preflight_reference_plane_nonzero_normal(self, fragment: dict[str, Any], _selectors: dict[str, dict[str, Any]], _base: dict[str, Any] | None, _require_through: bool) -> None:
plane = fragment.get("feature", {}).get("params", {}).get("plane")
normal = plane.get("normal") if isinstance(plane, dict) else None
if not isinstance(normal, list) or self._norm(normal) <= 1e-9:
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: reference plane normal is degenerate")
def _preflight_reference_axis_nonzero_direction(self, fragment: dict[str, Any], _selectors: dict[str, dict[str, Any]], _base: dict[str, Any] | None, _require_through: bool) -> None:
axis = fragment.get("feature", {}).get("params", {}).get("axis")
direction = axis.get("direction") if isinstance(axis, dict) else None
if not isinstance(direction, list) or self._norm(direction) <= 1e-9:
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: reference axis direction is degenerate")
def _preflight_selected_edges_exist(self, fragment: dict[str, Any], selectors: dict[str, dict[str, Any]], _base: dict[str, Any] | None, _require_through: bool) -> None:
supplied = fragment.get("feature", {}).get("selector_tokens", [])
if not supplied or not all(isinstance(selectors.get(token), dict) and selectors[token].get("kind") == "edge" for token in supplied):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: selected edge is absent or stale")
def _preflight_source_features_exist(self, fragment: dict[str, Any], _selectors: dict[str, dict[str, Any]], base: dict[str, Any] | None, _require_through: bool) -> None:
params = fragment.get("feature", {}).get("params", {})
known = {str(feature.get("id") or "") for feature in ((base or {}).get("features") or ()) if isinstance(feature, dict)}
if not set(params.get("source_feature_ids") or ()).issubset(known):
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: source feature is not part of current head")
def _preflight_mirror_plane_exists(self, fragment: dict[str, Any], selectors: dict[str, dict[str, Any]], _base: dict[str, Any] | None, _require_through: bool) -> None:
supplied = fragment.get("feature", {}).get("selector_tokens", [])
if len(supplied) != 1 or not isinstance(selectors.get(supplied[0]), dict) or selectors[supplied[0]].get("kind") != "plane":
raise RuntimeAdapterError("RUNTIME_PRECONDITION_FAILED: mirror plane is absent or stale")
@staticmethod
def _polygon_self_intersects(vertices: list[Any]) -> bool:
points = [(float(point[0]), float(point[1])) for point in vertices if isinstance(point, list) and len(point) == 2]
if len(points) != len(vertices):
return True
segments = list(zip(points, [*points[1:], points[0]]))
for first, (a, b) in enumerate(segments):
for second, (c, d) in enumerate(segments):
if second <= first + 1 or (first == 0 and second == len(segments) - 1):
continue
if ProfileCadRuntime._segments_intersect(a, b, c, d):
return True
return False
@staticmethod
def _segments_intersect(a: tuple[float, float], b: tuple[float, float], c: tuple[float, float], d: tuple[float, float]) -> bool:
def orientation(p: tuple[float, float], q: tuple[float, float], r: tuple[float, float]) -> float:
return (q[0] - p[0]) * (r[1] - p[1]) - (q[1] - p[1]) * (r[0] - p[0])
left = orientation(a, b, c)
right = orientation(a, b, d)
low = orientation(c, d, a)
high = orientation(c, d, b)
return (left > 0) != (right > 0) and (low > 0) != (high > 0)
@staticmethod
def _active_body_bbox(selector_tokens: dict[str, dict[str, Any]]) -> list[float] | None:
for token in selector_tokens.values():
if token.get("kind") != "body":
continue
geometry = token.get("geometry")
bbox = geometry.get("bbox_mm") if isinstance(geometry, dict) else None
if isinstance(bbox, list) and len(bbox) == 6 and all(isinstance(value, (int, float)) for value in bbox):
return [float(value) for value in bbox]
return None
@staticmethod
def _span_from_bbox(bbox: Any, normal: Any) -> float | None:
if not isinstance(bbox, list) or len(bbox) != 6 or not isinstance(normal, list) or len(normal) != 3:
return None
if not all(isinstance(value, (int, float)) for value in [*bbox, *normal]):
return None
length = math.sqrt(sum(float(value) ** 2 for value in normal))
if length <= 1e-9:
return None
return sum(
abs(float(normal[index]) / length) * abs(float(bbox[index + 3]) - float(bbox[index]))
for index in range(3)
)
@staticmethod
def _norm(vector: list[float]) -> float:
return math.sqrt(sum(float(item) ** 2 for item in vector))
@staticmethod
def _dot(left: list[float], right: list[float]) -> float:
return sum(float(left[index]) * float(right[index]) for index in range(3))
@staticmethod
def _valid_vector3(value: Any, *, require_nonzero: bool = False) -> bool:
valid = (
isinstance(value, list)
and len(value) == 3
and all(isinstance(item, (int, float)) and not isinstance(item, bool) and math.isfinite(float(item)) for item in value)
)
return valid and (not require_nonzero or ProfileCadRuntime._norm(value) > 1e-9)
@staticmethod
def _valid_bbox(value: Any) -> bool:
return (
isinstance(value, list)
and len(value) == 6
and all(isinstance(item, (int, float)) and not isinstance(item, bool) and math.isfinite(float(item)) for item in value)
and all(float(value[index]) <= float(value[index + 3]) for index in range(3))
)
@staticmethod
def _cross(a: list[float], b: list[float]) -> list[float]:
return [float(a[1]) * float(b[2]) - float(a[2]) * float(b[1]), float(a[2]) * float(b[0]) - float(a[0]) * float(b[2]), float(a[0]) * float(b[1]) - float(a[1]) * float(b[0])]
@staticmethod
def _write_json(path: Path, payload: dict[str, Any]) -> None:
path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
@staticmethod
def _health(engine_result: dict[str, Any], step_path: Path, glb_path: Path) -> dict[str, Any]:
bbox = engine_result.get("bbox_mm") if isinstance(engine_result.get("bbox_mm"), dict) else {}
minimum, maximum = bbox.get("min"), bbox.get("max")
if not isinstance(minimum, list) or not isinstance(maximum, list) or len(minimum) != 3 or len(maximum) != 3 or not step_path.is_file() or not glb_path.is_file():
raise RuntimeAdapterError("rebuild did not produce complete deterministic artifacts")
return {"bbox_mm": {"min": [float(item) for item in minimum], "max": [float(item) for item in maximum], "dimensions": [float(maximum[index]) - float(minimum[index]) for index in range(3)]}, "volume_mm3": float(engine_result["volume_mm3"]), "solid_count": int(engine_result["solid_count"]), "feature_count": len(engine_result.get("feature_results") or [])}
@@ -0,0 +1,414 @@
"""SQLite repository: the sole mutable state authority for protocol v3."""
from __future__ import annotations
from contextlib import contextmanager
import json
from pathlib import Path
import sqlite3
from threading import RLock
from typing import Any, Iterator
from app.cad_agent.domain.errors import ErrorCode
from app.cad_agent.domain.state import PendingAction, TaskPhase, TaskState
from app.cad_agent.ports import InvocationRecord
class SqliteTaskRepository:
def __init__(self, database_path: Path) -> None:
self.database_path = database_path
self.database_path.parent.mkdir(parents=True, exist_ok=True)
self._lock = RLock()
self._initialize()
@contextmanager
def _connection(self) -> Iterator[sqlite3.Connection]:
connection = sqlite3.connect(self.database_path, timeout=20, isolation_level=None)
try:
connection.row_factory = sqlite3.Row
connection.execute("PRAGMA foreign_keys = ON")
connection.execute("PRAGMA journal_mode = WAL")
yield connection
finally:
connection.close()
def _initialize(self) -> None:
with self._lock, self._connection() as connection:
connection.executescript(
"""
CREATE TABLE IF NOT EXISTS tasks (
task_id TEXT PRIMARY KEY,
protocol_version TEXT NOT NULL CHECK(protocol_version = '3.0'),
request TEXT NOT NULL,
phase TEXT NOT NULL,
state_version INTEGER NOT NULL,
active_revision TEXT NOT NULL DEFAULT '',
pending_action_json TEXT,
candidate_id TEXT NOT NULL DEFAULT '',
candidate_stage_id TEXT NOT NULL DEFAULT '',
repair_required INTEGER NOT NULL DEFAULT 0,
last_error TEXT,
retry_from_phase TEXT NOT NULL DEFAULT '',
requirements_draft_path TEXT NOT NULL DEFAULT '',
requirements_review_path TEXT NOT NULL DEFAULT '',
requirements_contract_path TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS invocations (
invocation_id TEXT PRIMARY KEY,
task_id TEXT NOT NULL REFERENCES tasks(task_id),
idempotency_key TEXT NOT NULL,
status TEXT NOT NULL,
result_json TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE(task_id, idempotency_key)
);
CREATE TABLE IF NOT EXISTS ledger (
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
task_id TEXT NOT NULL REFERENCES tasks(task_id),
event_json TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS outbox (
event_id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id TEXT NOT NULL REFERENCES tasks(task_id),
event_json TEXT NOT NULL,
published_at TEXT
);
CREATE TABLE IF NOT EXISTS usage_records (
usage_id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id TEXT NOT NULL REFERENCES tasks(task_id),
usage_json TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS tool_audits (
audit_id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id TEXT NOT NULL REFERENCES tasks(task_id),
audit_json TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS model_capabilities (
provider_id TEXT NOT NULL,
model_id TEXT NOT NULL,
schema_hash TEXT NOT NULL,
supported INTEGER NOT NULL,
report_json TEXT NOT NULL,
checked_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY(provider_id, model_id, schema_hash)
);
"""
)
columns = {str(row["name"]) for row in connection.execute("PRAGMA table_info(tasks)").fetchall()}
if "requirements_draft_path" not in columns:
connection.execute("ALTER TABLE tasks ADD COLUMN requirements_draft_path TEXT NOT NULL DEFAULT ''")
if "requirements_review_path" not in columns:
connection.execute("ALTER TABLE tasks ADD COLUMN requirements_review_path TEXT NOT NULL DEFAULT ''")
if "requirements_contract_path" not in columns:
connection.execute("ALTER TABLE tasks ADD COLUMN requirements_contract_path TEXT NOT NULL DEFAULT ''")
if "retry_from_phase" not in columns:
connection.execute("ALTER TABLE tasks ADD COLUMN retry_from_phase TEXT NOT NULL DEFAULT ''")
def create_task(self, task_id: str, request: str) -> TaskState:
with self._lock, self._connection() as connection:
connection.execute(
"INSERT OR IGNORE INTO tasks(task_id, protocol_version, request, phase, state_version) VALUES (?, '3.0', ?, ?, 0)",
(task_id, request, TaskPhase.DRAFTING_REQUIREMENTS.value),
)
state = self.get_state(task_id)
if state is None:
raise RuntimeError("SQLite task insert was not visible")
return state
def get_state(self, task_id: str) -> TaskState | None:
with self._lock, self._connection() as connection:
row = connection.execute("SELECT * FROM tasks WHERE task_id = ?", (task_id,)).fetchone()
return self._state(row) if row is not None else None
def get_task_projection(self, task_id: str) -> dict[str, Any] | None:
state = self.get_state(task_id)
if state is None:
return None
events = self.ledger_events(task_id)
revisions = [
{
"revision_id": item["revision_id"], "status": "success", "visibility": "final" if state.phase == TaskPhase.COMPLETED and item["revision_id"] == state.active_revision else "checkpoint",
"cdsl_path": f"revisions/{item['revision_id']}/model.cdsl.json", "step_path": f"revisions/{item['revision_id']}/model.step", "glb_path": f"revisions/{item['revision_id']}/model.glb", "report_path": f"revisions/{item['revision_id']}/rebuild-report.json", "candidate_review_path": item.get("review_path", ""),
}
for item in events
if item.get("event") == "accepted" and isinstance(item.get("revision_id"), str)
]
frozen = next((item for item in reversed(events) if item.get("event") == "requirements_contract_frozen"), {})
verification_warnings = [
str(item) for item in frozen.get("verification_warnings") or () if str(item)
] if isinstance(frozen, dict) else []
applied_normalizations = [
item for item in frozen.get("applied_normalizations") or () if isinstance(item, dict)
] if isinstance(frozen, dict) else []
status_event = next((
item for item in reversed(events)
if item.get("event") in {
"requirements_waiting_for_user", "waiting_retry", "call_budget_exhausted",
"requirements_review_limit_reached", "no_progress_limit",
"candidate_runtime_execution_failure", "candidate_recovery_runtime_execution_failure",
"failed_author_format", "runtime_contract_invalid",
}
), {}) if state.phase in {TaskPhase.FAILED, TaskPhase.WAITING_RETRY, TaskPhase.WAITING_FOR_USER} else {}
questions = [str(item) for item in status_event.get("questions") or () if str(item)] if isinstance(status_event, dict) else []
findings = [item for item in status_event.get("findings") or () if isinstance(item, dict)] if isinstance(status_event, dict) else []
issues = [str(item.get("description") or "") for item in findings if item.get("description")]
return {
"schema_version": "3.0",
"task_id": state.task_id,
"phase": state.phase.value,
"lifecycle": self._lifecycle(state.phase),
"state_version": state.version,
"active_revision": state.active_revision,
"current_revision": state.active_revision,
"published_revision": state.active_revision if state.phase == TaskPhase.COMPLETED else "",
"pending_action": self._pending_payload(state.pending_action),
"active_candidate_id": state.candidate_id,
"repair_required": state.repair_required,
"last_error": state.last_error.value if state.last_error else "",
"retry_from_phase": state.retry_from_phase.value if state.retry_from_phase else "",
"requirements_draft_path": state.requirements_draft_path,
"requirements_review_path": state.requirements_review_path,
"requirements_contract_path": state.requirements_contract_path,
"verification_status": "completed_with_risks" if verification_warnings else "verified",
"verification_warnings": verification_warnings,
"applied_normalizations": applied_normalizations,
"message": str(status_event.get("message") or "") if isinstance(status_event, dict) else "",
"questions": questions,
"issues": issues,
"blocker_type": (
"requirements_ambiguity" if state.phase == TaskPhase.WAITING_FOR_USER
else str(status_event.get("event") or "") if isinstance(status_event, dict) else ""
),
"user_action_required": state.phase == TaskPhase.WAITING_FOR_USER and bool(questions),
"action_ledger_summary": events[-12:],
"revisions": revisions,
}
def ledger_events(self, task_id: str) -> list[dict[str, Any]]:
with self._lock, self._connection() as connection:
rows = connection.execute("SELECT sequence, event_json, created_at FROM ledger WHERE task_id = ? ORDER BY sequence", (task_id,)).fetchall()
return [{"sequence": row["sequence"], "at": row["created_at"], **json.loads(row["event_json"])} for row in rows]
def invocation_records(self, task_id: str) -> list[dict[str, Any]]:
with self._lock, self._connection() as connection:
rows = connection.execute(
"SELECT invocation_id, idempotency_key, status, result_json, created_at, updated_at FROM invocations WHERE task_id = ? ORDER BY created_at, invocation_id",
(task_id,),
).fetchall()
return [
{
"invocation_id": str(row["invocation_id"]), "idempotency_key": str(row["idempotency_key"]),
"status": str(row["status"]), "result": json.loads(row["result_json"]) if row["result_json"] else None,
"created_at": str(row["created_at"]), "updated_at": str(row["updated_at"]),
}
for row in rows
]
def compare_and_swap(
self,
state: TaskState,
*,
events: list[dict[str, Any]] = (),
invocation_id: str | None = None,
invocation_result: dict[str, Any] | None = None,
) -> bool:
if (invocation_id is None) != (invocation_result is None):
raise ValueError("Invocation completion requires both invocation_id and invocation_result")
previous_version = state.version - 1
if previous_version < 0:
raise ValueError("State version must advance exactly once")
with self._lock, self._connection() as connection:
connection.execute("BEGIN IMMEDIATE")
try:
cursor = connection.execute(
"""UPDATE tasks SET phase = ?, state_version = ?, active_revision = ?, pending_action_json = ?,
candidate_id = ?, candidate_stage_id = ?, repair_required = ?, last_error = ?, retry_from_phase = ?, requirements_draft_path = ?,
requirements_review_path = ?, requirements_contract_path = ?, updated_at = CURRENT_TIMESTAMP
WHERE task_id = ? AND state_version = ?""",
(
state.phase.value, state.version, state.active_revision,
json.dumps(self._pending_payload(state.pending_action), ensure_ascii=True) if state.pending_action else None,
state.candidate_id, state.candidate_stage_id,
int(state.repair_required), state.last_error.value if state.last_error else None,
state.retry_from_phase.value if state.retry_from_phase else "", state.requirements_draft_path,
state.requirements_review_path, state.requirements_contract_path,
state.task_id, previous_version,
),
)
if cursor.rowcount != 1:
connection.execute("ROLLBACK")
return False
for event in events:
encoded = json.dumps(event, ensure_ascii=True, sort_keys=True)
connection.execute("INSERT INTO ledger(task_id, event_json) VALUES (?, ?)", (state.task_id, encoded))
connection.execute("INSERT INTO outbox(task_id, event_json) VALUES (?, ?)", (state.task_id, encoded))
if invocation_id is not None and invocation_result is not None:
finished = connection.execute(
"""UPDATE invocations
SET status = 'finished', result_json = ?, updated_at = CURRENT_TIMESTAMP
WHERE invocation_id = ? AND task_id = ? AND status = 'processing'""",
(json.dumps(invocation_result, ensure_ascii=True), invocation_id, state.task_id),
)
if finished.rowcount != 1:
raise ValueError("Invocation is not an active record for this state transition")
connection.execute("COMMIT")
return True
except Exception:
connection.execute("ROLLBACK")
raise
def begin_invocation(self, task_id: str, invocation_id: str, idempotency_key: str) -> InvocationRecord:
with self._lock, self._connection() as connection:
connection.execute("BEGIN IMMEDIATE")
try:
existing = connection.execute("SELECT * FROM invocations WHERE task_id = ? AND idempotency_key = ?", (task_id, idempotency_key)).fetchone()
if existing is not None:
connection.execute("COMMIT")
return self._invocation(existing)
connection.execute("INSERT INTO invocations(invocation_id, task_id, idempotency_key, status) VALUES (?, ?, ?, 'processing')", (invocation_id, task_id, idempotency_key))
connection.execute("COMMIT")
return InvocationRecord(invocation_id, idempotency_key, "processing")
except Exception:
connection.execute("ROLLBACK")
raise
def get_invocation(self, task_id: str, invocation_id: str) -> InvocationRecord | None:
with self._lock, self._connection() as connection:
row = connection.execute(
"SELECT * FROM invocations WHERE task_id = ? AND invocation_id = ?",
(task_id, invocation_id),
).fetchone()
return self._invocation(row) if row is not None else None
def finish_invocation(self, invocation_id: str, result: dict[str, Any]) -> None:
with self._lock, self._connection() as connection:
row = connection.execute("SELECT status, result_json FROM invocations WHERE invocation_id = ?", (invocation_id,)).fetchone()
if row is None:
raise ValueError("Unknown invocation")
if str(row["status"]) == "finished":
# A winning concurrent command has already recorded the only
# durable result for this idempotency key. Never overwrite it.
return
cursor = connection.execute(
"UPDATE invocations SET status = 'finished', result_json = ?, updated_at = CURRENT_TIMESTAMP WHERE invocation_id = ? AND status = 'processing'",
(json.dumps(result, ensure_ascii=True), invocation_id),
)
if cursor.rowcount != 1:
raise ValueError("Invocation could not be completed")
def append_outbox(self, task_id: str, event: dict[str, Any]) -> None:
with self._lock, self._connection() as connection:
connection.execute("INSERT INTO outbox(task_id, event_json) VALUES (?, ?)", (task_id, json.dumps(event, ensure_ascii=True, sort_keys=True)))
def pending_outbox(self, limit: int = 100, *, task_id: str | None = None) -> list[dict[str, Any]]:
with self._lock, self._connection() as connection:
if task_id is None:
rows = connection.execute("SELECT event_id, task_id, event_json FROM outbox WHERE published_at IS NULL ORDER BY event_id LIMIT ?", (limit,)).fetchall()
else:
rows = connection.execute("SELECT event_id, task_id, event_json FROM outbox WHERE published_at IS NULL AND task_id = ? ORDER BY event_id LIMIT ?", (task_id, limit)).fetchall()
return [{"event_id": row["event_id"], "task_id": row["task_id"], **json.loads(row["event_json"])} for row in rows]
def mark_outbox_published(self, event_id: int) -> None:
with self._lock, self._connection() as connection:
connection.execute("UPDATE outbox SET published_at = CURRENT_TIMESTAMP WHERE event_id = ? AND published_at IS NULL", (event_id,))
def record_usage(self, task_id: str, payload: dict[str, Any]) -> None:
with self._lock, self._connection() as connection:
connection.execute("INSERT INTO usage_records(task_id, usage_json) VALUES (?, ?)", (task_id, json.dumps(payload, ensure_ascii=True, sort_keys=True)))
def record_tool_audit(self, task_id: str, payload: dict[str, Any]) -> None:
"""Persist a diagnostic structured-output audit separately from usage."""
with self._lock, self._connection() as connection:
connection.execute(
"INSERT INTO tool_audits(task_id, audit_json) VALUES (?, ?)",
(task_id, json.dumps(payload, ensure_ascii=True, sort_keys=True)),
)
def tool_audits(self, task_id: str) -> list[dict[str, Any]]:
with self._lock, self._connection() as connection:
rows = connection.execute(
"SELECT audit_id, audit_json, created_at FROM tool_audits WHERE task_id = ? ORDER BY audit_id",
(task_id,),
).fetchall()
return [
{"audit_id": int(row["audit_id"]), "at": str(row["created_at"]), **json.loads(row["audit_json"])}
for row in rows
]
def usage_summary(self, task_id: str) -> dict[str, Any]:
with self._lock, self._connection() as connection:
rows = connection.execute("SELECT usage_json FROM usage_records WHERE task_id = ? ORDER BY usage_id", (task_id,)).fetchall()
values = [json.loads(row["usage_json"]) for row in rows]
return {
"calls": len(values),
"prompt_tokens": sum(int(item.get("prompt_tokens") or 0) for item in values if isinstance(item, dict)),
"completion_tokens": sum(int(item.get("completion_tokens") or 0) for item in values if isinstance(item, dict)),
"context_chars": sum(int(item.get("context_chars") or 0) for item in values if isinstance(item, dict)),
"records": values,
}
def running_task_ids(self) -> list[str]:
phases = tuple(phase.value for phase in TaskPhase if phase not in {TaskPhase.COMPLETED, TaskPhase.FAILED, TaskPhase.CANCELLED, TaskPhase.WAITING_FOR_USER, TaskPhase.WAITING_RETRY})
placeholders = ", ".join("?" for _ in phases)
with self._lock, self._connection() as connection:
rows = connection.execute(f"SELECT task_id FROM tasks WHERE phase IN ({placeholders}) ORDER BY created_at", phases).fetchall()
return [str(row["task_id"]) for row in rows]
def model_capability(self, provider_id: str, model_id: str, schema_hash: str) -> dict[str, Any] | None:
with self._lock, self._connection() as connection:
row = connection.execute("SELECT supported, report_json, checked_at FROM model_capabilities WHERE provider_id = ? AND model_id = ? AND schema_hash = ?", (provider_id, model_id, schema_hash)).fetchone()
return {"supported": bool(row["supported"]), "report": json.loads(row["report_json"]), "checked_at": row["checked_at"]} if row else None
def record_model_capability(self, provider_id: str, model_id: str, schema_hash: str, report: dict[str, Any]) -> None:
with self._lock, self._connection() as connection:
connection.execute("""INSERT INTO model_capabilities(provider_id, model_id, schema_hash, supported, report_json)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(provider_id, model_id, schema_hash) DO UPDATE SET supported = excluded.supported, report_json = excluded.report_json, checked_at = CURRENT_TIMESTAMP""", (provider_id, model_id, schema_hash, int(bool(report.get("supported"))), json.dumps(report, ensure_ascii=True, sort_keys=True)))
@staticmethod
def _state(row: sqlite3.Row) -> TaskState:
raw_pending = json.loads(row["pending_action_json"]) if row["pending_action_json"] else None
pending = PendingAction(
action_id=raw_pending["action_id"], working_head=raw_pending["working_head"], intent=raw_pending["intent"],
requirement_ids=tuple(raw_pending["requirement_ids"]), atomic_id=raw_pending["atomic_id"],
expected_change=raw_pending["expected_change"], contract_hash=raw_pending["contract_hash"], idempotency_key=raw_pending["idempotency_key"],
) if isinstance(raw_pending, dict) else None
return TaskState(
task_id=str(row["task_id"]), phase=TaskPhase(str(row["phase"])), version=int(row["state_version"]),
active_revision=str(row["active_revision"] or ""), pending_action=pending,
candidate_id=str(row["candidate_id"] or ""), candidate_stage_id=str(row["candidate_stage_id"] or ""),
repair_required=bool(row["repair_required"]),
last_error=ErrorCode(str(row["last_error"])) if row["last_error"] else None,
retry_from_phase=TaskPhase(str(row["retry_from_phase"])) if row["retry_from_phase"] else None,
requirements_draft_path=str(row["requirements_draft_path"] or ""),
requirements_review_path=str(row["requirements_review_path"] or ""),
requirements_contract_path=str(row["requirements_contract_path"] or ""),
)
@staticmethod
def _pending_payload(pending: PendingAction | None) -> dict[str, Any] | None:
if pending is None:
return None
return {"action_id": pending.action_id, "working_head": pending.working_head, "intent": pending.intent, "requirement_ids": list(pending.requirement_ids), "atomic_id": pending.atomic_id, "expected_change": pending.expected_change, "contract_hash": pending.contract_hash, "idempotency_key": pending.idempotency_key}
@staticmethod
def _invocation(row: sqlite3.Row) -> InvocationRecord:
return InvocationRecord(str(row["invocation_id"]), str(row["idempotency_key"]), str(row["status"]), json.loads(row["result_json"]) if row["result_json"] else None)
@staticmethod
def _lifecycle(phase: TaskPhase) -> str:
if phase == TaskPhase.COMPLETED:
return "completed"
if phase == TaskPhase.FAILED:
return "failed"
if phase == TaskPhase.CANCELLED:
return "cancelled"
if phase in {TaskPhase.WAITING_RETRY, TaskPhase.WAITING_FOR_USER}:
return phase.value.lower()
return "running"
@@ -0,0 +1,266 @@
"""The sole OpenAI-compatible structured-output adapter for protocol v3.
Raw tool arguments are intentionally preserved. Callers must run their
Pydantic/JSON-Schema canonical validator before causing any state transition.
"""
from __future__ import annotations
import asyncio
from dataclasses import dataclass
import json
from typing import Any
import httpx
from jsonschema import Draft202012Validator
from app.settings import ProviderConfig, ProviderModel, Settings
from app.cad_agent.ports import AdapterUnavailable
class StructuredModelError(AdapterUnavailable):
"""A provider-side structured-output failure, never an author mistake.
Invalid provider responses and provider-side schema rejections are outside
the model's tool arguments. The workflow must park the task for bounded
service recovery instead of spending the author-format retry budget.
"""
class StructuredTransportError(StructuredModelError):
pass
@dataclass(frozen=True, slots=True)
class StructuredResponse:
tool_calls: list[dict[str, Any]]
usage: dict[str, int]
raw_response: dict[str, Any]
class StructuredModelGateway:
"""Bounded HTTP adapter; it never performs tool execution or re-asks."""
def __init__(self, settings: Settings) -> None:
self.settings = settings
async def call_tool(self, *, messages: list[dict[str, Any]], tool: dict[str, Any], provider_id: str, model_id: str, required_tool_name: str) -> dict[str, Any]:
provider, model = self._provider_model(provider_id, model_id)
payload = self._payload(provider, model.id, messages, [tool], required_tool_name)
response = await self._request(provider, payload)
try:
return self._normalized_response(provider, response)
except (KeyError, TypeError, ValueError) as error:
raise StructuredModelError("Provider response cannot be normalized as a structured tool-call result") from error
async def conformance(self, *, provider_id: str, model_id: str, tools: list[dict[str, Any]]) -> dict[str, Any]:
failures: list[dict[str, str]] = []
usage = {"prompt_tokens": 0, "completion_tokens": 0}
for tool in tools:
name = str((tool.get("function") or {}).get("name") or "")
if not name:
failures.append({"tool": "", "message": "Tool has no function name"})
continue
try:
response = await self.call_tool(
messages=self._conformance_messages(tool),
tool=tool, provider_id=provider_id, model_id=model_id, required_tool_name=name,
)
usage["prompt_tokens"] += int(response["usage"]["prompt_tokens"])
usage["completion_tokens"] += int(response["usage"]["completion_tokens"])
calls = response["tool_calls"]
if len(calls) != 1:
failures.append({"tool": name, "message": "Provider did not return exactly one tool call"})
continue
function = calls[0].get("function") if isinstance(calls[0], dict) and isinstance(calls[0].get("function"), dict) else {}
if str(function.get("name") or "") != name:
failures.append({"tool": name, "message": "Provider returned a different tool"})
continue
raw = str(function.get("arguments") or "")
if len(raw.encode("utf-8")) > 48_000:
failures.append({"tool": name, "message": "Provider arguments exceed the protocol byte limit"})
continue
try:
arguments = json.loads(raw)
except json.JSONDecodeError as error:
failures.append({"tool": name, "message": f"Provider arguments are not JSON: {error.msg}"})
continue
if not isinstance(arguments, dict):
failures.append({"tool": name, "message": "Provider arguments are not a JSON object"})
continue
schema = (tool.get("function") or {}).get("parameters")
errors = list(Draft202012Validator(schema).iter_errors(arguments)) if isinstance(schema, dict) else []
if errors:
failures.append({"tool": name, "message": f"Provider arguments violate schema: {errors[0].message[:300]}"})
except StructuredTransportError as error:
# This probe cannot establish unsupported capability while
# the provider is unavailable. Stop instead of charging for
# every remaining schema after the first transport failure.
failures.append({"tool": name, "message": str(error)[:500]})
return {
"supported": False,
"failures": failures,
"usage": usage,
"mode": "openai_compatible_tool_calling",
"probe_unavailable": True,
}
except Exception as error:
failures.append({"tool": name, "message": str(error)[:500]})
return {
"supported": not failures,
"failures": failures,
"usage": usage,
"mode": "openai_compatible_tool_calling",
"probe_unavailable": False,
}
@staticmethod
def _conformance_messages(tool: dict[str, Any]) -> list[dict[str, Any]]:
"""Give capability probes the same selector fact production exposes.
A required opaque selector is not inferable from a generic request for
a minimal example. Production fragment turns expose a topology token
before the fragment tool, so the probe must do the same while still
letting the provider generate every other schema field itself.
"""
function = tool.get("function") if isinstance(tool.get("function"), dict) else {}
name = str(function.get("name") or "")
parameters = function.get("parameters") if isinstance(function.get("parameters"), dict) else {}
feature = parameters.get("properties", {}).get("feature") if isinstance(parameters.get("properties"), dict) else None
feature_properties = feature.get("properties") if isinstance(feature, dict) and isinstance(feature.get("properties"), dict) else {}
feature_required = feature.get("required") if isinstance(feature, dict) and isinstance(feature.get("required"), list) else []
selector_required = "selector_tokens" in feature_required and isinstance(feature_properties.get("selector_tokens"), dict)
instruction = "Return exactly the required function call with a minimal valid example. The active JSON Schema is authoritative."
if selector_required:
atomic_id = feature_properties.get("atomic_id", {}).get("const") if isinstance(feature_properties.get("atomic_id"), dict) else "..."
instruction += (
" The current topology exposes opaque selector token `sel_conformance`. "
f"Use root shape {{\"feature\":{{\"atomic_id\":{json.dumps(atomic_id)},"
"\"selector_tokens\":[\"sel_conformance\"],\"params\":{...}}}}. "
"Because feature.selector_tokens is required, it is author input and must not be moved into params."
)
if name == "review_candidate":
instruction += (
" This is a candidate-review probe: include every required top-level and nested field; "
"in particular provide a verdict and claim_coverage. Use accept and pass only where the schema permits them."
)
elif name == "review_final":
instruction += (
" This is a final-review probe: include every required top-level and nested field; "
"in particular provide a verdict and claim_coverage. Use pass only where the schema permits it."
)
return [{"role": "system", "content": instruction}]
def _provider_model(self, provider_id: str, model_id: str) -> tuple[ProviderConfig, ProviderModel]:
return self.settings.resolve_model(provider_id, model_id)
@staticmethod
def _responses_input(messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
values: list[dict[str, Any]] = []
for message in messages:
role = "developer" if message.get("role") == "system" else str(message.get("role") or "user")
raw_content = message.get("content")
if isinstance(raw_content, str):
content: list[dict[str, Any]] = [{"type": "input_text", "text": raw_content}]
elif isinstance(raw_content, list):
content = []
for item in raw_content:
if not isinstance(item, dict):
continue
if item.get("type") == "text" and isinstance(item.get("text"), str):
content.append({"type": "input_text", "text": item["text"]})
elif item.get("type") == "image_url" and isinstance(item.get("image_url"), dict) and isinstance(item["image_url"].get("url"), str):
content.append({"type": "input_image", "image_url": item["image_url"]["url"]})
if not content:
content = [{"type": "input_text", "text": ""}]
else:
content = [{"type": "input_text", "text": ""}]
values.append({"role": role, "content": content})
return values
@staticmethod
def _responses_tools(tools: list[dict[str, Any]]) -> list[dict[str, Any]]:
values: list[dict[str, Any]] = []
for tool in tools:
function = tool.get("function") if isinstance(tool.get("function"), dict) else {}
values.append({
"type": "function", "name": str(function.get("name") or ""),
"description": str(function.get("description") or ""),
"parameters": function.get("parameters") if isinstance(function.get("parameters"), dict) else {},
})
return values
def _payload(self, provider: ProviderConfig, model_id: str, messages: list[dict[str, Any]], tools: list[dict[str, Any]], required_tool_name: str) -> dict[str, Any]:
if len(tools) != 1 or not required_tool_name:
raise StructuredModelError("The v3 protocol requires exactly one named tool per provider call.")
if provider.api_style == "responses":
payload: dict[str, Any] = {
"model": model_id, "input": self._responses_input(messages), "tools": self._responses_tools(tools),
"tool_choice": {"type": "function", "name": required_tool_name},
**provider.request_options,
}
return payload
return {
"model": model_id, "messages": messages, "tools": tools,
"tool_choice": {"type": "function", "function": {"name": required_tool_name}},
"temperature": 0, **provider.request_options,
}
@staticmethod
def _normalized_response(provider: ProviderConfig, response: dict[str, Any]) -> dict[str, Any]:
usage = response.get("usage") if isinstance(response.get("usage"), dict) else {}
if provider.api_style == "responses":
output = response.get("output") if isinstance(response.get("output"), list) else []
calls = [
{"function": {"name": str(item.get("name") or ""), "arguments": str(item.get("arguments") or "")}}
for item in output
if isinstance(item, dict) and item.get("type") == "function_call"
]
prompt_tokens = int(usage.get("input_tokens") or usage.get("prompt_tokens") or 0)
completion_tokens = int(usage.get("output_tokens") or usage.get("completion_tokens") or 0)
else:
choices = response.get("choices") if isinstance(response.get("choices"), list) else []
message = choices[0].get("message") if choices and isinstance(choices[0], dict) and isinstance(choices[0].get("message"), dict) else {}
calls = message.get("tool_calls") if isinstance(message, dict) and isinstance(message.get("tool_calls"), list) else []
prompt_tokens = int(usage.get("prompt_tokens") or 0)
completion_tokens = int(usage.get("completion_tokens") or 0)
return {
"tool_calls": calls,
"usage": {
"prompt_tokens": prompt_tokens,
"completion_tokens": completion_tokens,
"total_tokens": int(usage.get("total_tokens") or prompt_tokens + completion_tokens),
"usage_available": bool(usage),
},
"raw_response": response,
}
async def _request(self, provider: ProviderConfig, payload: dict[str, Any]) -> dict[str, Any]:
headers = {"Authorization": f"Bearer {provider.api_key}", "Content-Type": "application/json"}
last_error: Exception | None = None
for attempt in range(3):
try:
async with httpx.AsyncClient(timeout=self.settings.llm_timeout_s) as client:
endpoint = "responses" if provider.api_style == "responses" else "chat/completions"
response = await asyncio.wait_for(client.post(f"{provider.base_url}/{endpoint}", headers=headers, json=payload), timeout=self.settings.llm_timeout_s)
if response.status_code >= 500 or response.status_code in {408, 429}:
raise StructuredTransportError(f"Provider temporary failure ({response.status_code}): {response.text[:300]}")
if response.status_code >= 400:
raise StructuredModelError(f"Provider rejected structured request ({response.status_code}): {response.text[:500]}")
try:
value = response.json()
except ValueError as error:
raise StructuredModelError("Provider response is not valid JSON") from error
if not isinstance(value, dict):
raise StructuredModelError("Provider response is not a JSON object")
if provider.api_style == "responses":
if not isinstance(value.get("output"), list):
raise StructuredModelError("Provider response has no Responses API output array")
elif not isinstance(value.get("choices"), list) or not value["choices"]:
raise StructuredModelError("Provider response has no completion choices")
return value
except (httpx.HTTPError, asyncio.TimeoutError, StructuredTransportError) as error:
last_error = error
if attempt < 2:
await asyncio.sleep(0.5 * (2**attempt))
raise StructuredTransportError(f"Provider transport unavailable after 3 attempts: {last_error}")
@@ -0,0 +1,19 @@
"""Adapter exposing the pure verifier registry through the application port."""
from __future__ import annotations
from typing import Any
from app.cad_agent.domain.verifier_registry import VerifierRegistry
class RegistryVerifierExecutor:
def __init__(self, registry: VerifierRegistry) -> None:
self.registry = registry
def evaluate(self, claims: list[dict[str, Any]], facts: dict[str, Any]) -> list[dict[str, Any]]:
results: list[dict[str, Any]] = []
for claim in claims:
result = self.registry.evaluate(str(claim["claim_kind"]), claim["expected"], facts)
results.append({"claim_id": claim["claim_id"], "claim_kind": claim["claim_kind"], "deterministic": self.registry.definition(str(claim["claim_kind"])).deterministic, **result})
return results