test_mission_workbench.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. from __future__ import annotations
  2. import json
  3. from dataclasses import asdict
  4. from types import SimpleNamespace
  5. from typing import Any
  6. import pytest
  7. from agent.orchestration import AgentRole, ArtifactRef, RoleContextRequest, TaskStatus
  8. from script_build_host.application.mission_workbench import MissionWorkbench, WorkbenchError
  9. from script_build_host.domain.task_contracts import (
  10. ScriptCriterion,
  11. ScriptIntentClass,
  12. ScriptTaskBudget,
  13. ScriptTaskContractV1,
  14. ScriptTaskKind,
  15. )
  16. from script_build_host.domain.workbench import NotModified, semantic_handle
  17. class _TaskStore:
  18. def __init__(self, ledger: Any) -> None:
  19. self.ledger = ledger
  20. async def load(self, root_trace_id: str) -> Any:
  21. assert root_trace_id == "root"
  22. return self.ledger
  23. class _Receipts:
  24. def __init__(self) -> None:
  25. self.events: list[dict[str, Any]] = []
  26. async def append_event(self, _trace_id: str, event: str, payload: dict[str, Any]) -> None:
  27. self.events.append({"event": event, "event_id": len(self.events) + 1, **payload})
  28. async def get_events(self, _trace_id: str) -> list[dict[str, Any]]:
  29. return list(self.events)
  30. class _Bindings:
  31. async def get_by_root(self, root_trace_id: str) -> Any:
  32. assert root_trace_id == "root"
  33. return SimpleNamespace(
  34. root_trace_id="root",
  35. script_build_id=900049,
  36. input_snapshot_id=3,
  37. active_direction_artifact_version_id=None,
  38. )
  39. class _Snapshots:
  40. async def get(self, snapshot_id: str, *, script_build_id: int) -> Any:
  41. assert (snapshot_id, script_build_id) == ("3", 900049)
  42. return SimpleNamespace(
  43. snapshot_id="3",
  44. topic_id=49,
  45. topic={"topic": {"result": "one bounded topic"}},
  46. account={"account_name": "fixture"},
  47. persona_points=tuple({"name": f"point-{index}"} for index in range(80)),
  48. section_patterns=tuple({"name": f"pattern-{index}"} for index in range(80)),
  49. strategies=(),
  50. )
  51. class _Contracts:
  52. def __init__(self, contract: ScriptTaskContractV1 | None = None) -> None:
  53. self.contract = contract
  54. async def read(self, root_trace_id: str, contract_ref: str) -> Any:
  55. assert root_trace_id == "root"
  56. assert contract_ref.startswith("script-build://task-contracts/sha256/")
  57. assert self.contract is not None
  58. return SimpleNamespace(contract=self.contract)
  59. class _Artifact:
  60. def content_payload(self) -> dict[str, Any]:
  61. return {
  62. "paragraphs": [
  63. {
  64. "paragraph_id": 77,
  65. "name": "opening",
  66. "full_description": "complete candidate prose",
  67. }
  68. ],
  69. "artifact_ref": "script-build://artifact-versions/77",
  70. "canonical_sha256": "sha256:" + "a" * 64,
  71. }
  72. class _Artifacts:
  73. async def read_by_ref(self, ref: ArtifactRef, *, script_build_id: int) -> Any:
  74. assert script_build_id == 900049
  75. assert ref.uri == "script-build://artifact-versions/77"
  76. return SimpleNamespace(artifact=_Artifact())
  77. class _Candidates:
  78. async def read_attempt_workspace(self, *, context: dict[str, Any]) -> dict[str, Any]:
  79. assert context["attempt_id"] == "attempt"
  80. return {
  81. "paragraphs": [
  82. {
  83. "paragraph_id": 77,
  84. "paragraph_index": 1,
  85. "name": "opening",
  86. "content_range": {"scope": "opening"},
  87. "level": 1,
  88. "theme": "theme",
  89. "form": "form",
  90. "function": "function",
  91. "feeling": "feeling",
  92. "description": "description",
  93. "full_description": "complete prose",
  94. }
  95. ],
  96. "elements": [],
  97. "paragraph_element_links": [],
  98. }
  99. def _root_task(*, children: list[str] | None = None) -> Any:
  100. return SimpleNamespace(
  101. task_id="root-task",
  102. parent_task_id=None,
  103. child_task_ids=list(children or []),
  104. display_path="1",
  105. status=TaskStatus.RUNNING,
  106. blocked_reason=None,
  107. current_spec=SimpleNamespace(objective="build script", context_refs=()),
  108. current_spec_version=1,
  109. attempt_ids=[],
  110. decision_ids=[],
  111. )
  112. def _contract() -> ScriptTaskContractV1:
  113. return ScriptTaskContractV1(
  114. task_kind=ScriptTaskKind.PARAGRAPH,
  115. scope_ref="script-build://scopes/full/opening",
  116. intent_class=ScriptIntentClass.EXPLORE,
  117. objective="write the opening",
  118. input_decision_refs=(),
  119. base_artifact_ref=None,
  120. write_scope=("script-build://writes/paragraphs/opening",),
  121. gap_ref=None,
  122. output_schema="paragraph-artifact/v1",
  123. criteria=(ScriptCriterion("criterion", "complete prose"),),
  124. budget=ScriptTaskBudget(),
  125. goal_ids=("goal-1",),
  126. compiler_manifest_digest="sha256:" + "b" * 64,
  127. )
  128. def _worker_task() -> Any:
  129. return SimpleNamespace(
  130. task_id="worker",
  131. parent_task_id="root-task",
  132. child_task_ids=[],
  133. display_path="1.1",
  134. status=TaskStatus.RUNNING,
  135. blocked_reason=None,
  136. current_spec=SimpleNamespace(
  137. objective="write the opening",
  138. context_refs=(
  139. "script-build://task-kinds/paragraph",
  140. "script-build://task-contracts/sha256/contract",
  141. ),
  142. ),
  143. current_spec_version=1,
  144. attempt_ids=["attempt"],
  145. decision_ids=[],
  146. )
  147. @pytest.mark.asyncio
  148. async def test_planner_projection_is_bounded_incremental_and_does_not_copy_retired_tree() -> None:
  149. tasks = {"root-task": _root_task(children=[f"done-{index}" for index in range(120)])}
  150. for index in range(120):
  151. tasks[f"done-{index}"] = SimpleNamespace(
  152. task_id=f"done-{index}",
  153. parent_task_id="root-task",
  154. child_task_ids=[],
  155. display_path=f"1.{index + 1}",
  156. status=TaskStatus.COMPLETED,
  157. blocked_reason=None,
  158. current_spec=SimpleNamespace(
  159. objective="retired detail " + "x" * 1000,
  160. context_refs=(),
  161. ),
  162. current_spec_version=1,
  163. attempt_ids=[],
  164. decision_ids=[],
  165. )
  166. ledger = SimpleNamespace(
  167. revision=49,
  168. root_task_id="root-task",
  169. focused_task_id="root-task",
  170. tasks=tasks,
  171. attempts={},
  172. decisions={},
  173. operations={},
  174. )
  175. receipts = _Receipts()
  176. workbench = MissionWorkbench(
  177. task_store=_TaskStore(ledger),
  178. bindings=_Bindings(),
  179. snapshots=_Snapshots(),
  180. artifacts=_Artifacts(),
  181. contracts=_Contracts(),
  182. receipt_store=receipts,
  183. )
  184. state = await workbench.planner_state("root")
  185. assert not isinstance(state, NotModified)
  186. assert state.retired_task_count == 120
  187. assert len(state.active_subgraph) == 1
  188. assert state.phase_protocol["phase_epoch"] == "script-build-phase-1"
  189. assert state.phase_protocol["current_task"]["allowed_child_kinds"] == ("direction",)
  190. encoded = await workbench.render("root", ledger)
  191. assert len(encoded) < 24_000
  192. assert "retired detail" not in encoded
  193. legacy_full_tree = json.dumps(
  194. [item.current_spec.objective for item in tasks.values()], ensure_ascii=False
  195. )
  196. assert len(encoded) * 2 < len(legacy_full_tree)
  197. for _ in range(15):
  198. assert isinstance(
  199. await workbench.planner_state("root", known_revision="ledger:49"),
  200. NotModified,
  201. )
  202. first_page = await workbench.search_mission_context(
  203. root_trace_id="root",
  204. role="planner",
  205. collection="tasks",
  206. page_size=20,
  207. )
  208. assert 0 < len(first_page["page"]["items"]) <= 20
  209. assert first_page["receipt"]["estimated_tokens"] <= 2_000
  210. assert first_page["page"]["has_more"] is True
  211. assert first_page["page"]["next_cursor"]
  212. assert len(json.dumps(first_page, ensure_ascii=False, separators=(",", ":"))) < 8_000
  213. await workbench.search_mission_context(
  214. root_trace_id="root",
  215. role="planner",
  216. collection="tasks",
  217. page_size=20,
  218. )
  219. progress = await workbench.progress_snapshot("root")
  220. assert len(progress["unique_observations"]) == 1
  221. await workbench.search_mission_context(
  222. root_trace_id="root",
  223. role="planner",
  224. collection="tasks",
  225. query="different semantic scope",
  226. page_size=20,
  227. )
  228. progress = await workbench.progress_snapshot("root")
  229. assert len(progress["unique_observations"]) == 2
  230. @pytest.mark.asyncio
  231. async def test_role_bootstrap_uses_semantic_targets_and_protects_artifact_identities() -> None:
  232. root = _root_task(children=["worker"])
  233. task = _worker_task()
  234. attempt = SimpleNamespace(attempt_id="attempt", failure=None)
  235. ledger = SimpleNamespace(
  236. revision=7,
  237. root_task_id="root-task",
  238. focused_task_id="worker",
  239. tasks={"root-task": root, "worker": task},
  240. attempts={"attempt": attempt},
  241. decisions={},
  242. operations={},
  243. )
  244. workbench = MissionWorkbench(
  245. task_store=_TaskStore(ledger),
  246. bindings=_Bindings(),
  247. snapshots=_Snapshots(),
  248. artifacts=_Artifacts(),
  249. contracts=_Contracts(_contract()),
  250. candidates=_Candidates(),
  251. )
  252. worker_request = RoleContextRequest(
  253. role=AgentRole.WORKER,
  254. root_trace_id="root",
  255. task_id="worker",
  256. spec_version=1,
  257. task_spec={},
  258. attempt_id="attempt",
  259. ledger_revision=7,
  260. )
  261. worker = await workbench.build(worker_request)
  262. encoded_worker = json.dumps(worker, ensure_ascii=False)
  263. assert worker["paragraph_targets"][0]["paragraph_target_key"].startswith("pt_")
  264. assert "paragraph_id" not in encoded_worker
  265. assert len(encoded_worker) < 32_000
  266. artifact_ref = ArtifactRef(
  267. uri="script-build://artifact-versions/77",
  268. kind="paragraph",
  269. version="77",
  270. digest="sha256:" + "a" * 64,
  271. )
  272. validator_request = RoleContextRequest(
  273. role=AgentRole.VALIDATOR,
  274. root_trace_id="root",
  275. task_id="worker",
  276. spec_version=1,
  277. task_spec={},
  278. attempt_id="attempt",
  279. ledger_revision=7,
  280. snapshot_id="snapshot",
  281. artifact_snapshot={
  282. "artifact_refs": [asdict(artifact_ref)],
  283. "evidence_refs": [],
  284. },
  285. )
  286. validator = await workbench.build(validator_request)
  287. encoded_validator = json.dumps(validator, ensure_ascii=False)
  288. assert "complete candidate prose" in encoded_validator
  289. assert "paragraph_id" not in encoded_validator
  290. assert "script-build://artifact-versions/77" not in encoded_validator
  291. assert "canonical_sha256" not in encoded_validator
  292. with pytest.raises(WorkbenchError) as physical:
  293. await workbench.read_mission_context(
  294. root_trace_id="root",
  295. role="planner",
  296. handle="script-build://artifact-versions/77",
  297. )
  298. assert physical.value.code == "CONTEXT_HANDLE_FORMAT_INVALID"
  299. unknown_handle = semantic_handle("unknown", "root", "not-present")
  300. with pytest.raises(WorkbenchError) as unknown:
  301. await workbench.read_mission_context(
  302. root_trace_id="root",
  303. role="planner",
  304. handle=unknown_handle,
  305. )
  306. assert unknown.value.code == "CONTEXT_HANDLE_UNKNOWN"
  307. with pytest.raises(WorkbenchError) as unauthorized:
  308. await workbench.read_mission_context(
  309. root_trace_id="root",
  310. role="worker",
  311. task_id="worker",
  312. attempt_id="attempt",
  313. handle=unknown_handle,
  314. )
  315. assert unauthorized.value.code == "CONTEXT_HANDLE_UNAUTHORIZED"