Skip to content

Commit b2cb869

Browse files
committed
feat: register planning artifacts in run manifests
1 parent cdfc07f commit b2cb869

4 files changed

Lines changed: 80 additions & 1 deletion

File tree

apps/orchestrator/src/cortexpilot_orch/api/main_pm_intake_helpers.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import hashlib
34
import json
45
import threading
56
from datetime import datetime, timezone
@@ -119,6 +120,29 @@ def _strip_intake_only_contract_fields(contract: dict[str, Any]) -> dict[str, An
119120
return sanitized
120121

121122

123+
def _artifact_ref_for_path(path: Path, *, rel_path: str, name: str, media_type: str = "application/json") -> dict[str, Any]:
124+
payload = path.read_bytes()
125+
return {
126+
"name": name,
127+
"path": rel_path,
128+
"sha256": hashlib.sha256(payload).hexdigest(),
129+
"media_type": media_type,
130+
"size_bytes": len(payload),
131+
}
132+
133+
134+
def _append_manifest_artifact(manifest: dict[str, Any], ref: dict[str, Any]) -> None:
135+
artifacts = manifest.get("artifacts") if isinstance(manifest.get("artifacts"), list) else []
136+
key = (str(ref.get("name") or ""), str(ref.get("path") or ""))
137+
for item in artifacts:
138+
if not isinstance(item, dict):
139+
continue
140+
if (str(item.get("name") or ""), str(item.get("path") or "")) == key:
141+
return
142+
artifacts.append(ref)
143+
manifest["artifacts"] = artifacts
144+
145+
122146
def _safe_read_intake_store_payload(store: object, method_name: str, intake_id: str) -> dict[str, Any]:
123147
reader = getattr(store, method_name, None)
124148
if not callable(reader):
@@ -144,18 +168,33 @@ def _persist_planning_artifacts_for_run(
144168
return []
145169

146170
run_store = RunStore(runs_root=runs_root)
171+
run_dir = run_store.run_dir(run_id)
147172
artifacts_to_write: list[tuple[str, Any]] = [
148173
("planning_wave_plan.json", _build_wave_plan(plan_bundle)),
149174
("planning_worker_prompt_contracts.json", _build_worker_prompt_contracts(plan_bundle, intake_payload)),
150175
]
151176
written: list[str] = []
177+
artifact_refs: list[dict[str, Any]] = []
152178
for filename, payload in artifacts_to_write:
153179
if payload in ({}, [], None):
154180
continue
155-
run_store.write_artifact(run_id, filename, json.dumps(payload, ensure_ascii=False, indent=2))
181+
artifact_path = run_store.write_artifact(run_id, filename, json.dumps(payload, ensure_ascii=False, indent=2))
156182
written.append(filename)
183+
artifact_refs.append(
184+
_artifact_ref_for_path(
185+
artifact_path,
186+
rel_path=f"artifacts/{filename}",
187+
name=filename.removesuffix(".json"),
188+
)
189+
)
157190

158191
if written:
192+
manifest_path = run_dir / "manifest.json"
193+
manifest = _read_json_file(manifest_path)
194+
if manifest:
195+
for ref in artifact_refs:
196+
_append_manifest_artifact(manifest, ref)
197+
manifest_path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8")
159198
run_store.append_event(
160199
run_id,
161200
{

apps/orchestrator/src/cortexpilot_orch/scheduler/scheduler_bridge_contract.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
from collections.abc import Callable
4+
import hashlib
45
import json
56
from pathlib import Path
67
from typing import Any
@@ -9,6 +10,29 @@
910
from cortexpilot_orch.store.run_store import RunStore
1011

1112

13+
def _artifact_ref_for_path(path: Path, *, rel_path: str, name: str, media_type: str = "application/json") -> dict[str, Any]:
14+
payload = path.read_bytes()
15+
return {
16+
"name": name,
17+
"path": rel_path,
18+
"sha256": hashlib.sha256(payload).hexdigest(),
19+
"media_type": media_type,
20+
"size_bytes": len(payload),
21+
}
22+
23+
24+
def _append_manifest_artifact(manifest: dict[str, Any], ref: dict[str, Any]) -> None:
25+
artifacts = manifest.get("artifacts") if isinstance(manifest.get("artifacts"), list) else []
26+
key = (str(ref.get("name") or ""), str(ref.get("path") or ""))
27+
for item in artifacts:
28+
if not isinstance(item, dict):
29+
continue
30+
if (str(item.get("name") or ""), str(item.get("path") or "")) == key:
31+
return
32+
artifacts.append(ref)
33+
manifest["artifacts"] = artifacts
34+
35+
1236
class ContractStateWriter:
1337
def __init__(
1438
self,
@@ -215,6 +239,16 @@ def persist_contract_state(
215239
"prompt_artifact.json",
216240
json.dumps(prompt_artifact, ensure_ascii=False, indent=2),
217241
)
242+
if manifest is not None:
243+
_append_manifest_artifact(
244+
manifest,
245+
_artifact_ref_for_path(
246+
prompt_artifact_path,
247+
rel_path="artifacts/prompt_artifact.json",
248+
name="prompt_artifact",
249+
),
250+
)
251+
write_manifest_fn(store, run_id, manifest)
218252
store.append_event(
219253
run_id,
220254
{

apps/orchestrator/tests/test_main_pm_intake_helpers_branches.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -564,12 +564,16 @@ def execute_task(contract_path: Path, mock_mode: bool = False) -> str:
564564
worker_contracts = json.loads(
565565
(runs_root / run_id / "artifacts" / "planning_worker_prompt_contracts.json").read_text(encoding="utf-8")
566566
)
567+
manifest = json.loads((runs_root / run_id / "manifest.json").read_text(encoding="utf-8"))
567568

568569
assert result["planning_artifacts"] == ["planning_wave_plan.json", "planning_worker_prompt_contracts.json"]
569570
assert wave_plan["wave_id"] == "bundle-1"
570571
assert wave_plan["objective"] == "Ship one planning artifact bridge"
571572
assert worker_contracts[0]["prompt_contract_id"] == "worker-1"
572573
assert worker_contracts[0]["continuation_policy"]["on_blocked"] == "spawn_independent_temporary_unblock_task"
574+
artifact_names = [item["name"] for item in manifest["artifacts"]]
575+
assert "planning_wave_plan" in artifact_names
576+
assert "planning_worker_prompt_contracts" in artifact_names
573577
assert intake_events[-1] == ("persist", {"event": "INTAKE_RUN", "run_id": run_id})
574578

575579

apps/orchestrator/tests/test_scheduler_bridge_runtime.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,3 +163,5 @@ def test_persist_contract_state_writes_role_binding_summary_to_manifest(tmp_path
163163
assert prompt_artifact["run_id"] == run_id
164164
assert prompt_artifact["task_id"] == "task-role-binding-summary"
165165
assert prompt_artifact["role_binding_summary"] == build_role_binding_summary(contract)
166+
artifact_names = [item["name"] for item in written["artifacts"]]
167+
assert "prompt_artifact" in artifact_names

0 commit comments

Comments
 (0)