test_mission_workbench.py 12 KB

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