wip: preserve local engine and agent workflow changes before ganjihong refactor integration

This commit is contained in:
2026-09-09 17:08:24 +08:00
parent a5c3ec1f36
commit 387b99e255
112 changed files with 3543 additions and 12177 deletions
+1 -1
View File
@@ -1 +1 @@
"""Infrastructure adapters for the v3 ports."""
"""Infrastructure adapters for the single-stage ports."""
@@ -1,4 +1,4 @@
"""Immutable v3 file artifacts with staging manifests and atomic publication."""
"""Immutable single-stage artifacts with staging manifests and atomic publication."""
from __future__ import annotations
@@ -8,10 +8,9 @@ import os
from pathlib import Path
import re
import secrets
import shutil
from typing import Any
from app.cad_agent.ports import CandidateStage
from app.cad_agent.ports import StagingRevision
_SAFE_RELATIVE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._/-]{0,240}$")
@@ -27,7 +26,7 @@ class FileArtifactStore:
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")
raise ValueError("Invalid CAD task id")
return self.root / task_id
def artifact_path(self, task_id: str, relative_path: str) -> Path:
@@ -43,7 +42,7 @@ class FileArtifactStore:
) -> None:
root = self.task_dir(task_id)
(root / "documents").mkdir(parents=True, exist_ok=True)
(root / "actions").mkdir(exist_ok=True)
(root / "events").mkdir(exist_ok=True)
(root / "revisions").mkdir(exist_ok=True)
(root / ".staging").mkdir(exist_ok=True)
source = root / "source-requirements.md"
@@ -67,14 +66,14 @@ class FileArtifactStore:
if images:
self.write_json_once(task_id, "documents/source-images.json", {"schema_version": "cad.source-images.v1", "images": images})
def sync_action_ledger(self, task_id: str, events: list[dict[str, Any]]) -> str:
def sync_event_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")
path = self._path(task_id, "events/event-ledger.jsonl")
existing: dict[int, dict[str, Any]] = {}
if path.is_file():
for raw in path.read_text(encoding="utf-8").splitlines():
@@ -83,18 +82,18 @@ class FileArtifactStore:
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")
raise ValueError("Event 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}
raise ValueError("Event ledger event has an invalid sequence")
entry = {"schema_version": "cad.event-ledger.v1", **event}
previous = existing.get(sequence)
if previous is not None:
if previous != entry:
raise ValueError("Action ledger diverges from committed SQLite event")
raise ValueError("Event ledger diverges from committed SQLite event")
continue
missing.append(json.dumps(entry, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + "\n")
if missing:
@@ -103,7 +102,7 @@ class FileArtifactStore:
handle.writelines(missing)
handle.flush()
os.fsync(handle.fileno())
return "actions/action-ledger.jsonl"
return "events/event-ledger.jsonl"
def write_source_index(
self,
@@ -136,9 +135,8 @@ class FileArtifactStore:
"""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.
callers cannot choose IDs. Attachment metadata is a closed,
server-provided projection rather than arbitrary model input.
"""
supplied = source_blocks
if supplied is None:
@@ -195,19 +193,6 @@ class FileArtifactStore:
if isinstance(item, dict) and item.get("path") and self._path(task_id, str(item["path"])).is_file()
]
def read_requirements_spec(self, task_id: str, artifact_path: str = "") -> dict[str, Any] | None:
return self.read_json(task_id, artifact_path or "documents/requirements-spec.json")
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():
@@ -225,36 +210,20 @@ class FileArtifactStore:
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:
def start_staging_revision(self, task_id: str, idempotency_key: str, payload: dict[str, Any]) -> StagingRevision:
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, ""))
return StagingRevision(stable_id, str(directory.resolve()))
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]:
def publish_staging_revision(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():
@@ -262,9 +231,8 @@ class FileArtifactStore:
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.
# Rebase report 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)
@@ -272,39 +240,6 @@ class FileArtifactStore:
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")
@@ -316,7 +251,7 @@ class FileArtifactStore:
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")
raise ValueError("Invalid staging revision id")
root = self.task_dir(task_id).resolve()
stage = (root / ".staging" / stage_id).resolve()
if root not in stage.parents:
@@ -351,13 +286,6 @@ class FileArtifactStore:
temporary.write_bytes(data)
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")
@@ -371,8 +299,8 @@ class FileArtifactStore:
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}
raise RuntimeError("Staging revision has no artifacts")
return {"schema_version": "cad.single-stage.artifact-manifest.v1", "files": files}
@staticmethod
def _manifest(directory: Path) -> dict[str, Any] | None:
@@ -1,191 +0,0 @@
"""Bounded, file-backed author guidance for the CDSL workflow.
The corpus is deliberately non-authoritative: contracts, schemas, topology
tokens, and server preflight always remain the executable source of truth.
Loading errors return an empty selection so authoring continues with the
pre-guidance prompt instead of turning documentation into an availability
dependency.
"""
from __future__ import annotations
import json
from dataclasses import dataclass
from pathlib import Path
from typing import Any
from app.cad_agent.domain.state import TaskPhase
from app.cad_agent.ports import AuthorGuidanceSelection
_MIN_CHARS = 1_200
_MAX_CHARS = 6_000
@dataclass(frozen=True, slots=True)
class _Section:
section_id: str
title: str
priority: int
mandatory: bool
content: str
@property
def block(self) -> str:
return f"## {self.title}\n{self.content.strip()}"
@dataclass(frozen=True, slots=True)
class _Corpus:
version: str
sections: dict[str, _Section]
phase_sections: dict[str, tuple[str, ...]]
repair_sections: tuple[str, ...]
final_sections: tuple[str, ...]
operation_sections: dict[str, tuple[str, ...]]
class FileAuthorGuidance:
"""Read and select the checked-in corpus deterministically.
Selection depends exclusively on workflow state and the runtime operation
registry. It intentionally receives neither the user's request nor image
observations, so it cannot become an implicit part-family classifier.
"""
def __init__(self, root: Path, *, enabled: bool = True, max_chars: int = 3_600) -> None:
self.root = root
self.enabled = enabled
self.max_chars = min(_MAX_CHARS, max(_MIN_CHARS, max_chars))
self._corpus: _Corpus | None = None
self._load_error = ""
def select(
self,
*,
phase: TaskPhase,
atomic_id: str,
repair_required: bool,
supported_atomic_ids: tuple[str, ...],
) -> AuthorGuidanceSelection:
if not self.enabled:
return AuthorGuidanceSelection(fallback_reason="guidance_disabled")
corpus = self._load()
if corpus is None:
return AuthorGuidanceSelection(fallback_reason=self._load_error or "guidance_unavailable")
supported = set(supported_atomic_ids)
if set(corpus.operation_sections) != supported:
return AuthorGuidanceSelection(fallback_reason="guidance_operation_coverage_mismatch")
if atomic_id and atomic_id not in supported:
return AuthorGuidanceSelection(fallback_reason="guidance_unknown_atomic_id")
if repair_required:
requested = list(corpus.repair_sections)
elif phase == TaskPhase.FINAL_VALIDATION:
requested = list(corpus.final_sections)
else:
requested = list(corpus.phase_sections.get(phase.value, corpus.phase_sections.get("DEFAULT", ())))
if atomic_id:
requested.extend(corpus.operation_sections[atomic_id])
requested = list(dict.fromkeys(requested))
if not requested:
return AuthorGuidanceSelection(fallback_reason="guidance_no_matching_sections")
mandatory = [section_id for section_id in requested if corpus.sections[section_id].mandatory]
optional = [section_id for section_id in requested if not corpus.sections[section_id].mandatory]
optional.sort(key=lambda section_id: (-corpus.sections[section_id].priority, requested.index(section_id)))
selected: list[str] = []
text = ""
for section_id in [*mandatory, *optional]:
block = corpus.sections[section_id].block
candidate = block if not text else f"{text}\n\n{block}"
if len(candidate) <= self.max_chars:
text = candidate
selected.append(section_id)
elif section_id in mandatory:
# Do not silently drop contract, datum, or operation guidance.
return AuthorGuidanceSelection(fallback_reason="guidance_required_sections_exceed_budget")
return AuthorGuidanceSelection(
version=corpus.version,
section_ids=tuple(selected),
content=text,
enabled=True,
)
def _load(self) -> _Corpus | None:
if self._corpus is not None:
return self._corpus
if self._load_error:
return None
try:
manifest_path = self.root / "manifest.json"
raw = json.loads(manifest_path.read_text(encoding="utf-8"))
if not isinstance(raw, dict):
raise ValueError("manifest is not an object")
version = raw.get("version")
if raw.get("schema_version") != "cdsl.author-guidance.manifest.v1" or not isinstance(version, str) or not version:
raise ValueError("manifest version is invalid")
raw_sections = raw.get("sections")
if not isinstance(raw_sections, list) or not raw_sections:
raise ValueError("manifest sections are invalid")
sections: dict[str, _Section] = {}
root = self.root.resolve()
for item in raw_sections:
if not isinstance(item, dict):
raise ValueError("section declaration is invalid")
section_id = item.get("id")
filename = item.get("file")
title = item.get("title")
priority = item.get("priority")
mandatory = item.get("mandatory", False)
if (
not isinstance(section_id, str) or not section_id
or not isinstance(filename, str) or not filename
or not isinstance(title, str) or not title
or not isinstance(priority, int) or isinstance(priority, bool)
or not isinstance(mandatory, bool)
or section_id in sections
):
raise ValueError("section metadata is invalid")
path = (self.root / filename).resolve()
if root not in path.parents or not path.is_file():
raise ValueError("section file is unavailable")
content = path.read_text(encoding="utf-8").strip()
if not content:
raise ValueError("section content is empty")
sections[section_id] = _Section(section_id, title, priority, mandatory, content)
def identifiers(value: Any, field: str) -> tuple[str, ...]:
if not isinstance(value, list) or not value or not all(isinstance(item, str) and item in sections for item in value):
raise ValueError(f"{field} is invalid")
return tuple(dict.fromkeys(value))
raw_phases = raw.get("phase_sections")
if not isinstance(raw_phases, dict) or "DEFAULT" not in raw_phases:
raise ValueError("phase sections are invalid")
phase_sections = {
phase: identifiers(section_ids, f"phase {phase}")
for phase, section_ids in raw_phases.items()
if isinstance(phase, str)
}
if len(phase_sections) != len(raw_phases):
raise ValueError("phase name is invalid")
operation_sections = {
atomic_id: identifiers(section_ids, f"operation {atomic_id}")
for atomic_id, section_ids in (raw.get("operation_sections") or {}).items()
if isinstance(atomic_id, str)
}
if not operation_sections or len(operation_sections) != len(raw.get("operation_sections") or {}):
raise ValueError("operation sections are invalid")
self._corpus = _Corpus(
version=version,
sections=sections,
phase_sections=phase_sections,
repair_sections=identifiers(raw.get("repair_sections"), "repair sections"),
final_sections=identifiers(raw.get("final_sections"), "final sections"),
operation_sections=operation_sections,
)
return self._corpus
except (OSError, ValueError, TypeError, json.JSONDecodeError) as error:
self._load_error = f"guidance_load_failed:{type(error).__name__}"
return None
@@ -1,4 +1,4 @@
"""In-process idempotent event delivery for the v3 delivery boundary."""
"""In-process idempotent event delivery for the single-stage protocol."""
from __future__ import annotations
@@ -1,64 +0,0 @@
"""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")
public_payload = {key: value for key, value in payload.items() if key != "reference_image_paths"}
content: list[dict[str, Any]] = [{"type": "text", "text": json.dumps(public_payload, ensure_ascii=False)}]
if kind in {"image_observation", "final"}:
for raw_path in payload.get("reference_image_paths") or ():
path = Path(str(raw_path))
if path.is_file():
content.append(self._image_part(path))
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')}"}}
File diff suppressed because it is too large Load Diff
@@ -1,5 +1,8 @@
"""SQLite repository: the sole mutable state authority for protocol v3."""
"""SQLite authority for ``cad.single-stage.v1`` tasks.
There is no reader or migration for the deleted feature-DAG protocol. Opening
a legacy task database drops its task data before creating this schema.
"""
from __future__ import annotations
from contextlib import contextmanager
@@ -10,11 +13,13 @@ 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.domain.state import TaskPhase, TaskState
from app.cad_agent.ports import InvocationRecord
class SqliteTaskRepository:
PROTOCOL_VERSION = "cad.single-stage.v1"
def __init__(self, database_path: Path) -> None:
self.database_path = database_path
self.database_path.parent.mkdir(parents=True, exist_ok=True)
@@ -35,271 +40,151 @@ class SqliteTaskRepository:
def _initialize(self) -> None:
with self._lock, self._connection() as connection:
# Protocol 3.2 intentionally has no migration path from the
# structured-only / review-loop task model. Deployment starts with
# an empty task database, as those tasks do not have immutable
# Markdown source artifacts to compile from.
existing = connection.execute("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'tasks'").fetchone()
if existing is not None and "'3.2'" not in str(existing[0] or ""):
existing = connection.execute("SELECT sql FROM sqlite_master WHERE type='table' AND name='tasks'").fetchone()
if existing is not None and self.PROTOCOL_VERSION not in str(existing[0] or ""):
self.protocol_reset = True
connection.executescript("""
DROP TABLE IF EXISTS outbox;
DROP TABLE IF EXISTS tool_audits;
DROP TABLE IF EXISTS usage_records;
DROP TABLE IF EXISTS invocations;
DROP TABLE IF EXISTS ledger;
DROP TABLE IF EXISTS model_capabilities;
DROP TABLE IF EXISTS tasks;
""")
connection.executescript(
"""
# This database is dedicated to CAD task state. An old
# protocol has no safe reader or migration, so remove every
# persisted task table and let SQLite remove their indexes.
tables = connection.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
).fetchall()
for row in tables:
name = str(row[0]).replace('"', '""')
connection.execute(f'DROP TABLE IF EXISTS "{name}"')
connection.executescript(f"""
CREATE TABLE IF NOT EXISTS tasks (
task_id TEXT PRIMARY KEY,
protocol_version TEXT NOT NULL CHECK(protocol_version = '3.2'),
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_spec_path TEXT NOT NULL DEFAULT '',
requirements_document_path TEXT NOT NULL DEFAULT '',
completion_target_path TEXT NOT NULL DEFAULT '',
modeling_plan_path TEXT NOT NULL DEFAULT '',
feature_plan_path TEXT NOT NULL DEFAULT '',
feature_plan_hash TEXT NOT NULL DEFAULT '',
feature_stage_id TEXT NOT NULL DEFAULT '',
protocol_version TEXT NOT NULL CHECK(protocol_version = '{self.PROTOCOL_VERSION}'),
request TEXT NOT NULL, phase TEXT NOT NULL, state_version INTEGER NOT NULL,
active_revision TEXT NOT NULL DEFAULT '', repair_count INTEGER NOT NULL DEFAULT 0,
last_error TEXT, retry_from_phase TEXT NOT NULL DEFAULT '',
requirements_path TEXT NOT NULL DEFAULT '', authoring_path TEXT NOT NULL DEFAULT '',
runtime_cdsl_path TEXT NOT NULL DEFAULT '', compile_audit_path TEXT NOT NULL DEFAULT '',
diagnostics_path TEXT NOT NULL DEFAULT '', completion_path TEXT NOT NULL DEFAULT '',
clarification_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
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)
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
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
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
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
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)
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)
);
"""
)
""")
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.2', ?, ?, 0)",
(task_id, request, TaskPhase.DRAFTING_REQUIREMENTS_DOCUMENT.value),
"INSERT OR IGNORE INTO tasks(task_id,protocol_version,request,phase,state_version) VALUES (?,?,?,?,0)",
(task_id, self.PROTOCOL_VERSION, request, TaskPhase.ANALYZING_REQUEST.value),
)
state = self.get_state(task_id)
if state is None:
raise RuntimeError("SQLite task insert was not visible")
raise RuntimeError("Task was not persisted")
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
row = connection.execute("SELECT * FROM tasks WHERE task_id=?", (task_id,)).fetchone()
return self._state(row) if row 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": "" if item.get("preview_unavailable") else 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") in {"accepted", "feature_node_verified"} and isinstance(item.get("revision_id"), str)
]
frozen = next((item for item in reversed(events) if item.get("event") == "requirements_compiled"), {})
verification_warnings = [
str(item) for item in frozen.get("verification_warnings") or () if str(item)
] 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",
"no_progress_limit",
"candidate_runtime_execution_failure", "candidate_recovery_runtime_execution_failure",
"failed_author_format", "runtime_contract_invalid",
"completed_best_effort",
}
), {}) if state.phase in {TaskPhase.COMPLETED, 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 []
issues = [str(item) for item in status_event.get("issues") or () if str(item)] if isinstance(status_event, dict) else []
# A later repair can end at authoring or compilation after an earlier
# build already published an executable prefix. Its terminal event
# intentionally contains diagnostics rather than a duplicate path
# map, so keep the most recent event that actually delivered a
# revision. Otherwise the completed projection loses the existing
# STEP/GLB links exactly when best-effort publication matters most.
result: dict[str, Any] = {}
paths: dict[str, Any] = {}
for item in reversed(events):
if item.get("event") not in {
"build_completed", "published_best_effort", "build_failed",
}:
continue
candidate_paths = item.get("paths")
if isinstance(candidate_paths, dict) and candidate_paths:
result, paths = item, candidate_paths
break
return {
"schema_version": "3.2",
"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,
"schema_version": self.PROTOCOL_VERSION, "task_id": 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,
"repair_count": state.repair_count, "repair_budget": 2,
"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_spec_path": state.requirements_spec_path,
"requirements_document_path": state.requirements_document_path,
"completion_target_path": state.completion_target_path,
"modeling_plan_path": state.modeling_plan_path,
"feature_plan_path": state.feature_plan_path,
"feature_plan_hash": state.feature_plan_hash,
"current_feature_node_id": state.pending_feature.node_id if state.pending_feature else "",
"pending_feature": self._pending_payload(state.pending_feature),
"feature_nodes": self._feature_node_projection(events),
"clarification_path": state.clarification_path,
"requirements_contract_path": state.requirements_contract_path,
"verification_status": (
"completed_with_risks" if state.phase == TaskPhase.COMPLETED and (verification_warnings or state.last_error == ErrorCode.BEST_EFFORT_COMPLETED)
else "verified" if state.phase == TaskPhase.COMPLETED
else "pending"
),
"verification_warnings": verification_warnings,
"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,
"requirements_path": state.requirements_path, "authoring_path": state.authoring_path,
"runtime_cdsl_path": state.runtime_cdsl_path, "compile_audit_path": state.compile_audit_path,
"diagnostics_path": state.diagnostics_path, "completion_path": state.completion_path,
"message": str(result.get("message") or ""),
"revisions": ([{
"revision_id": state.active_revision, "status": "success", "visibility": "final" if state.phase == TaskPhase.COMPLETED else "checkpoint",
"cdsl_path": paths.get("model.cdsl.json", ""), "step_path": paths.get("model.step", ""),
"glb_path": paths.get("model.glb", ""), "report_path": paths.get("rebuild-report.json", ""),
}] if state.active_revision else []),
"action_ledger_summary": events[-16:],
}
@staticmethod
def _feature_node_projection(events: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""Return ledger-backed execution evidence; API adds immutable plan fields."""
nodes: dict[str, dict[str, Any]] = {}
for event in events:
node_id = str(event.get("node_id") or "")
if not node_id:
continue
item = nodes.setdefault(node_id, {"node_id": node_id, "status": "pending", "attempt": 0})
if event.get("event") == "feature_node_scheduled":
item.update({"status": "running", "atomic_id": str(event.get("atomic_id") or ""), "priority": event.get("priority"), "claim_ids": event.get("claim_ids") or [], "depends_on": event.get("depends_on") or []})
elif event.get("event") == "feature_node_failed":
item.update({"status": "failed" if event.get("terminal") else "pending", "attempt": int(event.get("attempt") or 0), "failure_class": str(event.get("failure_class") or ""), "error": str(event.get("message") or "")})
elif event.get("event") == "feature_node_verified":
item.update({"status": "done", "revision_id": str(event.get("revision_id") or ""), "feature_id": str(event.get("feature_id") or ""), "evidence": event.get("claim_results") or []})
elif event.get("event") == "feature_node_invalidated" and item.get("status") != "done":
item["status"] = "invalidated"
return list(nodes.values())
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]
rows = connection.execute("SELECT sequence,event_json,created_at FROM ledger WHERE task_id=? ORDER BY sequence", (task_id,)).fetchall()
return [{"sequence": int(row["sequence"]), "at": str(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:
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")
raise ValueError("Invocation completion requires both values")
previous = state.version - 1
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_spec_path = ?,
requirements_document_path = ?, completion_target_path = ?, modeling_plan_path = ?, feature_plan_path = ?, feature_plan_hash = ?, feature_stage_id = ?, clarification_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_spec_path,
state.requirements_document_path, state.completion_target_path, state.modeling_plan_path,
state.feature_plan_path, state.feature_plan_hash, state.feature_stage_id,
state.clarification_path, state.requirements_contract_path,
state.task_id, previous_version,
),
)
cursor = connection.execute("""
UPDATE tasks SET phase=?,state_version=?,active_revision=?,repair_count=?,last_error=?,retry_from_phase=?,
requirements_path=?,authoring_path=?,runtime_cdsl_path=?,compile_audit_path=?,diagnostics_path=?,completion_path=?,clarification_path=?,updated_at=CURRENT_TIMESTAMP
WHERE task_id=? AND state_version=?
""", (
state.phase.value, state.version, state.active_revision, state.repair_count,
state.last_error.value if state.last_error else None, state.retry_from_phase.value if state.retry_from_phase else "",
state.requirements_path, state.authoring_path, state.runtime_cdsl_path, state.compile_audit_path,
state.diagnostics_path, state.completion_path, state.clarification_path, state.task_id, previous,
))
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")
for item in events:
encoded = json.dumps(item, 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:
updated = 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 updated.rowcount != 1:
raise ValueError("Invocation is not active")
connection.execute("COMMIT")
return True
except Exception:
@@ -310,144 +195,92 @@ class SqliteTaskRepository:
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:
row = connection.execute("SELECT * FROM invocations WHERE task_id=? AND idempotency_key=?", (task_id, idempotency_key)).fetchone()
if row is None:
connection.execute("INSERT INTO invocations(invocation_id,task_id,idempotency_key,status) VALUES (?,?,?,'processing')", (invocation_id, task_id, idempotency_key))
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))
return InvocationRecord(invocation_id, idempotency_key, "processing")
connection.execute("COMMIT")
return InvocationRecord(invocation_id, idempotency_key, "processing")
return self._invocation(row)
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
row = connection.execute("SELECT * FROM invocations WHERE task_id=? AND invocation_id=?", (task_id, invocation_id)).fetchone()
return self._invocation(row) if row 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")
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))
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)))
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]]:
query = "SELECT event_id,task_id,event_json FROM outbox WHERE published_at IS NULL"
args: tuple[Any, ...] = ()
if task_id is not None:
query += " AND task_id=?"
args += (task_id,)
query += " ORDER BY event_id LIMIT ?"
args += (limit,)
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]
rows = connection.execute(query, args).fetchall()
return [{"event_id": int(row["event_id"]), "task_id": str(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,))
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)))
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)),
)
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
]
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,
}
rows = connection.execute("SELECT usage_json FROM usage_records WHERE task_id=? ORDER BY usage_id", (task_id,)).fetchall()
return {"calls": len(rows), "records": [json.loads(row["usage_json"]) for row in rows]}
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)
terminal = tuple(item.value for item in (TaskPhase.COMPLETED, TaskPhase.FAILED, TaskPhase.CANCELLED, TaskPhase.WAITING_FOR_USER))
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()
rows = connection.execute("SELECT task_id FROM tasks WHERE phase NOT IN (?,?,?,?) ORDER BY created_at", terminal).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
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": str(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)))
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"],
node_id=str(raw_pending.get("node_id") or ""), plan_hash=str(raw_pending.get("plan_hash") or ""),
claim_ids=tuple(raw_pending.get("claim_ids") or ()), depends_on_node_ids=tuple(raw_pending.get("depends_on_node_ids") or ()),
) 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"]),
active_revision=str(row["active_revision"]), repair_count=int(row["repair_count"]),
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_spec_path=str(row["requirements_spec_path"] or ""),
requirements_document_path=str(row["requirements_document_path"] or ""),
completion_target_path=str(row["completion_target_path"] or ""),
modeling_plan_path=str(row["modeling_plan_path"] or ""),
feature_plan_path=str(row["feature_plan_path"] or ""),
feature_plan_hash=str(row["feature_plan_hash"] or ""),
feature_stage_id=str(row["feature_stage_id"] or ""),
clarification_path=str(row["clarification_path"] or ""),
requirements_contract_path=str(row["requirements_contract_path"] or ""),
requirements_path=str(row["requirements_path"]), authoring_path=str(row["authoring_path"]),
runtime_cdsl_path=str(row["runtime_cdsl_path"]), compile_audit_path=str(row["compile_audit_path"]),
diagnostics_path=str(row["diagnostics_path"]), completion_path=str(row["completion_path"]), clarification_path=str(row["clarification_path"]),
)
@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, "node_id": pending.node_id, "plan_hash": pending.plan_hash, "claim_ids": list(pending.claim_ids), "depends_on_node_ids": list(pending.depends_on_node_ids)}
@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)
@@ -460,6 +293,6 @@ class SqliteTaskRepository:
return "failed"
if phase == TaskPhase.CANCELLED:
return "cancelled"
if phase in {TaskPhase.WAITING_RETRY, TaskPhase.WAITING_FOR_USER}:
return phase.value.lower()
if phase == TaskPhase.WAITING_FOR_USER:
return "waiting_for_user"
return "running"
@@ -1,4 +1,4 @@
"""The sole OpenAI-compatible structured-output adapter for protocol v3.
"""The sole OpenAI-compatible structured-output adapter for Authoring CDSL.
Raw tool arguments are intentionally preserved. Callers must run their
Pydantic/JSON-Schema canonical validator before causing any state transition.
@@ -170,39 +170,12 @@ class StructuredModelGateway:
@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.
"""
"""Probe the two standalone structured documents used by this protocol."""
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."
)
if name == "write_authoring_cdsl":
instruction += " Return cad.author.v1 with document-local names only; do not include runtime IDs, stable selectors, snapshots, or selector tokens."
return [{"role": "system", "content": instruction}]
def _provider_model(self, provider_id: str, model_id: str) -> tuple[ProviderConfig, ProviderModel]:
@@ -256,7 +229,7 @@ class StructuredModelGateway:
force_tool_name: bool = True,
) -> 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.")
raise StructuredModelError("The Authoring protocol requires exactly one named tool per provider call.")
request_options = provider.request_options if include_reasoning else {}
if provider.api_style == "responses":
payload: dict[str, Any] = {
@@ -1,19 +0,0 @@
"""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