test_mission_workbench.py 9.6 KB

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