Skip to content

Commit 75d1cf1

Browse files
committed
feat: persist prompt artifacts per run
1 parent a668fbb commit 75d1cf1

4 files changed

Lines changed: 76 additions & 1 deletion

File tree

apps/orchestrator/src/cortexpilot_orch/contract/compiler.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,53 @@ def build_role_binding_summary(contract: dict[str, Any]) -> dict[str, Any]:
421421
}
422422

423423

424+
def build_prompt_artifact(
425+
contract: dict[str, Any],
426+
*,
427+
run_id: str = "",
428+
task_id: str = "",
429+
) -> dict[str, Any]:
430+
role_contract = contract.get("role_contract") if isinstance(contract.get("role_contract"), dict) else {}
431+
if not role_contract:
432+
role_contract = _build_role_contract(contract, _load_agent_registry())
433+
assigned_agent = contract.get("assigned_agent") if isinstance(contract.get("assigned_agent"), dict) else {}
434+
role = str(
435+
assigned_agent.get("role")
436+
or (role_contract.get("identity", {}) if isinstance(role_contract.get("identity"), dict) else {}).get("role")
437+
or "WORKER"
438+
).strip().upper() or "WORKER"
439+
role_contract = _merge_role_config_defaults(
440+
role_contract,
441+
_find_role_config_defaults(_load_role_config_registry(), role),
442+
)
443+
identity = role_contract.get("identity") if isinstance(role_contract.get("identity"), dict) else {}
444+
runtime_binding_raw = role_contract.get("runtime_binding") if isinstance(role_contract.get("runtime_binding"), dict) else {}
445+
runtime_binding = {
446+
"runner": _normalize_optional_ref(runtime_binding_raw.get("runner")),
447+
"provider": _normalize_optional_ref(runtime_binding_raw.get("provider")),
448+
"model": _normalize_optional_ref(runtime_binding_raw.get("model")),
449+
}
450+
resolved_task_id = str(task_id or contract.get("task_id") or "").strip()
451+
return {
452+
"artifact_type": "prompt_artifact",
453+
"version": "v1",
454+
"source": "contract-derived",
455+
"execution_authority": "task_contract",
456+
"run_id": str(run_id or "").strip(),
457+
"task_id": resolved_task_id,
458+
"assigned_agent": {
459+
"role": role,
460+
"agent_id": str(identity.get("agent_id") or assigned_agent.get("agent_id") or "").strip(),
461+
},
462+
"purpose": str(role_contract.get("purpose") or "").strip(),
463+
"system_prompt_ref": _normalize_optional_ref(role_contract.get("system_prompt_ref")),
464+
"skills_bundle_ref": _normalize_optional_ref(role_contract.get("skills_bundle_ref")),
465+
"mcp_bundle_ref": _normalize_optional_ref(role_contract.get("mcp_bundle_ref")),
466+
"runtime_binding": runtime_binding,
467+
"role_binding_summary": build_role_binding_summary(contract),
468+
}
469+
470+
424471
def _build_role_contract(contract: dict[str, Any], registry: dict[str, Any] | None) -> dict[str, Any]:
425472
assigned_agent = contract.get("assigned_agent") if isinstance(contract.get("assigned_agent"), dict) else {}
426473
role = str(assigned_agent.get("role") or "WORKER").strip().upper() or "WORKER"

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

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

33
from collections.abc import Callable
4+
import json
45
from pathlib import Path
56
from typing import Any
67

7-
from cortexpilot_orch.contract.compiler import build_role_binding_summary
8+
from cortexpilot_orch.contract.compiler import build_prompt_artifact, build_role_binding_summary
89
from cortexpilot_orch.store.run_store import RunStore
910

1011

@@ -208,5 +209,21 @@ def persist_contract_state(
208209
)
209210
store.write_task_contract(run_id, task_id, contract)
210211
store.write_active_contract(run_id, contract)
212+
prompt_artifact = build_prompt_artifact(contract, run_id=run_id, task_id=task_id)
213+
prompt_artifact_path = store.write_artifact(
214+
run_id,
215+
"prompt_artifact.json",
216+
json.dumps(prompt_artifact, ensure_ascii=False, indent=2),
217+
)
218+
store.append_event(
219+
run_id,
220+
{
221+
"level": "INFO",
222+
"event": "PROMPT_ARTIFACT_WRITTEN",
223+
"run_id": run_id,
224+
"task_id": task_id,
225+
"meta": {"path": str(prompt_artifact_path.relative_to(store.run_dir(run_id)))},
226+
},
227+
)
211228
if ensure_evidence_bundle_fn is not None and failure_reason:
212229
ensure_evidence_bundle_fn(store, run_id, contract, failure_reason)

apps/orchestrator/tests/test_scheduler_bridge_runtime.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,3 +155,11 @@ def test_persist_contract_state_writes_role_binding_summary_to_manifest(tmp_path
155155

156156
written = json.loads((store._runs_root / run_id / "manifest.json").read_text(encoding="utf-8"))
157157
assert written["role_binding_summary"] == build_role_binding_summary(contract)
158+
prompt_artifact = json.loads(
159+
(store._runs_root / run_id / "artifacts" / "prompt_artifact.json").read_text(encoding="utf-8")
160+
)
161+
assert prompt_artifact["artifact_type"] == "prompt_artifact"
162+
assert prompt_artifact["execution_authority"] == "task_contract"
163+
assert prompt_artifact["run_id"] == run_id
164+
assert prompt_artifact["task_id"] == "task-role-binding-summary"
165+
assert prompt_artifact["role_binding_summary"] == build_role_binding_summary(contract)

docs/architecture/runtime-topology.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ flowchart LR
7272
`workflow_case_read_model` directly for operator inspection, but those UI
7373
cards remain read-only mirrors below `task_contract`.
7474
- Runtime artifacts (`manifest`, `events.jsonl`, reports) are generated per run.
75+
- Runs may now also persist `artifacts/prompt_artifact.json`, a contract-derived
76+
snapshot of prompt/bundle/runtime-binding refs for that run. It is a
77+
read-only audit artifact, not a second execution authority source.
7578
- Run detail views may now include derived decision packs such as
7679
`incident_pack.json`, while approval queues synthesize `approval_pack`
7780
summaries from run events plus manifest metadata. These are derived operator

0 commit comments

Comments
 (0)