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
+22 -123
View File
@@ -7,13 +7,11 @@ from fastapi import FastAPI, File, HTTPException, UploadFile
from fastapi.responses import JSONResponse, StreamingResponse
from app.models.contracts import ChatRequest, ConversationPatch
from app.cad_agent.domain.feature_plan import FeaturePlan, FeatureScheduler
from app.services.agent_service import AgentService
from app.services.library import CdslLibrary
from app.services.storage import WorkspaceStore, safe_conversation_id, safe_task_id
from app.services.attachments import attachment_record, classify_upload, extract_document_text
from app.services.image_processing import image_metadata
from app.services.review_renderer import renderer_status
from app.settings import get_settings
@@ -57,12 +55,6 @@ async def config() -> dict[str, Any]:
for model in provider.models
],
})
try:
settings.resolve_review_model()
renderer_ready, renderer_detail = renderer_status()
review_error = "" if renderer_ready else renderer_detail
except ValueError as error:
review_error = str(error)
return {
"default_provider": settings.default_provider_id,
"default_model": settings.llm_model,
@@ -71,8 +63,6 @@ async def config() -> dict[str, Any]:
"configured": settings.llm_configured,
"library_samples": library.count(),
"autonomous_generation": settings.autonomous_generation,
"review_configured": not review_error,
"review_error": review_error,
}
@@ -123,7 +113,7 @@ async def upload_conversation_attachment(
if current is None:
raise HTTPException(status_code=404, detail="Conversation not found")
active_task_id = str(current.get("current_task_id") or "")
active_task = agent.v3.repository.get_task_projection(active_task_id) if active_task_id else None
active_task = agent.cad.repository.get_task_projection(active_task_id) if active_task_id else None
if str((active_task or {}).get("lifecycle") or "") == "running":
raise HTTPException(status_code=409, detail="CAD task is running; attachments are locked until it reaches a terminal state")
kind = classify_upload(filename, file.content_type or "", len(data))
@@ -145,101 +135,25 @@ async def upload_conversation_attachment(
async def read_task(task_id: str) -> JSONResponse:
try:
safe_id = safe_task_id(task_id)
task = agent.v3.repository.get_task_projection(safe_id)
task = agent.cad.repository.get_task_projection(safe_id)
except ValueError as error:
raise HTTPException(status_code=400, detail=str(error)) from error
if task is None:
raise HTTPException(status_code=404, detail="Task not found")
task["preview_revision"] = str(task.get("active_revision") or task.get("current_revision") or "")
state = agent.v3.repository.get_state(safe_id)
task["requirements_spec"] = agent.v3.artifacts.read_requirements_spec(safe_id, state.requirements_spec_path) if state is not None else None
task["requirements_contract"] = agent.v3.artifacts.read_requirements_contract(safe_id, state.requirements_contract_path) if state is not None else None
task["claim_summary"] = _claim_summary(task["requirements_contract"], task.get("action_ledger_summary"))
task["checklist_progress"] = _checklist_progress(task["requirements_contract"], task["claim_summary"])
requirements_path = agent.v3.artifacts.artifact_path(safe_id, state.requirements_document_path) if state is not None and state.requirements_document_path else None
task["requirements_markdown"] = requirements_path.read_text(encoding="utf-8") if requirements_path and requirements_path.is_file() else None
target_path = agent.v3.artifacts.artifact_path(safe_id, state.completion_target_path) if state is not None and state.completion_target_path else None
plan_path = agent.v3.artifacts.artifact_path(safe_id, state.feature_plan_path) if state is not None and state.feature_plan_path else None
result_path = agent.v3.artifacts.task_dir(safe_id) / "completion-result.md"
task["completion_target_markdown"] = target_path.read_text(encoding="utf-8") if target_path and target_path.is_file() else None
task["completion_target_path"] = state.completion_target_path if target_path and target_path.is_file() else ""
task["feature_plan"] = agent.v3.artifacts.read_json(safe_id, state.feature_plan_path) if state is not None and state.feature_plan_path else None
task["feature_plan_path"] = state.feature_plan_path if plan_path and plan_path.is_file() else ""
task["feature_plan_hash"] = state.feature_plan_hash if state is not None else ""
if isinstance(task["feature_plan"], dict):
plan = FeaturePlan.model_validate(task["feature_plan"])
statuses = FeatureScheduler(plan, agent.v3.repository.ledger_events(safe_id)).statuses()
node_evidence = {str(item.get("node_id") or ""): item for item in task.get("feature_nodes") or () if isinstance(item, dict)}
task["feature_nodes"] = [
{
"node_id": str(node.get("node_id") or ""), "intent": str(node.get("intent") or ""),
"atomic_id": str(node.get("atomic_id") or ""), "priority": node.get("priority"),
"depends_on": node.get("depends_on") or [], "claim_ids": node.get("claim_ids") or [],
"status": statuses.get(str(node.get("node_id") or ""), "pending"),
**node_evidence.get(str(node.get("node_id") or ""), {}),
}
for node in task["feature_plan"].get("nodes") or () if isinstance(node, dict)
]
task["completion_result_markdown"] = result_path.read_text(encoding="utf-8") if result_path.is_file() else None
task["completion_result_path"] = "completion-result.md" if result_path.is_file() else ""
task["usage"] = agent.v3.repository.usage_summary(safe_id)
task["preview_revision"] = str(task.get("active_revision") or "")
state = agent.cad.repository.get_state(safe_id)
task["requirements_analysis"] = agent.cad.artifacts.read_json(safe_id, state.requirements_path) if state and state.requirements_path else None
task["authoring_cdsl"] = agent.cad.artifacts.read_json(safe_id, state.authoring_path) if state and state.authoring_path else None
task["runtime_cdsl"] = agent.cad.artifacts.read_json(safe_id, state.runtime_cdsl_path) if state and state.runtime_cdsl_path else None
task["compile_audit"] = agent.cad.artifacts.read_json(safe_id, state.compile_audit_path) if state and state.compile_audit_path else None
task["diagnostics"] = agent.cad.artifacts.read_json(safe_id, state.diagnostics_path) if state and state.diagnostics_path else None
task["claim_report"] = agent.cad.artifacts.read_json(safe_id, "documents/claim-report.json")
result_path = agent.cad.artifacts.artifact_path(safe_id, state.completion_path) if state and state.completion_path else None
task["completion_result_markdown"] = result_path.read_text(encoding="utf-8") if result_path and result_path.is_file() else None
task["usage"] = agent.cad.repository.usage_summary(safe_id)
return JSONResponse(task)
def _claim_summary(contract: dict[str, Any] | None, ledger: Any) -> list[dict[str, Any]]:
"""Project frozen claims with the newest committed verification evidence.
This is API-only data derived from SQLite-backed ledger entries and the
state-referenced frozen contract. It never parses Markdown or exposes a
staged candidate as a task checkpoint.
"""
latest: dict[str, dict[str, Any]] = {}
for event in reversed(ledger if isinstance(ledger, list) else []):
if not isinstance(event, dict) or event.get("event") not in {"accepted", "completed", "feature_node_verified", "final_visual_reviewed"}:
continue
values = event.get("claim_results")
if not isinstance(values, list):
values = event.get("coverage")
if not isinstance(values, list):
continue
for value in values:
if isinstance(value, dict) and isinstance(value.get("claim_id"), str) and value["claim_id"] not in latest:
latest[value["claim_id"]] = value
result: list[dict[str, Any]] = []
for requirement in (contract or {}).get("requirements") or ():
if not isinstance(requirement, dict):
continue
for claim in requirement.get("acceptance_claims") or ():
if not isinstance(claim, dict) or not isinstance(claim.get("claim_id"), str):
continue
evidence = latest.get(claim["claim_id"], {})
result.append({
"requirement_id": str(requirement.get("requirement_id") or ""),
"claim_id": claim["claim_id"],
"claim_kind": str(claim.get("claim_kind") or ""),
"deterministic": claim.get("verification_mode") == "deterministic",
"status": str(evidence.get("status") or "pending"),
"evidence": evidence.get("evidence") if isinstance(evidence.get("evidence"), dict) else {},
})
return result
def _checklist_progress(contract: dict[str, Any] | None, claims: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""Expose server-evaluated checklist progress without parsing Markdown."""
by_claim = {str(item.get("claim_id") or ""): str(item.get("status") or "pending") for item in claims if isinstance(item, dict)}
progress: list[dict[str, Any]] = []
for requirement in (contract or {}).get("requirements") or ():
if not isinstance(requirement, dict):
continue
statuses = [by_claim.get(str(claim.get("claim_id") or ""), "pending") for claim in requirement.get("acceptance_claims") or () if isinstance(claim, dict)]
progress.append({
"requirement_id": str(requirement.get("requirement_id") or ""),
"statement": str(requirement.get("statement") or ""),
"status": "pass" if statuses and all(status == "pass" for status in statuses) else "fail" if "fail" in statuses or "unavailable" in statuses else "pending",
})
return progress
@app.delete("/v1/tasks/{task_id}")
async def cancel_task(task_id: str) -> JSONResponse:
try:
@@ -281,19 +195,19 @@ async def read_artifact(task_id: str, artifact_path: str) -> StreamingResponse:
try:
safe_id = safe_task_id(task_id)
path = agent.v3.artifacts.artifact_path(safe_id, artifact_path)
path = agent.cad.artifacts.artifact_path(safe_id, artifact_path)
except ValueError as error:
raise HTTPException(status_code=400, detail=str(error)) from error
if not path.is_file():
raise HTTPException(status_code=404, detail="Artifact not found")
task = agent.v3.repository.get_task_projection(safe_id) or {}
task = agent.cad.repository.get_task_projection(safe_id) or {}
parts = artifact_path.split("/")
revision_id = parts[1] if len(parts) >= 3 and parts[0] == "revisions" else ""
published_revision = str(task.get("active_revision") or "") if str(task.get("lifecycle") or "") == "completed" else ""
published_revision = str(task.get("published_revision") or "")
active_revision = str(task.get("active_revision") or task.get("current_revision") or "")
if not revision_id:
# The task directory also contains candidate staging, agent audit and
# frozen-input files. None of those are a public artifact surface.
if artifact_path == "completion-result.md" and str(task.get("completion_path") or "") == artifact_path:
return FileResponse(path, filename=path.name)
raise HTTPException(status_code=403, detail="This task artifact is not public")
if revision_id == published_revision:
@@ -302,30 +216,15 @@ async def read_artifact(task_id: str, artifact_path: str) -> StreamingResponse:
f"revisions/{revision_id}/model.step",
f"revisions/{revision_id}/model.glb",
f"revisions/{revision_id}/rebuild-report.json",
f"revisions/{revision_id}/build-diagnostics.json",
f"revisions/{revision_id}/renders/render-manifest.json",
}
if artifact_path not in published_paths:
raise HTTPException(status_code=403, detail="Only final delivery artifacts are downloadable")
return FileResponse(path, filename=path.name)
v32_checkpoint_paths = {
f"revisions/{revision_id}/model.cdsl.json",
f"revisions/{revision_id}/model.step",
f"revisions/{revision_id}/model.glb",
f"revisions/{revision_id}/rebuild-report.json",
}
verified_revisions = {
str(item.get("revision_id") or "")
for item in task.get("revisions") or ()
if isinstance(item, dict) and item.get("status") == "success"
}
# Each v3.2 node revision is immutable and manifest-published. Make those
# checkpoints inspectable from the DAG while keeping every staging input,
# rejected candidate and arbitrary task artifact private.
if task.get("schema_version") == "3.2" and revision_id in verified_revisions and artifact_path in v32_checkpoint_paths:
return FileResponse(path, media_type="model/gltf-binary" if path.suffix == ".glb" else None, headers={"Content-Disposition": "inline" if path.suffix == ".glb" else f"attachment; filename={path.name}"})
# A legacy running task may render its active checkpoint in the browser,
# but cannot expose any other checkpoint artifact or failed-task preview.
# A running task can render its active executable prefix, but its staging
# inputs and diagnostics remain private until publication.
if (
str(task.get("lifecycle") or "") != "running"
or revision_id != active_revision