| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874 |
- from __future__ import annotations
- import asyncio
- from dataclasses import replace
- from datetime import UTC, datetime
- from hashlib import sha256
- from pathlib import Path
- from types import SimpleNamespace
- from typing import Any, cast
- import pytest
- from agent import AgentRunner, FileSystemArtifactStore, FileSystemTaskStore, FileSystemTraceStore
- from agent.orchestration import (
- AgentRole,
- ArtifactRef,
- AttemptSubmission,
- CriterionResult,
- DecisionAction,
- OperationStatus,
- OrchestrationConfig,
- TaskCoordinator,
- TaskStatus,
- ValidationVerdict,
- )
- from agent.orchestration.protocols import ValidatorRunResult, WorkerRunResult
- from script_build_host.application.mission_factory import ScriptMissionFactory
- from script_build_host.application.mission_service import ScriptMissionService
- from script_build_host.application.phase_two_planning import PhaseTwoPlanningService
- from script_build_host.domain.errors import ProtocolViolation
- from script_build_host.domain.records import BuildStatus, MissionBinding, Principal
- from script_build_host.domain.task_contracts import (
- PhaseTwoLimits,
- ScriptTaskContractV1,
- TaskContractError,
- )
- from script_build_host.infrastructure.task_contract_store import FileScriptTaskContractStore
- def _contract(
- kind: str,
- *,
- scope: str = "script-build://scopes/full",
- execution_ready: bool = True,
- ) -> dict[str, object]:
- output = {
- "direction": "script-direction/v1",
- "candidate-portfolio": "candidate-portfolio/v1",
- "compose": "structured-script/v1",
- "paragraph": "paragraph-artifact/v1",
- "element-set": "element-set-artifact/v1",
- "structure": "structure-artifact/v1",
- "compare": "comparison-artifact/v1",
- "decode-retrieval": "evidence-record/v1",
- }[kind]
- payload: dict[str, object] = {
- "schema_version": "script-task-contract/v1",
- "task_kind": kind,
- "scope_ref": scope,
- "intent_class": (
- "portfolio"
- if kind == "candidate-portfolio"
- else "compose"
- if kind == "compose"
- else "explore"
- ),
- "objective": f"produce one bounded {kind} increment",
- "input_decision_refs": [],
- "base_artifact_ref": None,
- "write_scope": [scope],
- "gap_ref": None,
- "output_schema": output,
- "criteria": [
- {
- "criterion_id": "closed",
- "description": "the bounded increment is independently verifiable",
- "hard": True,
- }
- ],
- "budget": {
- "max_attempts": 4,
- "max_tokens": 32000,
- "max_seconds": 900,
- "max_external_queries": 40,
- "max_no_improvement": 3,
- },
- "supersedes_decision_ids": [],
- "candidate_closure_decision_refs": [],
- "adopted_decision_ids": [],
- "held_or_rejected_decision_ids": [],
- "compose_order": [],
- "comparison_decision_refs": [],
- }
- if execution_ready and kind in {"compose", "candidate-portfolio"}:
- ref = {
- "decision_id": "decision-1",
- "artifact_ref": {
- "uri": "script-build://artifact-versions/1",
- "kind": "structured_script" if kind == "candidate-portfolio" else "paragraph",
- "version": "1",
- "digest": "sha256:" + "a" * 64,
- },
- "scope_ref": scope,
- "expected_task_kind": "compose" if kind == "candidate-portfolio" else "paragraph",
- }
- payload["candidate_closure_decision_refs"] = [ref]
- payload["adopted_decision_ids"] = ["decision-1"]
- payload["compose_order"] = ["decision-1"]
- return payload
- class _Bindings:
- def __init__(self, root: str) -> None:
- self.binding = MissionBinding(
- binding_id=1,
- script_build_id=7,
- root_trace_id=root,
- input_snapshot_id=11,
- active_direction_artifact_version_id=None,
- accepted_root_artifact_version_id=None,
- engine_version="test",
- schema_version="v1",
- created_at=datetime.now(UTC),
- updated_at=datetime.now(UTC),
- )
- async def get_by_root(self, root: str) -> MissionBinding:
- assert root == self.binding.root_trace_id
- return self.binding
- async def get_by_build(self, script_build_id: int) -> MissionBinding:
- assert script_build_id == self.binding.script_build_id
- return self.binding
- class _PlaceholderThenReplacementExecutor:
- """Exercise Coordinator state transitions without bypassing durable operations."""
- def __init__(self, coordinator: TaskCoordinator) -> None:
- self.coordinator = coordinator
- self.artifact_version = 100
- async def run_worker(self, context: dict[str, Any]) -> WorkerRunResult:
- refs = cast(dict[str, Any], context["task_spec"])["context_refs"]
- kind = next(item.rsplit("/", 1)[-1] for item in refs if "/task-kinds/" in item)
- artifact_kind = {
- "compose": "structured_script",
- "structure": "structure",
- "paragraph": "paragraph",
- "element-set": "element_set",
- }[kind]
- digest = "sha256:" + sha256(context["attempt_id"].encode()).hexdigest()
- version = str(self.artifact_version)
- self.artifact_version += 1
- ref = ArtifactRef(
- f"script-build://artifact-versions/{version}",
- artifact_kind,
- version,
- digest,
- )
- await self.coordinator.submit_attempt(
- {
- "role": AgentRole.WORKER.value,
- "root_trace_id": context["root_trace_id"],
- "task_id": context["task_id"],
- "attempt_id": context["attempt_id"],
- "spec_version": context["spec_version"],
- "trace_id": context["worker_trace_id"],
- "tool_call_id": f"submit:{context['attempt_id']}",
- "operation_id": context["operation_id"],
- "execution_epoch": context["execution_epoch"],
- },
- AttemptSubmission(
- summary=f"kind={artifact_kind};status=frozen",
- artifact_refs=[ref],
- ),
- )
- return WorkerRunResult(context["worker_trace_id"], "completed")
- async def run_validator(self, context: dict[str, Any]) -> ValidatorRunResult:
- refs = cast(dict[str, Any], context["task_spec"])["context_refs"]
- kind = next(item.rsplit("/", 1)[-1] for item in refs if "/task-kinds/" in item)
- placeholder = kind == "compose" and context["spec_version"] == 1
- verdict = ValidationVerdict.FAILED if placeholder else ValidationVerdict.PASSED
- criterion = cast(dict[str, Any], context["task_spec"])["acceptance_criteria"][0]
- await self.coordinator.submit_validation(
- {
- "role": AgentRole.VALIDATOR.value,
- "root_trace_id": context["root_trace_id"],
- "task_id": context["task_id"],
- "attempt_id": context["attempt_id"],
- "validation_id": context["validation_id"],
- "snapshot_id": context["snapshot_id"],
- "trace_id": context["validator_trace_id"],
- "tool_call_id": f"validate:{context['validation_id']}",
- "operation_id": context["operation_id"],
- "execution_epoch": context["execution_epoch"],
- },
- verdict,
- [
- CriterionResult(
- criterion_id=criterion["criterion_id"],
- verdict=verdict,
- reason=(
- "REALIZATION_PLACEHOLDER at artifact.paragraphs[0].description"
- if placeholder
- else "the replacement is concretely rendered"
- ),
- )
- ],
- (
- "hard_defects=1;defect_codes=REALIZATION_PLACEHOLDER"
- if placeholder
- else "hard_defects=0"
- ),
- (),
- ("artifact.paragraphs[0].description",) if placeholder else (),
- ("REALIZATION_PLACEHOLDER",) if placeholder else (),
- "split" if placeholder else "accept",
- )
- return ValidatorRunResult(context["validator_trace_id"], "completed")
- async def stop(self, trace_id: str) -> bool:
- del trace_id
- return True
- async def _service(tmp_path: Path) -> tuple[PhaseTwoPlanningService, TaskCoordinator, str]:
- root = "root-contracts"
- coordinator = TaskCoordinator(
- FileSystemTaskStore(str(tmp_path / "ledger")),
- FileSystemArtifactStore(str(tmp_path / "snapshots")),
- FileSystemTraceStore(str(tmp_path / "traces")),
- )
- await coordinator.ensure_ledger(
- root,
- {
- "objective": "build a script",
- "acceptance_criteria": [
- {"criterion_id": "ready", "description": "ready", "hard": True}
- ],
- "context_refs": ["script-build://inputs/11"],
- },
- )
- service = PhaseTwoPlanningService(
- coordinator=coordinator,
- bindings=_Bindings(root),
- contracts=FileScriptTaskContractStore(tmp_path / "data"),
- )
- return service, coordinator, root
- class _BlockingExecutor:
- def __init__(self) -> None:
- self.started = asyncio.Event()
- self.stopped_trace_ids: list[str] = []
- async def run_worker(self, context: dict[str, Any]) -> WorkerRunResult:
- self.started.set()
- await asyncio.Event().wait()
- raise AssertionError(f"worker unexpectedly resumed: {context['attempt_id']}")
- async def run_validator(self, context: dict[str, Any]) -> ValidatorRunResult:
- raise AssertionError(f"validator must not start: {context['validation_id']}")
- async def stop(self, trace_id: str) -> bool:
- self.stopped_trace_ids.append(trace_id)
- return True
- class _StopState:
- def __init__(self) -> None:
- self.status = BuildStatus.RUNNING
- self.error_summary: str | None = None
- async def get_status(self, script_build_id: int) -> BuildStatus:
- assert script_build_id == 7
- return self.status
- async def set_status(
- self,
- script_build_id: int,
- status: BuildStatus,
- *,
- error_summary: str | None = None,
- ) -> None:
- assert script_build_id == 7
- self.status = status
- self.error_summary = error_summary
- @pytest.mark.asyncio
- async def test_real_operation_stop_converges_and_forbids_new_dispatch(tmp_path: Path) -> None:
- root = "root-stop-operation"
- trace_store = FileSystemTraceStore(str(tmp_path / "stop-traces"))
- coordinator = TaskCoordinator(
- FileSystemTaskStore(str(tmp_path / "stop-ledger")),
- FileSystemArtifactStore(str(tmp_path / "stop-artifacts")),
- trace_store,
- config=OrchestrationConfig(stop_grace_seconds=0.01),
- )
- await coordinator.ensure_ledger(
- root,
- {
- "objective": "build a script",
- "acceptance_criteria": [
- {"criterion_id": "ready", "description": "ready", "hard": True}
- ],
- "context_refs": ["script-build://inputs/11"],
- },
- )
- bindings = _Bindings(root)
- planning = PhaseTwoPlanningService(
- coordinator=coordinator,
- bindings=bindings,
- contracts=FileScriptTaskContractStore(tmp_path / "stop-contracts"),
- )
- portfolio = await planning.plan_script_tasks(
- contract_payloads=[_contract("candidate-portfolio", execution_ready=False)],
- parent_task_id=None,
- context={"root_trace_id": root, "tool_call_id": "stop:portfolio"},
- )
- compose = await planning.plan_script_tasks(
- contract_payloads=[_contract("compose", execution_ready=False)],
- parent_task_id=portfolio["task_ids"][0],
- context={"root_trace_id": root, "tool_call_id": "stop:compose"},
- )
- paragraph = await planning.plan_script_tasks(
- contract_payloads=[_contract("paragraph")],
- parent_task_id=compose["task_ids"][0],
- context={"root_trace_id": root, "tool_call_id": "stop:paragraph"},
- )
- executor = _BlockingExecutor()
- coordinator.set_executor(executor)
- async def unused_llm(**_kwargs: Any) -> Any:
- raise AssertionError("Planner LLM must not run during operation stop")
- runner = AgentRunner(trace_store=trace_store, llm_call=unused_llm)
- state = _StopState()
- class Authorizer:
- async def require_access(self, principal: Principal, script_build_id: int) -> None:
- assert principal.subject == "owner" and script_build_id == 7
- mission = ScriptMissionService(
- runner=runner,
- coordinator=coordinator,
- factory=ScriptMissionFactory(),
- input_snapshots=SimpleNamespace(),
- bindings=bindings,
- legacy_state=state,
- authorizer=Authorizer(),
- direction_reconciler=SimpleNamespace(),
- stop_timeout_seconds=1,
- )
- planning.dispatch_guard = mission
- dispatch = asyncio.create_task(
- planning.dispatch_script_tasks(
- task_ids=paragraph["task_ids"],
- context={"root_trace_id": root, "tool_call_id": "stop:dispatch"},
- )
- )
- await asyncio.wait_for(executor.started.wait(), timeout=1)
- stopped = await mission.stop(7, Principal("owner"))
- dispatch_result = await asyncio.gather(dispatch, return_exceptions=True)
- assert stopped.status is BuildStatus.STOPPED
- assert len(stopped.stopped_operation_ids) == 1
- assert executor.stopped_trace_ids
- assert not isinstance(dispatch_result[0], asyncio.CancelledError)
- ledger = await coordinator.task_store.load(root)
- operation = ledger.operations[stopped.stopped_operation_ids[0]]
- assert operation.status is OperationStatus.STOPPED
- assert ledger.tasks[paragraph["task_ids"][0]].status is TaskStatus.NEEDS_REPLAN
- assert (await mission.stop(7, Principal("owner"))).status is BuildStatus.STOPPED
- with pytest.raises(ProtocolViolation, match="forbidden after stop intent"):
- await planning.dispatch_script_tasks(
- task_ids=paragraph["task_ids"],
- context={"root_trace_id": root, "tool_call_id": "stop:late-dispatch"},
- )
- @pytest.mark.asyncio
- async def test_contract_store_is_content_addressed_and_detects_tampering(tmp_path: Path) -> None:
- store = FileScriptTaskContractStore(tmp_path)
- from script_build_host.domain.task_contracts import ScriptTaskContractV1
- contract = ScriptTaskContractV1.from_payload(_contract("paragraph"))
- first = await store.freeze("root-a", contract)
- second = await store.freeze("root-a", contract)
- assert first == second
- path = tmp_path / "script-task-contracts" / "root-a" / "sha256" / first.uri.rsplit("/", 1)[-1]
- path.write_bytes(b"{}")
- with pytest.raises(TaskContractError, match="TASK_CONTRACT_DIGEST_MISMATCH"):
- await store.read("root-a", first.uri)
- assert not await store.verify("root-a", first.uri, first.digest)
- @pytest.mark.asyncio
- async def test_orphan_contract_reclamation_uses_only_ledger_references(tmp_path: Path) -> None:
- service, _, root = await _service(tmp_path)
- referenced = await service.plan_script_tasks(
- contract_payloads=[_contract("candidate-portfolio", execution_ready=False)],
- parent_task_id=None,
- context={"root_trace_id": root, "tool_call_id": "portfolio"},
- )
- orphan = await service.contracts.freeze(
- root, ScriptTaskContractV1.from_payload(_contract("paragraph"))
- )
- removed = await service.reclaim_orphan_contracts(root_trace_id=root)
- assert removed == (orphan.uri,)
- assert await service.contracts.verify(
- root,
- referenced["contracts"][0]["contract_ref"],
- referenced["contracts"][0]["contract_digest"],
- )
- assert not await service.contracts.verify(root, orphan.uri, orphan.digest)
- @pytest.mark.asyncio
- async def test_failed_operation_discards_workspace_but_recovery_required_preserves_it(
- tmp_path: Path,
- ) -> None:
- service, _, root = await _service(tmp_path)
- class Lifecycle:
- def __init__(self) -> None:
- self.calls: list[dict[str, str]] = []
- async def discard_attempt(self, **values: str) -> None:
- self.calls.append(values)
- lifecycle = Lifecycle()
- service.workspace_lifecycle = lifecycle
- attempts = {
- "failed-attempt": SimpleNamespace(attempt_id="failed-attempt", task_id="paragraph-task"),
- "recovery-attempt": SimpleNamespace(
- attempt_id="recovery-attempt", task_id="paragraph-task"
- ),
- }
- ledger = SimpleNamespace(
- attempts=attempts,
- operations={
- "failed": SimpleNamespace(
- status=OperationStatus.FAILED,
- error="worker failed before submit",
- attempt_ids=["failed-attempt"],
- ),
- "recovery": SimpleNamespace(
- status=OperationStatus.FAILED,
- error="MISSION_RECOVERY_REQUIRED",
- attempt_ids=["recovery-attempt"],
- ),
- },
- )
- await service._reconcile_terminal_workspaces(root, ledger)
- assert lifecycle.calls == [
- {
- "root_trace_id": root,
- "task_id": "paragraph-task",
- "attempt_id": "failed-attempt",
- "reason": "worker failed before submit",
- }
- ]
- def test_comparison_refs_are_kind_specific_and_limits_cannot_be_raised() -> None:
- candidate_refs = []
- for index, kind in ((1, "paragraph"), (2, "structure")):
- candidate_refs.append(
- {
- "decision_id": f"decision-{index}",
- "artifact_ref": {
- "uri": f"script-build://artifact-versions/{index}",
- "kind": kind,
- "version": str(index),
- "digest": "sha256:" + str(index) * 64,
- },
- "scope_ref": "script-build://scopes/full",
- "expected_task_kind": kind,
- }
- )
- compare = _contract("compare")
- compare["comparison_decision_refs"] = candidate_refs
- assert ScriptTaskContractV1.from_payload(compare).task_kind.value == "compare"
- comparison_ref = {
- "decision_id": "decision-compare",
- "artifact_ref": {
- "uri": "script-build://artifact-versions/3",
- "kind": "comparison",
- "version": "3",
- "digest": "sha256:" + "3" * 64,
- },
- "scope_ref": "script-build://scopes/full",
- "expected_task_kind": "compare",
- }
- compose = _contract("compose")
- compose["comparison_decision_refs"] = [comparison_ref]
- ScriptTaskContractV1.from_payload(compose)
- portfolio = _contract("candidate-portfolio")
- portfolio["comparison_decision_refs"] = [comparison_ref]
- with pytest.raises(TaskContractError, match="comparison_decision_refs"):
- ScriptTaskContractV1.from_payload(portfolio)
- with pytest.raises(TaskContractError, match="TASK_BUDGET_EXCEEDED"):
- PhaseTwoLimits(max_tasks=65)
- @pytest.mark.asyncio
- async def test_planning_freezes_contract_and_creates_only_one_portfolio(tmp_path: Path) -> None:
- service, coordinator, root = await _service(tmp_path)
- result = await service.plan_script_tasks(
- contract_payloads=[_contract("candidate-portfolio", execution_ready=False)],
- parent_task_id=None,
- context={"root_trace_id": root, "tool_call_id": "plan-portfolio"},
- )
- task_id = result["task_ids"][0]
- ledger = await coordinator.task_store.load(root)
- task = ledger.tasks[task_id]
- assert task.current_spec.context_refs[0].endswith("candidate-portfolio")
- assert task.current_spec.context_refs[1].startswith("script-build://task-contracts/sha256/")
- assert task.current_spec.context_refs[2] == "script-build://inputs/11"
- frozen = await service.contract_for_task(root, task)
- assert frozen.contract.scope_ref == "script-build://scopes/full"
- with pytest.raises(TaskContractError, match="one Portfolio"):
- await service.plan_script_tasks(
- contract_payloads=[_contract("candidate-portfolio", execution_ready=False)],
- parent_task_id=None,
- context={"root_trace_id": root, "tool_call_id": "plan-portfolio-2"},
- )
- @pytest.mark.asyncio
- async def test_invalid_contract_batch_creates_zero_tasks(tmp_path: Path) -> None:
- service, coordinator, root = await _service(tmp_path)
- invalid = _contract("paragraph")
- invalid["output_schema"] = "structured-script/v1"
- with pytest.raises(TaskContractError, match="output_schema"):
- await service.plan_script_tasks(
- contract_payloads=[_contract("candidate-portfolio"), invalid],
- parent_task_id=None,
- context={"root_trace_id": root},
- )
- ledger = await coordinator.task_store.load(root)
- assert list(ledger.tasks) == [ledger.root_task_id]
- @pytest.mark.asyncio
- async def test_planning_rejects_unresolved_supersession_before_freeze(tmp_path: Path) -> None:
- service, coordinator, root = await _service(tmp_path)
- portfolio = await service.plan_script_tasks(
- contract_payloads=[_contract("candidate-portfolio", execution_ready=False)],
- parent_task_id=None,
- context={"root_trace_id": root, "tool_call_id": "portfolio"},
- )
- compose = await service.plan_script_tasks(
- contract_payloads=[_contract("compose", execution_ready=False)],
- parent_task_id=portfolio["task_ids"][0],
- context={"root_trace_id": root, "tool_call_id": "compose"},
- )
- replacement = _contract("paragraph")
- replacement["supersedes_decision_ids"] = ["missing-accept"]
- with pytest.raises(TaskContractError, match="import every superseded"):
- await service.plan_script_tasks(
- contract_payloads=[replacement],
- parent_task_id=compose["task_ids"][0],
- context={"root_trace_id": root, "tool_call_id": "invalid-replacement"},
- )
- ledger = await coordinator.task_store.load(root)
- assert set(ledger.tasks) == {
- ledger.root_task_id,
- portfolio["task_ids"][0],
- compose["task_ids"][0],
- }
- @pytest.mark.asyncio
- async def test_open_compose_container_cannot_dispatch(tmp_path: Path) -> None:
- service, coordinator, root = await _service(tmp_path)
- portfolio = await service.plan_script_tasks(
- contract_payloads=[_contract("candidate-portfolio", execution_ready=False)],
- parent_task_id=None,
- context={"root_trace_id": root, "tool_call_id": "p"},
- )
- compose = await service.plan_script_tasks(
- contract_payloads=[_contract("compose", execution_ready=False)],
- parent_task_id=portfolio["task_ids"][0],
- context={"root_trace_id": root, "tool_call_id": "c"},
- )
- with pytest.raises(TaskContractError, match="not closed"):
- await service.dispatch_script_tasks(
- task_ids=compose["task_ids"], context={"root_trace_id": root}
- )
- ledger = await coordinator.task_store.load(root)
- assert not ledger.operations
- @pytest.mark.asyncio
- async def test_compose_placeholder_split_replacement_then_revised_v2_passes(
- tmp_path: Path,
- ) -> None:
- service, coordinator, root = await _service(tmp_path)
- coordinator.set_executor(_PlaceholderThenReplacementExecutor(coordinator))
- portfolio = await service.plan_script_tasks(
- contract_payloads=[_contract("candidate-portfolio", execution_ready=False)],
- parent_task_id=None,
- context={"root_trace_id": root, "tool_call_id": "portfolio"},
- )
- compose = await service.plan_script_tasks(
- contract_payloads=[_contract("compose")],
- parent_task_id=portfolio["task_ids"][0],
- context={"root_trace_id": root, "tool_call_id": "compose-v1"},
- )
- compose_id = compose["task_ids"][0]
- first_cycle = (
- await service.dispatch_script_tasks(
- task_ids=(compose_id,),
- context={"root_trace_id": root, "tool_call_id": "dispatch-compose-v1"},
- )
- )[0]
- assert first_cycle["error"] is None
- ledger = await coordinator.task_store.load(root)
- first_validation = ledger.validations[first_cycle["validation_id"]]
- assert first_validation.verdict is ValidationVerdict.FAILED
- assert first_validation.unverified_claims == ["artifact.paragraphs[0].description"]
- assert first_validation.risks == ["REALIZATION_PLACEHOLDER"]
- split = await service.decide_script_task(
- task_id=compose_id,
- action=DecisionAction.SPLIT.value,
- reason="replace only the unrealized opening scope",
- validation_id=first_validation.validation_id,
- replacement_contract=None,
- child_contracts=(_contract("paragraph"),),
- context={"root_trace_id": root, "tool_call_id": "split-placeholder"},
- )
- replacement_id = split["payload"]["child_task_ids"][0]
- replacement_cycle = (
- await service.dispatch_script_tasks(
- task_ids=(replacement_id,),
- context={"root_trace_id": root, "tool_call_id": "dispatch-replacement"},
- )
- )[0]
- replacement_accept = await service.decide_script_task(
- task_id=replacement_id,
- action=DecisionAction.ACCEPT.value,
- reason="the local replacement passed independent validation",
- validation_id=replacement_cycle["validation_id"],
- replacement_contract=None,
- child_contracts=(),
- context={"root_trace_id": root, "tool_call_id": "accept-replacement"},
- )
- ledger = await coordinator.task_store.load(root)
- assert ledger.tasks[compose_id].status is TaskStatus.NEEDS_REPLAN
- replacement_decision_id = replacement_accept["decision_id"]
- replacement_attempt = ledger.attempts[
- cast(str, ledger.decisions[replacement_decision_id].attempt_id)
- ]
- assert replacement_attempt.submission is not None
- replacement_ref = replacement_attempt.submission.artifact_refs[0]
- frozen_replacement = {
- "decision_id": replacement_decision_id,
- "artifact_ref": {
- "uri": replacement_ref.uri,
- "kind": replacement_ref.kind,
- "version": replacement_ref.version,
- "digest": replacement_ref.digest,
- },
- "scope_ref": "script-build://scopes/full",
- "expected_task_kind": "paragraph",
- }
- compose_v2 = _contract("compose", execution_ready=False)
- compose_v2["candidate_closure_decision_refs"] = [frozen_replacement]
- compose_v2["adopted_decision_ids"] = [replacement_decision_id]
- compose_v2["compose_order"] = [replacement_decision_id]
- await service.decide_script_task(
- task_id=compose_id,
- action=DecisionAction.REVISE.value,
- reason="pin the accepted replacement and retry the complete render",
- validation_id=None,
- replacement_contract=compose_v2,
- child_contracts=(),
- context={"root_trace_id": root, "tool_call_id": "revise-compose-v2"},
- )
- second_cycle = (
- await service.dispatch_script_tasks(
- task_ids=(compose_id,),
- context={"root_trace_id": root, "tool_call_id": "dispatch-compose-v2"},
- )
- )[0]
- compose_accept = await service.decide_script_task(
- task_id=compose_id,
- action=DecisionAction.ACCEPT.value,
- reason="the revised full render passed independent validation",
- validation_id=second_cycle["validation_id"],
- replacement_contract=None,
- child_contracts=(),
- context={"root_trace_id": root, "tool_call_id": "accept-compose-v2"},
- )
- reloaded = await FileSystemTaskStore(str(tmp_path / "ledger")).load(root)
- compose_task = reloaded.tasks[compose_id]
- assert compose_task.status is TaskStatus.COMPLETED
- assert compose_task.current_spec_version == 2
- assert len(compose_task.attempt_ids) == 2
- assert [
- reloaded.validations[validation_id].verdict for validation_id in compose_task.validation_ids
- ] == [ValidationVerdict.FAILED, ValidationVerdict.PASSED]
- assert [reloaded.decisions[item].action for item in compose_task.decision_ids] == [
- DecisionAction.SPLIT,
- DecisionAction.REVISE,
- DecisionAction.ACCEPT,
- ]
- assert compose_accept["status"] == TaskStatus.COMPLETED.value
- assert reloaded.tasks[replacement_id].status is TaskStatus.COMPLETED
- @pytest.mark.asyncio
- @pytest.mark.parametrize(
- "creation_order",
- [
- ("element-set", "paragraph", "structure"),
- ("paragraph", "element-set", "structure"),
- ("structure", "paragraph", "element-set"),
- ],
- )
- async def test_exploration_order_and_completion_order_do_not_choose_compose_order(
- tmp_path: Path,
- creation_order: tuple[str, str, str],
- ) -> None:
- service, coordinator, root = await _service(tmp_path)
- coordinator.set_executor(_PlaceholderThenReplacementExecutor(coordinator))
- portfolio = await service.plan_script_tasks(
- contract_payloads=[_contract("candidate-portfolio", execution_ready=False)],
- parent_task_id=None,
- context={"root_trace_id": root, "tool_call_id": "portfolio"},
- )
- compose = await service.plan_script_tasks(
- contract_payloads=[_contract("compose", execution_ready=False)],
- parent_task_id=portfolio["task_ids"][0],
- context={"root_trace_id": root, "tool_call_id": "compose"},
- )
- compose_id = compose["task_ids"][0]
- task_by_kind: dict[str, str] = {}
- for kind in creation_order:
- planned = await service.plan_script_tasks(
- contract_payloads=[_contract(kind)],
- parent_task_id=compose_id,
- context={"root_trace_id": root, "tool_call_id": f"plan:{kind}"},
- )
- task_by_kind[kind] = planned["task_ids"][0]
- decision_by_kind: dict[str, str] = {}
- ref_by_kind: dict[str, ArtifactRef] = {}
- completion_order = tuple(reversed(creation_order))
- for kind in completion_order:
- task_id = task_by_kind[kind]
- cycle = (
- await service.dispatch_script_tasks(
- task_ids=(task_id,),
- context={"root_trace_id": root, "tool_call_id": f"dispatch:{kind}"},
- )
- )[0]
- accepted = await service.decide_script_task(
- task_id=task_id,
- action=DecisionAction.ACCEPT.value,
- reason=f"the bounded {kind} increment passed validation",
- validation_id=cycle["validation_id"],
- replacement_contract=None,
- child_contracts=(),
- context={"root_trace_id": root, "tool_call_id": f"accept:{kind}"},
- )
- decision_id = accepted["decision_id"]
- decision_by_kind[kind] = decision_id
- ledger = await coordinator.task_store.load(root)
- attempt = ledger.attempts[cast(str, ledger.decisions[decision_id].attempt_id)]
- assert attempt.submission is not None
- ref_by_kind[kind] = attempt.submission.artifact_refs[0]
- ledger = await coordinator.task_store.load(root)
- assert ledger.tasks[compose_id].status is TaskStatus.NEEDS_REPLAN
- adoption_kinds = ("paragraph", "structure", "element-set")
- closure = []
- for kind in adoption_kinds:
- ref = ref_by_kind[kind]
- closure.append(
- {
- "decision_id": decision_by_kind[kind],
- "artifact_ref": {
- "uri": ref.uri,
- "kind": ref.kind,
- "version": ref.version,
- "digest": ref.digest,
- },
- "scope_ref": "script-build://scopes/full",
- "expected_task_kind": kind,
- }
- )
- execution_ready = _contract("compose", execution_ready=False)
- execution_ready["candidate_closure_decision_refs"] = closure
- execution_ready["adopted_decision_ids"] = [decision_by_kind[kind] for kind in adoption_kinds]
- execution_ready["compose_order"] = [decision_by_kind[kind] for kind in adoption_kinds]
- await service.decide_script_task(
- task_id=compose_id,
- action=DecisionAction.REVISE.value,
- reason="formal adoption is based on scope and intent, not timing",
- validation_id=None,
- replacement_contract=execution_ready,
- child_contracts=(),
- context={"root_trace_id": root, "tool_call_id": "close-compose"},
- )
- reloaded = await FileSystemTaskStore(str(tmp_path / "ledger")).load(root)
- frozen = await service.contract_for_task(root, reloaded.tasks[compose_id])
- assert frozen.contract.compose_order == tuple(decision_by_kind[kind] for kind in adoption_kinds)
- assert [
- next(kind for kind, task_id in task_by_kind.items() if task_id == child_id)
- for child_id in reloaded.tasks[compose_id].child_task_ids
- ] == list(creation_order)
- assert completion_order == tuple(reversed(creation_order))
- @pytest.mark.asyncio
- async def test_phase_two_retrieval_counts_toward_task_and_depth_limits(tmp_path: Path) -> None:
- service, _, root = await _service(tmp_path)
- service.limits = PhaseTwoLimits(max_tasks=2)
- portfolio = await service.plan_script_tasks(
- contract_payloads=[_contract("candidate-portfolio", execution_ready=False)],
- parent_task_id=None,
- context={"root_trace_id": root, "tool_call_id": "portfolio"},
- )
- compose = await service.plan_script_tasks(
- contract_payloads=[_contract("compose", execution_ready=False)],
- parent_task_id=portfolio["task_ids"][0],
- context={"root_trace_id": root, "tool_call_id": "compose"},
- )
- with pytest.raises(TaskContractError, match="Task budget"):
- await service.plan_script_tasks(
- contract_payloads=[_contract("decode-retrieval")],
- parent_task_id=compose["task_ids"][0],
- context={"root_trace_id": root, "tool_call_id": "retrieval"},
- )
- service.limits = PhaseTwoLimits(max_depth=1)
- with pytest.raises(TaskContractError, match="depth"):
- await service.plan_script_tasks(
- contract_payloads=[_contract("structure", scope="script-build://scopes/full/local")],
- parent_task_id=compose["task_ids"][0],
- context={"root_trace_id": root, "tool_call_id": "too-deep"},
- )
- @pytest.mark.asyncio
- async def test_task_contract_cannot_be_silently_changed_in_task_spec(tmp_path: Path) -> None:
- service, coordinator, root = await _service(tmp_path)
- result = await service.plan_script_tasks(
- contract_payloads=[_contract("candidate-portfolio", execution_ready=False)],
- parent_task_id=None,
- context={"root_trace_id": root},
- )
- ledger = await coordinator.task_store.load(root)
- task = ledger.tasks[result["task_ids"][0]]
- task.specs[-1] = replace(task.specs[-1], objective="tampered")
- await coordinator.task_store.commit(ledger, expected_revision=ledger.revision)
- with pytest.raises(TaskContractError, match="DIGEST_MISMATCH"):
- await service.contract_for_task(root, task)
|