chore: save current work
This commit is contained in:
@@ -39,6 +39,7 @@ class FileArtifactStore:
|
||||
request: str,
|
||||
*,
|
||||
source_blocks: list[dict[str, Any]] | None = None,
|
||||
image_inputs: list[dict[str, str]] | None = None,
|
||||
) -> None:
|
||||
root = self.task_dir(task_id)
|
||||
(root / "documents").mkdir(parents=True, exist_ok=True)
|
||||
@@ -50,6 +51,21 @@ class FileArtifactStore:
|
||||
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)
|
||||
images: list[dict[str, str]] = []
|
||||
for position, item in enumerate(image_inputs or (), 1):
|
||||
source_path = Path(str(item.get("path") or "")).resolve()
|
||||
digest = str(item.get("sha256") or "")
|
||||
if not source_path.is_file() or not re.fullmatch(r"[a-f0-9]{64}", digest):
|
||||
raise ValueError("Image input is unavailable or has no valid checksum")
|
||||
data = source_path.read_bytes()
|
||||
if sha256(data).hexdigest() != digest:
|
||||
raise ValueError("Image input checksum mismatch")
|
||||
suffix = source_path.suffix.lower() if source_path.suffix.lower() in {".png", ".jpg", ".jpeg", ".webp"} else ".bin"
|
||||
relative = f"inputs/reference-{position:03d}{suffix}"
|
||||
self._write_bytes_once(self._path(task_id, relative), data)
|
||||
images.append({"path": relative, "mime": str(item.get("mime") or "image/*"), "sha256": digest})
|
||||
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:
|
||||
"""Mirror committed SQLite events into an append-only JSONL audit log.
|
||||
@@ -171,30 +187,16 @@ class FileArtifactStore:
|
||||
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 source_image_paths(self, task_id: str) -> list[str]:
|
||||
manifest = self.read_json(task_id, "documents/source-images.json") or {}
|
||||
return [
|
||||
str(self._path(task_id, str(item.get("path") or "")))
|
||||
for item in manifest.get("images") or ()
|
||||
if isinstance(item, dict) and item.get("path") and self._path(task_id, str(item["path"])).is_file()
|
||||
]
|
||||
|
||||
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_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
|
||||
@@ -338,6 +340,17 @@ class FileArtifactStore:
|
||||
temporary.write_bytes(encoded)
|
||||
os.replace(temporary, path)
|
||||
|
||||
@staticmethod
|
||||
def _write_bytes_once(path: Path, data: bytes) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
if path.exists():
|
||||
if path.read_bytes() != data:
|
||||
raise ValueError(f"Immutable artifact already exists: {path.name}")
|
||||
return
|
||||
temporary = path.with_name(path.name + ".tmp-" + secrets.token_hex(4))
|
||||
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]
|
||||
|
||||
@@ -19,7 +19,13 @@ class RenderedReviewGateway:
|
||||
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)}]
|
||||
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):
|
||||
|
||||
@@ -49,8 +49,8 @@ class SqliteTaskRepository:
|
||||
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_spec_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
|
||||
@@ -100,15 +100,6 @@ class SqliteTaskRepository:
|
||||
);
|
||||
"""
|
||||
)
|
||||
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:
|
||||
@@ -143,21 +134,17 @@ class SqliteTaskRepository:
|
||||
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",
|
||||
"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")]
|
||||
issues = [str(item) for item in status_event.get("issues") or () if str(item)] if isinstance(status_event, dict) else []
|
||||
return {
|
||||
"schema_version": "3.0",
|
||||
"task_id": state.task_id,
|
||||
@@ -172,12 +159,15 @@ class SqliteTaskRepository:
|
||||
"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_spec_path": state.requirements_spec_path,
|
||||
"clarification_path": state.clarification_path,
|
||||
"requirements_contract_path": state.requirements_contract_path,
|
||||
"verification_status": "completed_with_risks" if verification_warnings else "verified",
|
||||
"verification_status": (
|
||||
"completed_with_risks" if state.phase == TaskPhase.COMPLETED and verification_warnings
|
||||
else "verified" if state.phase == TaskPhase.COMPLETED
|
||||
else "pending"
|
||||
),
|
||||
"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,
|
||||
@@ -228,16 +218,16 @@ class SqliteTaskRepository:
|
||||
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
|
||||
candidate_id = ?, candidate_stage_id = ?, repair_required = ?, last_error = ?, retry_from_phase = ?, requirements_spec_path = ?,
|
||||
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_draft_path,
|
||||
state.requirements_review_path, state.requirements_contract_path,
|
||||
state.retry_from_phase.value if state.retry_from_phase else "", state.requirements_spec_path,
|
||||
state.clarification_path, state.requirements_contract_path,
|
||||
state.task_id, previous_version,
|
||||
),
|
||||
)
|
||||
@@ -386,8 +376,8 @@ class SqliteTaskRepository:
|
||||
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_spec_path=str(row["requirements_spec_path"] or ""),
|
||||
clarification_path=str(row["clarification_path"] or ""),
|
||||
requirements_contract_path=str(row["requirements_contract_path"] or ""),
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user