添加cadfs_to_cdsl代码,优化cad生成

This commit is contained in:
2026-09-03 10:40:51 +08:00
parent f3eb5dff54
commit c25b73c71e
34 changed files with 3586 additions and 147 deletions
@@ -0,0 +1,40 @@
"""Create a resumable live-evaluation task without starting its workflow."""
from __future__ import annotations
import argparse
from dataclasses import replace
import json
from pathlib import Path
import secrets
import sys
from app.cad_agent.composition import compose_v3
from app.settings import get_settings
def _arguments() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Create one isolated live CAD task for resume_one_step.")
parser.add_argument("--report-root", required=True, type=Path)
parser.add_argument("--prompt", required=True)
parser.add_argument("--task-id")
return parser.parse_args()
def main() -> int:
arguments = _arguments()
root = arguments.report_root.resolve()
settings = get_settings()
services = compose_v3(replace(
settings,
task_root=root / "artifacts",
conversation_root=root / "conversations",
))
task_id = arguments.task_id or f"cad_{secrets.token_hex(6)}"
services.workflow.create_task(task_id, arguments.prompt)
print(json.dumps({"task_id": task_id, "report_root": str(root)}, ensure_ascii=False))
return 0
if __name__ == "__main__":
sys.exit(main())
+50 -10
View File
@@ -36,7 +36,7 @@ from app.cad_agent.evals.token_baseline import (
from app.settings import BACKEND_ROOT, get_settings
_CANDIDATE_EVIDENCE_FILES = frozenset({
_LEGACY_CANDIDATE_EVIDENCE_FILES = frozenset({
"candidate.json",
"candidate-review.json",
"model.cdsl.json",
@@ -48,6 +48,19 @@ _CANDIDATE_EVIDENCE_FILES = frozenset({
"renders/contact-sheet.jpg",
})
# A v3.2 Feature DAG publishes one atomic checkpoint after local verification.
# It deliberately has no candidate review or technical render bundle; the
# final review owns that later evidence. GLB is optional because a preview
# conversion outage must not invalidate an otherwise sound STEP checkpoint.
_FEATURE_NODE_EVIDENCE_FILES = frozenset({
"input.json",
"model.cdsl.json",
"model.step",
"model.topology.json",
"node-verification.json",
"rebuild-report.json",
})
_FAILURE_LAYERS = frozenset({
"model_format_or_decision",
"v3_contract_or_verifier",
@@ -240,13 +253,14 @@ def _artifact_evidence_complete(
revisions: list[str],
active_revision: str,
artifact_manifest: dict[str, Any] | None,
ledger: list[dict[str, Any]],
) -> bool:
"""Require every published CAD decision to retain its reviewable evidence.
A non-empty report manifest is not enough: a task could otherwise report
only a rendered contract view while silently losing the STEP, topology, or
candidate review used to accept a revision. Candidate manifests protect
the immutable build inputs/outputs; the report manifest additionally
node verification used to accept a revision. Checkpoint manifests protect
immutable build inputs and outputs; the report manifest additionally
protects the frozen contract and final independent review written later.
"""
if not revisions or not active_revision:
@@ -264,9 +278,19 @@ def _artifact_evidence_complete(
return False
if not all((artifact_root / path).is_file() for path in required_report_files):
return False
feature_revisions = {
str(item.get("revision_id") or "")
for item in ledger
if isinstance(item, dict) and item.get("event") == "feature_node_verified"
}
for revision_id in sorted(set(revisions)):
revision_root = artifact_root / "revisions" / revision_id
required_paths = {revision_root / relative for relative in _CANDIDATE_EVIDENCE_FILES}
evidence_files = (
_FEATURE_NODE_EVIDENCE_FILES
if revision_id in feature_revisions
else _LEGACY_CANDIDATE_EVIDENCE_FILES
)
required_paths = {revision_root / relative for relative in evidence_files}
manifest_path = revision_root / "manifest.json"
if not manifest_path.is_file() or not all(path.is_file() for path in required_paths):
return False
@@ -275,7 +299,7 @@ def _artifact_evidence_complete(
except (OSError, json.JSONDecodeError):
return False
declared = manifest.get("files") if isinstance(manifest, dict) else None
if not isinstance(declared, dict) or not _CANDIDATE_EVIDENCE_FILES.issubset(declared):
if not isinstance(declared, dict) or not evidence_files.issubset(declared):
return False
for relative, digest in declared.items():
path = revision_root / str(relative)
@@ -284,7 +308,7 @@ def _artifact_evidence_complete(
if sha256(path.read_bytes()).hexdigest() != digest:
return False
if artifact_manifest is not None:
expected_report_paths = {f"revisions/{revision_id}/{relative}" for relative in _CANDIDATE_EVIDENCE_FILES}
expected_report_paths = {f"revisions/{revision_id}/{relative}" for relative in evidence_files}
if not expected_report_paths.issubset(report_files):
return False
return True
@@ -356,9 +380,24 @@ def _run_checks(
reviewer_calls = [item for item in records if isinstance(item, dict) and item.get("role") == "reviewer"]
total_tokens = int(usage.get("prompt_tokens") or 0) + int(usage.get("completion_tokens") or 0)
audit_ledger = ledger if ledger is not None else [item for item in projection.get("action_ledger_summary") or () if isinstance(item, dict)]
accepted = [item for item in audit_ledger if isinstance(item, dict) and item.get("event") == "accepted"]
revisions = [str(item.get("revision_id") or "") for item in accepted]
operation_ids = {str(item.get("actual_atomic_id") or "") for item in accepted}
published = [
item for item in audit_ledger
if isinstance(item, dict) and item.get("event") in {"accepted", "feature_node_verified"}
]
revisions = [str(item.get("revision_id") or "") for item in published]
scheduled_atomic_ids = {
str(item.get("node_id") or ""): str(item.get("atomic_id") or "")
for item in audit_ledger
if isinstance(item, dict) and item.get("event") == "feature_node_scheduled"
}
operation_ids = {
str(
item.get("actual_atomic_id")
or item.get("atomic_id")
or scheduled_atomic_ids.get(str(item.get("node_id") or ""), "")
)
for item in published
}
required_operation_groups = [
{str(atomic_id) for atomic_id in group if isinstance(atomic_id, str) and atomic_id}
for group in scenario.get("required_any_atomic_id_groups") or ()
@@ -386,6 +425,7 @@ def _run_checks(
revisions,
active_revision,
artifact_manifest,
audit_ledger,
)
return {
"expected_terminal_phase": projection.get("phase") == scenario.get("expected_phase", "COMPLETED"),
@@ -712,7 +752,7 @@ async def _run(arguments: argparse.Namespace, report_root: Path) -> dict[str, An
"reviewer": str(reviewer_capability.get("mode") or ""),
},
"git_revision": _git_revision(),
"protocol_version": "3.0",
"protocol_version": "3.2",
"runtime_profile_sha256": runtime_profile_hash,
"operation_contracts": contracts,
"verifier_registry_version": "cad.verifier-registry.v1",
@@ -0,0 +1,90 @@
"""Advance one persisted event for a task in an isolated live-evaluation root.
Long real-provider evaluations can outlive a command host's execution window.
This utility takes exactly one event from ``WorkflowCoordinator.run`` and
closes the async generator after that event has been durably handled. Repeated
invocations therefore resume the same task without re-running already
persisted nodes.
"""
from __future__ import annotations
import argparse
import asyncio
from dataclasses import replace
import json
from pathlib import Path
import sys
from typing import Any
from app.cad_agent.application.workflow import ModelIdentity
from app.cad_agent.composition import compose_v3
from app.settings import get_settings
def _arguments() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Advance one event for an isolated live CAD evaluation task.")
parser.add_argument("--report-root", required=True, type=Path)
parser.add_argument("--task-id", required=True)
parser.add_argument("--author-provider")
parser.add_argument("--author-model")
parser.add_argument("--review-provider")
parser.add_argument("--review-model")
return parser.parse_args()
async def _advance(arguments: argparse.Namespace) -> dict[str, Any]:
settings = get_settings()
author_provider, author_model = settings.resolve_model(arguments.author_provider, arguments.author_model)
if arguments.review_provider or arguments.review_model:
review_provider, review_model = settings.resolve_model(arguments.review_provider, arguments.review_model)
else:
review_provider, review_model = settings.resolve_independent_review_model(author_provider, author_model)
report_root = arguments.report_root.resolve()
services = compose_v3(replace(
settings,
task_root=report_root / "artifacts",
conversation_root=report_root / "conversations",
))
before = services.repository.get_state(arguments.task_id)
if before is None:
raise ValueError(f"Unknown task {arguments.task_id!r} in {report_root}")
runner = services.workflow.run(
task_id=arguments.task_id,
author=ModelIdentity(author_provider.id, author_model.id),
reviewer=ModelIdentity(review_provider.id, review_model.id),
)
try:
name, payload = await anext(runner)
except StopAsyncIteration:
name, payload = "workflow_exhausted", {}
finally:
await runner.aclose()
after = services.repository.get_state(arguments.task_id)
return {
"task_id": arguments.task_id,
"event": {"name": name, "payload": payload},
"before": {"phase": before.phase.value, "version": before.version},
"after": {
"phase": after.phase.value if after is not None else "",
"version": after.version if after is not None else -1,
"active_revision": after.active_revision if after is not None else "",
"last_error": after.last_error.value if after is not None and after.last_error else "",
},
}
def main() -> int:
arguments = _arguments()
try:
result = asyncio.run(_advance(arguments))
except BaseException as error:
result = {"error": f"{type(error).__name__}: {str(error)[:1000]}"}
print(json.dumps(result, ensure_ascii=False))
return 2
print(json.dumps(result, ensure_ascii=False))
return 0
if __name__ == "__main__":
sys.exit(main())