|
|
@@ -0,0 +1,2039 @@
|
|
|
+from __future__ import annotations
|
|
|
+
|
|
|
+import asyncio
|
|
|
+import json
|
|
|
+from collections.abc import Mapping, Sequence
|
|
|
+from pathlib import Path
|
|
|
+from typing import Any, cast
|
|
|
+
|
|
|
+import httpx
|
|
|
+import pytest
|
|
|
+from agent import AgentRunner, FileSystemArtifactStore, FileSystemTaskStore, FileSystemTraceStore
|
|
|
+from agent.orchestration import ArtifactRef, DecisionAction, TaskStatus, ValidationVerdict
|
|
|
+from sqlalchemy import select
|
|
|
+
|
|
|
+from script_build_host.agents.prompts import (
|
|
|
+ phase_one_prompt_manifest,
|
|
|
+ script_build_prompt_manifest,
|
|
|
+)
|
|
|
+from script_build_host.application.input_snapshot_service import ScriptInputSnapshotService
|
|
|
+from script_build_host.application.mission_service import PHASE_ONE_CAPABILITY_BOUNDARY
|
|
|
+from script_build_host.application.phase_two_planning import (
|
|
|
+ PHASE_TWO_CANDIDATE_PORTFOLIO_READY,
|
|
|
+)
|
|
|
+from script_build_host.composition import HostDependencies, compose_host
|
|
|
+from script_build_host.domain.artifacts import ArtifactKind
|
|
|
+from script_build_host.domain.input_snapshot import ScriptBuildInput
|
|
|
+from script_build_host.domain.records import BuildStatus, Principal
|
|
|
+from script_build_host.infrastructure.canonical_json import canonical_sha256
|
|
|
+from script_build_host.infrastructure.legacy_tables import (
|
|
|
+ script_build_paragraph,
|
|
|
+ script_build_record,
|
|
|
+)
|
|
|
+from script_build_host.infrastructure.tables import (
|
|
|
+ artifact_version_table,
|
|
|
+ input_snapshot_table,
|
|
|
+ mission_binding_table,
|
|
|
+)
|
|
|
+from script_build_host.infrastructure.task_contract_store import FileScriptTaskContractStore
|
|
|
+from script_build_host.repositories.legacy_state import SqlAlchemyLegacyBuildStateRepository
|
|
|
+from script_build_host.repositories.sqlalchemy import (
|
|
|
+ SqlAlchemyInputSnapshotRepository,
|
|
|
+ SqlAlchemyMissionBindingRepository,
|
|
|
+ SqlAlchemyPublicationRepository,
|
|
|
+ SqlAlchemyScriptBusinessArtifactRepository,
|
|
|
+)
|
|
|
+from script_build_host.repositories.workspace import SqlAlchemyCandidateWorkspaceRepository
|
|
|
+from script_build_host.tools.gateway import RetrievalResult
|
|
|
+
|
|
|
+ROOT = "phase-two-real-runner-root"
|
|
|
+FULL_SCOPE = "script-build://scopes/full"
|
|
|
+
|
|
|
+
|
|
|
+def _tool(call_id: str, name: str, arguments: Mapping[str, Any]) -> dict[str, Any]:
|
|
|
+ return {
|
|
|
+ "content": "",
|
|
|
+ "tool_calls": [
|
|
|
+ {
|
|
|
+ "id": call_id,
|
|
|
+ "type": "function",
|
|
|
+ "function": {"name": name, "arguments": json.dumps(arguments)},
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ "finish_reason": "tool_calls",
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+def _role_prompt(messages: Sequence[Mapping[str, Any]]) -> Mapping[str, Any]:
|
|
|
+ for message in reversed(messages):
|
|
|
+ if message.get("role") != "user" or not isinstance(message.get("content"), str):
|
|
|
+ continue
|
|
|
+ try:
|
|
|
+ value = json.loads(cast(str, message["content"]))
|
|
|
+ except ValueError:
|
|
|
+ continue
|
|
|
+ if isinstance(value, dict) and "task_spec" in value:
|
|
|
+ return value
|
|
|
+ raise AssertionError("role trace does not contain the protected Task prompt")
|
|
|
+
|
|
|
+
|
|
|
+def _last_tool_value(messages: Sequence[Mapping[str, Any]]) -> Any:
|
|
|
+ for message in reversed(messages):
|
|
|
+ if message.get("role") != "tool" or not isinstance(message.get("content"), str):
|
|
|
+ continue
|
|
|
+ try:
|
|
|
+ return json.loads(cast(str, message["content"]))
|
|
|
+ except ValueError:
|
|
|
+ return None
|
|
|
+ return None
|
|
|
+
|
|
|
+
|
|
|
+def _artifact_ref_payload(ref: ArtifactRef) -> dict[str, Any]:
|
|
|
+ return {
|
|
|
+ "uri": ref.uri,
|
|
|
+ "kind": ref.kind,
|
|
|
+ "version": ref.version,
|
|
|
+ "digest": ref.digest,
|
|
|
+ "summary": ref.summary,
|
|
|
+ "metadata": ref.metadata,
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+def _decision_ref(
|
|
|
+ decision_id: str,
|
|
|
+ artifact_ref: ArtifactRef,
|
|
|
+ *,
|
|
|
+ scope_ref: str,
|
|
|
+ task_kind: str,
|
|
|
+) -> dict[str, Any]:
|
|
|
+ return {
|
|
|
+ "decision_id": decision_id,
|
|
|
+ "artifact_ref": _artifact_ref_payload(artifact_ref),
|
|
|
+ "scope_ref": scope_ref,
|
|
|
+ "expected_task_kind": task_kind,
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+def _contract(
|
|
|
+ task_kind: str,
|
|
|
+ *,
|
|
|
+ scope_ref: str,
|
|
|
+ write_scope: Sequence[str],
|
|
|
+ objective: str | None = None,
|
|
|
+ input_decision_refs: Sequence[Mapping[str, Any]] = (),
|
|
|
+ base_artifact_ref: Mapping[str, Any] | None = None,
|
|
|
+ closure: Sequence[Mapping[str, Any]] = (),
|
|
|
+ adopted: Sequence[str] = (),
|
|
|
+ held: Sequence[str] = (),
|
|
|
+ compose_order: Sequence[str] = (),
|
|
|
+ comparison_decision_refs: Sequence[Mapping[str, Any]] = (),
|
|
|
+ supersedes_decision_ids: Sequence[str] = (),
|
|
|
+) -> dict[str, Any]:
|
|
|
+ output_schema = {
|
|
|
+ "direction": "script-direction/v1",
|
|
|
+ "decode-retrieval": "evidence-record/v1",
|
|
|
+ "structure": "structure-artifact/v1",
|
|
|
+ "paragraph": "paragraph-artifact/v1",
|
|
|
+ "element-set": "element-set-artifact/v1",
|
|
|
+ "compare": "comparison-artifact/v1",
|
|
|
+ "compose": "structured-script/v1",
|
|
|
+ "candidate-portfolio": "candidate-portfolio/v1",
|
|
|
+ }[task_kind]
|
|
|
+ return {
|
|
|
+ "schema_version": "script-task-contract/v1",
|
|
|
+ "task_kind": task_kind,
|
|
|
+ "scope_ref": scope_ref,
|
|
|
+ "intent_class": (
|
|
|
+ "compose"
|
|
|
+ if task_kind == "compose"
|
|
|
+ else "portfolio"
|
|
|
+ if task_kind == "candidate-portfolio"
|
|
|
+ else "explore"
|
|
|
+ ),
|
|
|
+ "objective": objective or f"produce one concrete and independently verifiable {task_kind}",
|
|
|
+ "input_decision_refs": list(input_decision_refs),
|
|
|
+ "base_artifact_ref": base_artifact_ref,
|
|
|
+ "write_scope": list(write_scope),
|
|
|
+ "gap_ref": None,
|
|
|
+ "output_schema": output_schema,
|
|
|
+ "criteria": [
|
|
|
+ {
|
|
|
+ "criterion_id": f"{task_kind}-closed",
|
|
|
+ "description": "the immutable output is concrete and causally closed",
|
|
|
+ "hard": True,
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ "budget": {
|
|
|
+ "max_attempts": 4,
|
|
|
+ "max_tokens": 32_000,
|
|
|
+ "max_seconds": 900,
|
|
|
+ "max_external_queries": 40,
|
|
|
+ "max_no_improvement": 3,
|
|
|
+ },
|
|
|
+ "supersedes_decision_ids": list(supersedes_decision_ids),
|
|
|
+ "candidate_closure_decision_refs": list(closure),
|
|
|
+ "adopted_decision_ids": list(adopted),
|
|
|
+ "held_or_rejected_decision_ids": list(held),
|
|
|
+ "compose_order": list(compose_order),
|
|
|
+ "comparison_decision_refs": list(comparison_decision_refs),
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+def _task_kind(task: Any) -> str:
|
|
|
+ values = [
|
|
|
+ item.rsplit("/", 1)[-1] for item in task.current_spec.context_refs if "/task-kinds/" in item
|
|
|
+ ]
|
|
|
+ assert len(values) == 1
|
|
|
+ return values[0]
|
|
|
+
|
|
|
+
|
|
|
+def _accepted_ref(ledger: Any, task: Any) -> tuple[str, ArtifactRef]:
|
|
|
+ decision_id = task.decision_ids[-1]
|
|
|
+ decision = ledger.decisions[decision_id]
|
|
|
+ attempt = ledger.attempts[decision.attempt_id]
|
|
|
+ refs = [*attempt.submission.artifact_refs, *attempt.submission.evidence_refs]
|
|
|
+ assert len(refs) == 1
|
|
|
+ return decision_id, refs[0]
|
|
|
+
|
|
|
+
|
|
|
+class _PhaseTwoScriptedLLM:
|
|
|
+ """Exercise phase-two through the actual preset/tool/LocalExecutor surface."""
|
|
|
+
|
|
|
+ def __init__(self, store: FileSystemTaskStore) -> None:
|
|
|
+ self.store = store
|
|
|
+ self.calls = 0
|
|
|
+ self.submit_attempt_arguments: list[dict[str, Any]] = []
|
|
|
+ self.worker_kinds: list[str] = []
|
|
|
+ self.validator_kinds: list[str] = []
|
|
|
+ self.phase_two_active = False
|
|
|
+ self.compose_candidates_saved = 0
|
|
|
+
|
|
|
+ def _call(self, name: str, arguments: Mapping[str, Any]) -> dict[str, Any]:
|
|
|
+ self.calls += 1
|
|
|
+ if name == "submit_attempt":
|
|
|
+ self.submit_attempt_arguments.append(dict(arguments))
|
|
|
+ return _tool(f"phase2-call-{self.calls}", name, arguments)
|
|
|
+
|
|
|
+ async def __call__(self, messages, tools, **_kwargs):
|
|
|
+ self.phase_two_active = self.phase_two_active or any(
|
|
|
+ PHASE_TWO_CANDIDATE_PORTFOLIO_READY in str(item.get("content", "")) for item in messages
|
|
|
+ )
|
|
|
+ names = {item["function"]["name"] for item in tools or []}
|
|
|
+ if "plan_script_tasks" in names:
|
|
|
+ return await self._planner()
|
|
|
+ if "submit_validation" in names:
|
|
|
+ return self._validator(messages)
|
|
|
+ if "submit_attempt" in names:
|
|
|
+ return self._worker(messages, names)
|
|
|
+ raise AssertionError(f"unexpected tool surface: {sorted(names)}")
|
|
|
+
|
|
|
+ async def _planner(self) -> dict[str, Any]:
|
|
|
+ ledger = await self.store.load(ROOT)
|
|
|
+ root = ledger.tasks[ledger.root_task_id]
|
|
|
+ tasks = [task for task in ledger.tasks.values() if task.task_id != ledger.root_task_id]
|
|
|
+
|
|
|
+ def by_kind(kind: str) -> list[Any]:
|
|
|
+ return [task for task in tasks if _task_kind(task) == kind]
|
|
|
+
|
|
|
+ def by_objective(value: str) -> Any | None:
|
|
|
+ return next(
|
|
|
+ (task for task in tasks if task.current_spec.objective == value),
|
|
|
+ None,
|
|
|
+ )
|
|
|
+
|
|
|
+ direction = next(iter(by_kind("direction")), None)
|
|
|
+ if direction is None:
|
|
|
+ return self._call(
|
|
|
+ "plan_script_tasks",
|
|
|
+ {
|
|
|
+ "contracts": [
|
|
|
+ _contract(
|
|
|
+ "direction",
|
|
|
+ scope_ref=FULL_SCOPE,
|
|
|
+ write_scope=("script-build://writes/direction",),
|
|
|
+ )
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ )
|
|
|
+ retrieval = next(
|
|
|
+ (
|
|
|
+ task
|
|
|
+ for task in by_kind("decode-retrieval")
|
|
|
+ if task.parent_task_id == direction.task_id
|
|
|
+ ),
|
|
|
+ None,
|
|
|
+ )
|
|
|
+ if retrieval is None:
|
|
|
+ return self._call(
|
|
|
+ "plan_script_tasks",
|
|
|
+ {
|
|
|
+ "parent_task_id": direction.task_id,
|
|
|
+ "contracts": [
|
|
|
+ _contract(
|
|
|
+ "decode-retrieval",
|
|
|
+ scope_ref="script-build://scopes/full/direction-evidence",
|
|
|
+ write_scope=(),
|
|
|
+ )
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ )
|
|
|
+ if retrieval.status in {TaskStatus.PENDING, TaskStatus.NEEDS_REPLAN}:
|
|
|
+ return self._call("dispatch_script_tasks", {"task_ids": [retrieval.task_id]})
|
|
|
+ if retrieval.status is TaskStatus.AWAITING_DECISION:
|
|
|
+ return self._accept(retrieval, ledger)
|
|
|
+ if direction.status in {TaskStatus.PENDING, TaskStatus.NEEDS_REPLAN}:
|
|
|
+ return self._call("dispatch_script_tasks", {"task_ids": [direction.task_id]})
|
|
|
+ if direction.status is TaskStatus.AWAITING_DECISION:
|
|
|
+ return self._accept(direction, ledger)
|
|
|
+ if not self.phase_two_active:
|
|
|
+ return self._call(
|
|
|
+ "decide_script_task",
|
|
|
+ {
|
|
|
+ "task_id": root.task_id,
|
|
|
+ "action": "block",
|
|
|
+ "reason": PHASE_ONE_CAPABILITY_BOUNDARY,
|
|
|
+ },
|
|
|
+ )
|
|
|
+
|
|
|
+ portfolio = next(iter(by_kind("candidate-portfolio")), None)
|
|
|
+ if portfolio is None:
|
|
|
+ return self._call(
|
|
|
+ "plan_script_tasks",
|
|
|
+ {
|
|
|
+ "contracts": [
|
|
|
+ _contract(
|
|
|
+ "candidate-portfolio",
|
|
|
+ scope_ref=FULL_SCOPE,
|
|
|
+ write_scope=("script-build://writes",),
|
|
|
+ )
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ )
|
|
|
+ compose = next(iter(by_kind("compose")), None)
|
|
|
+ if compose is None:
|
|
|
+ return self._call(
|
|
|
+ "plan_script_tasks",
|
|
|
+ {
|
|
|
+ "parent_task_id": portfolio.task_id,
|
|
|
+ "contracts": [
|
|
|
+ _contract(
|
|
|
+ "compose",
|
|
|
+ scope_ref=FULL_SCOPE,
|
|
|
+ write_scope=("script-build://writes",),
|
|
|
+ )
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ )
|
|
|
+ structure = next(iter(by_kind("structure")), None)
|
|
|
+ if structure is None:
|
|
|
+ return self._call(
|
|
|
+ "plan_script_tasks",
|
|
|
+ {
|
|
|
+ "parent_task_id": compose.task_id,
|
|
|
+ "contracts": [
|
|
|
+ _contract(
|
|
|
+ "structure",
|
|
|
+ scope_ref="script-build://scopes/full/opening",
|
|
|
+ write_scope=("script-build://writes/paragraphs/opening",),
|
|
|
+ )
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ )
|
|
|
+ if structure.status in {TaskStatus.PENDING, TaskStatus.NEEDS_REPLAN}:
|
|
|
+ return self._call("dispatch_script_tasks", {"task_ids": [structure.task_id]})
|
|
|
+ if structure.status is TaskStatus.AWAITING_DECISION:
|
|
|
+ return self._accept(structure, ledger)
|
|
|
+
|
|
|
+ structure_decision, structure_ref = _accepted_ref(ledger, structure)
|
|
|
+ first_paragraph_objective = "draft opening candidate A"
|
|
|
+ replacement_objective = "replace the placeholder opening with retrieved evidence"
|
|
|
+ paragraph = by_objective(first_paragraph_objective)
|
|
|
+ if paragraph is None:
|
|
|
+ structure_pin = _decision_ref(
|
|
|
+ structure_decision,
|
|
|
+ structure_ref,
|
|
|
+ scope_ref="script-build://scopes/full/opening",
|
|
|
+ task_kind="structure",
|
|
|
+ )
|
|
|
+ return self._call(
|
|
|
+ "plan_script_tasks",
|
|
|
+ {
|
|
|
+ "parent_task_id": compose.task_id,
|
|
|
+ "contracts": [
|
|
|
+ _contract(
|
|
|
+ "paragraph",
|
|
|
+ scope_ref="script-build://scopes/full/opening",
|
|
|
+ write_scope=("script-build://writes/paragraphs/opening",),
|
|
|
+ objective=first_paragraph_objective,
|
|
|
+ input_decision_refs=(structure_pin,),
|
|
|
+ base_artifact_ref=_artifact_ref_payload(structure_ref),
|
|
|
+ )
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ )
|
|
|
+ if paragraph.status in {TaskStatus.PENDING, TaskStatus.NEEDS_REPLAN}:
|
|
|
+ return self._call("dispatch_script_tasks", {"task_ids": [paragraph.task_id]})
|
|
|
+ if paragraph.status is TaskStatus.AWAITING_DECISION:
|
|
|
+ return self._accept(paragraph, ledger)
|
|
|
+
|
|
|
+ direction_decision, direction_ref = _accepted_ref(ledger, direction)
|
|
|
+ paragraph_decision, paragraph_ref = _accepted_ref(ledger, paragraph)
|
|
|
+ if compose.current_spec_version == 1:
|
|
|
+ structure_pin = _decision_ref(
|
|
|
+ structure_decision,
|
|
|
+ structure_ref,
|
|
|
+ scope_ref="script-build://scopes/full/opening",
|
|
|
+ task_kind="structure",
|
|
|
+ )
|
|
|
+ paragraph_pin = _decision_ref(
|
|
|
+ paragraph_decision,
|
|
|
+ paragraph_ref,
|
|
|
+ scope_ref="script-build://scopes/full/opening",
|
|
|
+ task_kind="paragraph",
|
|
|
+ )
|
|
|
+ return self._call(
|
|
|
+ "decide_script_task",
|
|
|
+ {
|
|
|
+ "task_id": compose.task_id,
|
|
|
+ "action": "revise",
|
|
|
+ "reason": "pin the adopted paragraph and formal compose order",
|
|
|
+ "replacement_contract": _contract(
|
|
|
+ "compose",
|
|
|
+ scope_ref=FULL_SCOPE,
|
|
|
+ write_scope=("script-build://writes",),
|
|
|
+ input_decision_refs=(
|
|
|
+ _decision_ref(
|
|
|
+ direction_decision,
|
|
|
+ direction_ref,
|
|
|
+ scope_ref=FULL_SCOPE,
|
|
|
+ task_kind="direction",
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ closure=(structure_pin, paragraph_pin),
|
|
|
+ adopted=(structure_decision, paragraph_decision),
|
|
|
+ compose_order=(structure_decision, paragraph_decision),
|
|
|
+ ),
|
|
|
+ },
|
|
|
+ )
|
|
|
+ if compose.status is TaskStatus.PENDING:
|
|
|
+ return self._call("dispatch_script_tasks", {"task_ids": [compose.task_id]})
|
|
|
+ if compose.status is TaskStatus.AWAITING_DECISION:
|
|
|
+ validation = ledger.validations[compose.validation_ids[-1]]
|
|
|
+ if validation.verdict is ValidationVerdict.PASSED:
|
|
|
+ return self._accept(compose, ledger)
|
|
|
+ return self._call(
|
|
|
+ "decide_script_task",
|
|
|
+ {
|
|
|
+ "task_id": compose.task_id,
|
|
|
+ "validation_id": validation.validation_id,
|
|
|
+ "action": "split",
|
|
|
+ "reason": "replace only the unrealized opening after targeted retrieval",
|
|
|
+ "child_contracts": [
|
|
|
+ _contract(
|
|
|
+ "paragraph",
|
|
|
+ scope_ref="script-build://scopes/full/opening",
|
|
|
+ write_scope=("script-build://writes/paragraphs/opening",),
|
|
|
+ objective=replacement_objective,
|
|
|
+ input_decision_refs=(
|
|
|
+ _decision_ref(
|
|
|
+ structure_decision,
|
|
|
+ structure_ref,
|
|
|
+ scope_ref="script-build://scopes/full/opening",
|
|
|
+ task_kind="structure",
|
|
|
+ ),
|
|
|
+ _decision_ref(
|
|
|
+ paragraph_decision,
|
|
|
+ paragraph_ref,
|
|
|
+ scope_ref="script-build://scopes/full/opening",
|
|
|
+ task_kind="paragraph",
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ base_artifact_ref=_artifact_ref_payload(structure_ref),
|
|
|
+ supersedes_decision_ids=(paragraph_decision,),
|
|
|
+ )
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ )
|
|
|
+
|
|
|
+ replacement = by_objective(replacement_objective)
|
|
|
+ if replacement is None:
|
|
|
+ raise AssertionError("failed Compose must SPLIT one bounded replacement Task")
|
|
|
+ phase_two_retrieval = next(
|
|
|
+ (
|
|
|
+ task
|
|
|
+ for task in by_kind("decode-retrieval")
|
|
|
+ if task.parent_task_id == replacement.task_id
|
|
|
+ ),
|
|
|
+ None,
|
|
|
+ )
|
|
|
+ if phase_two_retrieval is None:
|
|
|
+ return self._call(
|
|
|
+ "plan_script_tasks",
|
|
|
+ {
|
|
|
+ "parent_task_id": replacement.task_id,
|
|
|
+ "contracts": [
|
|
|
+ _contract(
|
|
|
+ "decode-retrieval",
|
|
|
+ scope_ref="script-build://scopes/full/opening/evidence",
|
|
|
+ write_scope=(),
|
|
|
+ objective="retrieve evidence for the replacement opening",
|
|
|
+ )
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ )
|
|
|
+ if phase_two_retrieval.status in {TaskStatus.PENDING, TaskStatus.NEEDS_REPLAN}:
|
|
|
+ return self._call("dispatch_script_tasks", {"task_ids": [phase_two_retrieval.task_id]})
|
|
|
+ if phase_two_retrieval.status is TaskStatus.AWAITING_DECISION:
|
|
|
+ return self._accept(phase_two_retrieval, ledger)
|
|
|
+ if replacement.status in {TaskStatus.PENDING, TaskStatus.NEEDS_REPLAN}:
|
|
|
+ return self._call("dispatch_script_tasks", {"task_ids": [replacement.task_id]})
|
|
|
+ if replacement.status is TaskStatus.AWAITING_DECISION:
|
|
|
+ return self._accept(replacement, ledger)
|
|
|
+
|
|
|
+ replacement_decision, replacement_ref = _accepted_ref(ledger, replacement)
|
|
|
+ comparison = next(iter(by_kind("compare")), None)
|
|
|
+ if comparison is None:
|
|
|
+ return self._call(
|
|
|
+ "plan_script_tasks",
|
|
|
+ {
|
|
|
+ "parent_task_id": compose.task_id,
|
|
|
+ "contracts": [
|
|
|
+ _contract(
|
|
|
+ "compare",
|
|
|
+ scope_ref="script-build://scopes/full/opening",
|
|
|
+ write_scope=(),
|
|
|
+ objective="compare opening candidates A and B symmetrically",
|
|
|
+ comparison_decision_refs=(
|
|
|
+ _decision_ref(
|
|
|
+ paragraph_decision,
|
|
|
+ paragraph_ref,
|
|
|
+ scope_ref="script-build://scopes/full/opening",
|
|
|
+ task_kind="paragraph",
|
|
|
+ ),
|
|
|
+ _decision_ref(
|
|
|
+ replacement_decision,
|
|
|
+ replacement_ref,
|
|
|
+ scope_ref="script-build://scopes/full/opening",
|
|
|
+ task_kind="paragraph",
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ )
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ )
|
|
|
+ if comparison.status in {TaskStatus.PENDING, TaskStatus.NEEDS_REPLAN}:
|
|
|
+ return self._call("dispatch_script_tasks", {"task_ids": [comparison.task_id]})
|
|
|
+ if comparison.status is TaskStatus.AWAITING_DECISION:
|
|
|
+ return self._accept(comparison, ledger)
|
|
|
+
|
|
|
+ comparison_decision, comparison_ref = _accepted_ref(ledger, comparison)
|
|
|
+ if compose.status is TaskStatus.NEEDS_REPLAN:
|
|
|
+ structure_pin = _decision_ref(
|
|
|
+ structure_decision,
|
|
|
+ structure_ref,
|
|
|
+ scope_ref="script-build://scopes/full/opening",
|
|
|
+ task_kind="structure",
|
|
|
+ )
|
|
|
+ paragraph_pin = _decision_ref(
|
|
|
+ paragraph_decision,
|
|
|
+ paragraph_ref,
|
|
|
+ scope_ref="script-build://scopes/full/opening",
|
|
|
+ task_kind="paragraph",
|
|
|
+ )
|
|
|
+ replacement_pin = _decision_ref(
|
|
|
+ replacement_decision,
|
|
|
+ replacement_ref,
|
|
|
+ scope_ref="script-build://scopes/full/opening",
|
|
|
+ task_kind="paragraph",
|
|
|
+ )
|
|
|
+ return self._call(
|
|
|
+ "decide_script_task",
|
|
|
+ {
|
|
|
+ "task_id": compose.task_id,
|
|
|
+ "action": "revise",
|
|
|
+ "reason": "pin the accepted replacement and the symmetric comparison",
|
|
|
+ "replacement_contract": _contract(
|
|
|
+ "compose",
|
|
|
+ scope_ref=FULL_SCOPE,
|
|
|
+ write_scope=("script-build://writes",),
|
|
|
+ input_decision_refs=(
|
|
|
+ _decision_ref(
|
|
|
+ direction_decision,
|
|
|
+ direction_ref,
|
|
|
+ scope_ref=FULL_SCOPE,
|
|
|
+ task_kind="direction",
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ closure=(structure_pin, paragraph_pin, replacement_pin),
|
|
|
+ adopted=(structure_decision, replacement_decision),
|
|
|
+ held=(paragraph_decision,),
|
|
|
+ compose_order=(structure_decision, replacement_decision),
|
|
|
+ comparison_decision_refs=(
|
|
|
+ _decision_ref(
|
|
|
+ comparison_decision,
|
|
|
+ comparison_ref,
|
|
|
+ scope_ref="script-build://scopes/full/opening",
|
|
|
+ task_kind="compare",
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ },
|
|
|
+ )
|
|
|
+ if compose.status is TaskStatus.PENDING:
|
|
|
+ return self._call("dispatch_script_tasks", {"task_ids": [compose.task_id]})
|
|
|
+ if compose.status is TaskStatus.AWAITING_DECISION:
|
|
|
+ return self._accept(compose, ledger)
|
|
|
+
|
|
|
+ compose_decision, compose_ref = _accepted_ref(ledger, compose)
|
|
|
+ if portfolio.current_spec_version == 1:
|
|
|
+ compose_pin = _decision_ref(
|
|
|
+ compose_decision,
|
|
|
+ compose_ref,
|
|
|
+ scope_ref=FULL_SCOPE,
|
|
|
+ task_kind="compose",
|
|
|
+ )
|
|
|
+ return self._call(
|
|
|
+ "decide_script_task",
|
|
|
+ {
|
|
|
+ "task_id": portfolio.task_id,
|
|
|
+ "action": "revise",
|
|
|
+ "reason": "pin the uniquely adopted StructuredScript",
|
|
|
+ "replacement_contract": _contract(
|
|
|
+ "candidate-portfolio",
|
|
|
+ scope_ref=FULL_SCOPE,
|
|
|
+ write_scope=("script-build://writes",),
|
|
|
+ closure=(compose_pin,),
|
|
|
+ adopted=(compose_decision,),
|
|
|
+ compose_order=(compose_decision,),
|
|
|
+ ),
|
|
|
+ },
|
|
|
+ )
|
|
|
+ if portfolio.status in {TaskStatus.PENDING, TaskStatus.NEEDS_REPLAN}:
|
|
|
+ return self._call("dispatch_script_tasks", {"task_ids": [portfolio.task_id]})
|
|
|
+ if portfolio.status is TaskStatus.AWAITING_DECISION:
|
|
|
+ return self._accept(portfolio, ledger)
|
|
|
+
|
|
|
+ assert portfolio.status is TaskStatus.COMPLETED
|
|
|
+ return self._call(
|
|
|
+ "decide_script_task",
|
|
|
+ {
|
|
|
+ "task_id": root.task_id,
|
|
|
+ "action": "block",
|
|
|
+ "reason": PHASE_TWO_CANDIDATE_PORTFOLIO_READY,
|
|
|
+ },
|
|
|
+ )
|
|
|
+
|
|
|
+ def _accept(self, task: Any, ledger: Any) -> dict[str, Any]:
|
|
|
+ validation = ledger.validations[task.validation_ids[-1]]
|
|
|
+ assert validation.verdict is ValidationVerdict.PASSED
|
|
|
+ return self._call(
|
|
|
+ "decide_script_task",
|
|
|
+ {
|
|
|
+ "task_id": task.task_id,
|
|
|
+ "validation_id": validation.validation_id,
|
|
|
+ "action": "accept",
|
|
|
+ "reason": "the independent Validator passed the frozen artifact",
|
|
|
+ },
|
|
|
+ )
|
|
|
+
|
|
|
+ def _worker(self, messages, names: set[str]) -> dict[str, Any]:
|
|
|
+ prompt = _role_prompt(messages)
|
|
|
+ spec = cast(Mapping[str, Any], prompt["task_spec"])
|
|
|
+ refs = cast(Sequence[str], spec["context_refs"])
|
|
|
+ kind = next(item.rsplit("/", 1)[-1] for item in refs if "/task-kinds/" in item)
|
|
|
+ if kind not in self.worker_kinds:
|
|
|
+ self.worker_kinds.append(kind)
|
|
|
+ last = _last_tool_value(messages)
|
|
|
+ if kind == "decode-retrieval":
|
|
|
+ if not isinstance(last, dict) or "artifact_ref" not in last:
|
|
|
+ return self._call(
|
|
|
+ "search_script_decode_case",
|
|
|
+ {"return_field": "主脉络", "keyword": "observable reversal", "top_k": 3},
|
|
|
+ )
|
|
|
+ return self._call("submit_attempt", {})
|
|
|
+ if kind == "direction":
|
|
|
+ if not isinstance(last, dict) or "artifact_ref" not in last:
|
|
|
+ accepted = cast(Sequence[Mapping[str, Any]], prompt["accepted_child_results"])
|
|
|
+ evidence_refs = cast(
|
|
|
+ Sequence[Mapping[str, Any]],
|
|
|
+ cast(Mapping[str, Any], accepted[0]["submission"])["evidence_refs"],
|
|
|
+ )
|
|
|
+ return self._call(
|
|
|
+ "save_direction_candidate",
|
|
|
+ {
|
|
|
+ "goals": [
|
|
|
+ {
|
|
|
+ "goal_id": "direction-goal",
|
|
|
+ "statement": "open with one observable reversal",
|
|
|
+ "rationale": "the frozen topic benefits from a concrete hook",
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ "criteria": [
|
|
|
+ {
|
|
|
+ "criterion_id": "grounded-direction",
|
|
|
+ "description": "the direction is concrete and testable",
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ "legacy_markdown": "# Direction\n\nOpen with one observable reversal.",
|
|
|
+ "evidence_refs": [evidence_refs[0]["uri"]],
|
|
|
+ },
|
|
|
+ )
|
|
|
+ return self._call("submit_attempt", {})
|
|
|
+ if kind == "structure":
|
|
|
+ if not isinstance(last, dict):
|
|
|
+ return self._call(
|
|
|
+ "create_script_paragraph",
|
|
|
+ {
|
|
|
+ "paragraph_index": 1,
|
|
|
+ "name": "Opening structure",
|
|
|
+ "content_range": {"scope": "opening", "beats": ["setup", "reversal"]},
|
|
|
+ "theme_elements": [
|
|
|
+ {
|
|
|
+ "原子点": "assumption changes after evidence",
|
|
|
+ "维度": "claim",
|
|
|
+ "维度类型": "主维度",
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ "form_elements": [
|
|
|
+ {
|
|
|
+ "原子点": "setup then concrete reversal",
|
|
|
+ "维度": "shape",
|
|
|
+ "维度类型": "主维度",
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ "function_elements": [
|
|
|
+ {
|
|
|
+ "原子点": "create curiosity without abstraction",
|
|
|
+ "维度": "job",
|
|
|
+ "维度类型": "主维度",
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ "feeling_elements": [{"原子点": "measured surprise", "维度": "tone"}],
|
|
|
+ },
|
|
|
+ )
|
|
|
+ return self._call("submit_attempt", {})
|
|
|
+ if kind == "paragraph":
|
|
|
+ if isinstance(last, dict) and last.get("updated") == 1:
|
|
|
+ return self._call("submit_attempt", {})
|
|
|
+ if not isinstance(last, dict) or "paragraphs" not in last:
|
|
|
+ return self._call("read_attempt_workspace", {})
|
|
|
+ paragraphs = cast(Sequence[Mapping[str, Any]], last["paragraphs"])
|
|
|
+ assert len(paragraphs) == 1
|
|
|
+ replacement = "replace the placeholder" in str(spec["objective"])
|
|
|
+ return self._call(
|
|
|
+ "batch_update_script_paragraphs",
|
|
|
+ {
|
|
|
+ "updates": [
|
|
|
+ {
|
|
|
+ "paragraph_id": paragraphs[0]["paragraph_id"],
|
|
|
+ "theme": (
|
|
|
+ "The retrieved observable reversal changes the opening claim."
|
|
|
+ if replacement
|
|
|
+ else "A familiar expectation changes after a visible detail."
|
|
|
+ ),
|
|
|
+ "form": "A concrete setup followed by one concise reversal.",
|
|
|
+ "function": "Give the audience an immediate reason to continue.",
|
|
|
+ "feeling": "Measured surprise grounded in observation.",
|
|
|
+ "description": "The opening overturns one assumption with evidence.",
|
|
|
+ "full_description": (
|
|
|
+ "Use the accepted retrieval evidence to replace the unrealized "
|
|
|
+ "opening and show exactly how the observed detail changes the "
|
|
|
+ "interpretation."
|
|
|
+ if replacement
|
|
|
+ else "Start from the familiar expectation, name the visible "
|
|
|
+ "detail, and show exactly how that detail changes the "
|
|
|
+ "interpretation."
|
|
|
+ ),
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ )
|
|
|
+ if kind == "compare":
|
|
|
+ if isinstance(last, dict) and "artifact_ref" in last:
|
|
|
+ return self._call("submit_attempt", {})
|
|
|
+ if not isinstance(last, list):
|
|
|
+ return self._call("read_pinned_candidates", {})
|
|
|
+ assert len(last) == 2
|
|
|
+ candidates = [str(item["artifact_ref"]["uri"]) for item in last]
|
|
|
+ return self._call(
|
|
|
+ "save_comparison_candidate",
|
|
|
+ {
|
|
|
+ "criterion_results": [
|
|
|
+ {
|
|
|
+ "criterion_id": "compare-closed",
|
|
|
+ "candidate_results": [
|
|
|
+ {
|
|
|
+ "artifact_ref": candidate,
|
|
|
+ "reason": "reviewed against the same frozen criterion",
|
|
|
+ }
|
|
|
+ for candidate in candidates
|
|
|
+ ],
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ "conflicts": [
|
|
|
+ "candidate B realizes the retrieved evidence while candidate A does not"
|
|
|
+ ],
|
|
|
+ "recommendation": candidates[-1],
|
|
|
+ },
|
|
|
+ )
|
|
|
+ if kind == "compose":
|
|
|
+ if isinstance(last, dict) and "artifact_ref" in last:
|
|
|
+ return self._call("submit_attempt", {})
|
|
|
+ if not isinstance(last, dict) or last.get("schema_version") != "active-frontier/v1":
|
|
|
+ return self._call("read_active_frontier", {})
|
|
|
+ self.compose_candidates_saved += 1
|
|
|
+ return self._call(
|
|
|
+ "save_structured_script_candidate",
|
|
|
+ {
|
|
|
+ "acceptance_notes": [
|
|
|
+ "TODO replace this placeholder opening with retrieved evidence."
|
|
|
+ if self.compose_candidates_saved == 1
|
|
|
+ else "The adopted replacement is concrete and fully rendered."
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ )
|
|
|
+ if kind == "candidate-portfolio":
|
|
|
+ if isinstance(last, dict) and "artifact_ref" in last:
|
|
|
+ return self._call("submit_attempt", {})
|
|
|
+ if not isinstance(last, list):
|
|
|
+ return self._call("read_pinned_candidates", {})
|
|
|
+ assert len(last) == 1
|
|
|
+ return self._call(
|
|
|
+ "save_candidate_portfolio",
|
|
|
+ {
|
|
|
+ "superseded_decision_ids": [],
|
|
|
+ "unresolved_defects": [],
|
|
|
+ },
|
|
|
+ )
|
|
|
+ raise AssertionError(f"unexpected Worker kind {kind}; tools={sorted(names)}")
|
|
|
+
|
|
|
+ def _validator(self, messages) -> dict[str, Any]:
|
|
|
+ prompt = _role_prompt(messages)
|
|
|
+ spec = cast(Mapping[str, Any], prompt["task_spec"])
|
|
|
+ refs = cast(Sequence[str], spec["context_refs"])
|
|
|
+ kind = next(item.rsplit("/", 1)[-1] for item in refs if "/task-kinds/" in item)
|
|
|
+ if kind not in self.validator_kinds:
|
|
|
+ self.validator_kinds.append(kind)
|
|
|
+ last = _last_tool_value(messages)
|
|
|
+ if not isinstance(last, dict) or "rule_results" not in last:
|
|
|
+ return self._call("deterministic_precheck", {})
|
|
|
+ criteria = cast(Sequence[Mapping[str, Any]], spec["acceptance_criteria"])
|
|
|
+ failed_rules = [item for item in last["rule_results"] if item["verdict"] != "passed"]
|
|
|
+ if failed_rules:
|
|
|
+ artifact_snapshot = cast(Mapping[str, Any], prompt["artifact_snapshot"])
|
|
|
+ refs = cast(Sequence[Mapping[str, Any]], artifact_snapshot["artifact_refs"])
|
|
|
+ assert len(refs) == 1
|
|
|
+ return self._call(
|
|
|
+ "submit_validation",
|
|
|
+ {
|
|
|
+ "verdict": "failed",
|
|
|
+ "criterion_results": [
|
|
|
+ {
|
|
|
+ "criterion_id": item["criterion_id"],
|
|
|
+ "verdict": "failed",
|
|
|
+ "reason": "the rendered candidate still contains a placeholder",
|
|
|
+ }
|
|
|
+ for item in criteria
|
|
|
+ ],
|
|
|
+ "defects": [
|
|
|
+ {
|
|
|
+ "defect_code": "REALIZATION_PLACEHOLDER",
|
|
|
+ "criterion_id": criteria[0]["criterion_id"],
|
|
|
+ "scope_ref": "script-build://scopes/full/opening",
|
|
|
+ "observed_excerpt": (
|
|
|
+ "TODO replace this placeholder opening with retrieved evidence."
|
|
|
+ ),
|
|
|
+ "evidence_refs": [dict(refs[0])],
|
|
|
+ "severity": "hard",
|
|
|
+ "invalidated_inputs": [],
|
|
|
+ "recommended_action_class": "split",
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ "recommendation": "split",
|
|
|
+ },
|
|
|
+ )
|
|
|
+ assert all(item["verdict"] == "passed" for item in last["rule_results"])
|
|
|
+ return self._call(
|
|
|
+ "submit_validation",
|
|
|
+ {
|
|
|
+ "verdict": "passed",
|
|
|
+ "criterion_results": [
|
|
|
+ {
|
|
|
+ "criterion_id": item["criterion_id"],
|
|
|
+ "verdict": "passed",
|
|
|
+ "reason": "the immutable artifact passed deterministic and semantic review",
|
|
|
+ }
|
|
|
+ for item in criteria
|
|
|
+ ],
|
|
|
+ "defects": [],
|
|
|
+ "recommendation": "accept",
|
|
|
+ },
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+class _OrderedExplorationScriptedLLM(_PhaseTwoScriptedLLM):
|
|
|
+ """Close Phase2 after exercising each creative preset in a chosen order."""
|
|
|
+
|
|
|
+ def __init__(self, store: FileSystemTaskStore, order: tuple[str, str, str]) -> None:
|
|
|
+ super().__init__(store)
|
|
|
+ self.order = order
|
|
|
+ # This scenario checks ordering, not placeholder repair. The parent Worker
|
|
|
+ # increments this before saving, so its first Compose is fully realized.
|
|
|
+ self.compose_candidates_saved = 1
|
|
|
+
|
|
|
+ async def _planner(self) -> dict[str, Any]:
|
|
|
+ ledger = await self.store.load(ROOT)
|
|
|
+ tasks = [task for task in ledger.tasks.values() if task.task_id != ledger.root_task_id]
|
|
|
+ direction = next((task for task in tasks if _task_kind(task) == "direction"), None)
|
|
|
+ if (
|
|
|
+ direction is None
|
|
|
+ or direction.status is not TaskStatus.COMPLETED
|
|
|
+ or not self.phase_two_active
|
|
|
+ ):
|
|
|
+ return await super()._planner()
|
|
|
+
|
|
|
+ root = ledger.tasks[ledger.root_task_id]
|
|
|
+
|
|
|
+ def by_objective(value: str) -> Any | None:
|
|
|
+ return next((task for task in tasks if task.current_spec.objective == value), None)
|
|
|
+
|
|
|
+ portfolio = next(
|
|
|
+ (task for task in tasks if _task_kind(task) == "candidate-portfolio"), None
|
|
|
+ )
|
|
|
+ if portfolio is None:
|
|
|
+ return self._call(
|
|
|
+ "plan_script_tasks",
|
|
|
+ {
|
|
|
+ "contracts": [
|
|
|
+ _contract(
|
|
|
+ "candidate-portfolio",
|
|
|
+ scope_ref=FULL_SCOPE,
|
|
|
+ write_scope=("script-build://writes",),
|
|
|
+ )
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ )
|
|
|
+ compose = next((task for task in tasks if _task_kind(task) == "compose"), None)
|
|
|
+ if compose is None:
|
|
|
+ return self._call(
|
|
|
+ "plan_script_tasks",
|
|
|
+ {
|
|
|
+ "parent_task_id": portfolio.task_id,
|
|
|
+ "contracts": [
|
|
|
+ _contract(
|
|
|
+ "compose",
|
|
|
+ scope_ref=FULL_SCOPE,
|
|
|
+ write_scope=("script-build://writes",),
|
|
|
+ )
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ )
|
|
|
+
|
|
|
+ exploratory: dict[str, Any] = {}
|
|
|
+ scopes = {
|
|
|
+ "structure": "script-build://scopes/full/opening/structure-sketch",
|
|
|
+ "paragraph": "script-build://scopes/full/opening",
|
|
|
+ "element-set": "script-build://scopes/full/opening/elements",
|
|
|
+ }
|
|
|
+ write_scopes = {
|
|
|
+ "structure": ("script-build://writes/paragraphs/opening",),
|
|
|
+ "paragraph": ("script-build://writes/paragraphs/opening",),
|
|
|
+ "element-set": ("script-build://writes/elements",),
|
|
|
+ }
|
|
|
+ for kind in self.order:
|
|
|
+ objective = f"explore {kind} before formal adoption"
|
|
|
+ task = by_objective(objective)
|
|
|
+ if task is None:
|
|
|
+ return self._call(
|
|
|
+ "plan_script_tasks",
|
|
|
+ {
|
|
|
+ "parent_task_id": compose.task_id,
|
|
|
+ "contracts": [
|
|
|
+ _contract(
|
|
|
+ kind,
|
|
|
+ scope_ref=scopes[kind],
|
|
|
+ write_scope=write_scopes[kind],
|
|
|
+ objective=objective,
|
|
|
+ )
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ )
|
|
|
+ exploratory[kind] = task
|
|
|
+ if task.status in {TaskStatus.PENDING, TaskStatus.NEEDS_REPLAN}:
|
|
|
+ return self._call("dispatch_script_tasks", {"task_ids": [task.task_id]})
|
|
|
+ if task.status is TaskStatus.AWAITING_DECISION:
|
|
|
+ return self._accept(task, ledger)
|
|
|
+
|
|
|
+ structure_decision, structure_ref = _accepted_ref(ledger, exploratory["structure"])
|
|
|
+ paragraph_decision, paragraph_ref = _accepted_ref(ledger, exploratory["paragraph"])
|
|
|
+ element_decision, element_ref = _accepted_ref(ledger, exploratory["element-set"])
|
|
|
+
|
|
|
+ paragraph_objective = "adopt structure and paragraph as one logical opening"
|
|
|
+ paragraph_replacement = by_objective(paragraph_objective)
|
|
|
+ if paragraph_replacement is None:
|
|
|
+ return self._call(
|
|
|
+ "plan_script_tasks",
|
|
|
+ {
|
|
|
+ "parent_task_id": compose.task_id,
|
|
|
+ "contracts": [
|
|
|
+ _contract(
|
|
|
+ "paragraph",
|
|
|
+ scope_ref="script-build://scopes/full/opening",
|
|
|
+ write_scope=("script-build://writes/paragraphs/opening",),
|
|
|
+ objective=paragraph_objective,
|
|
|
+ input_decision_refs=(
|
|
|
+ _decision_ref(
|
|
|
+ structure_decision,
|
|
|
+ structure_ref,
|
|
|
+ scope_ref=scopes["structure"],
|
|
|
+ task_kind="structure",
|
|
|
+ ),
|
|
|
+ _decision_ref(
|
|
|
+ paragraph_decision,
|
|
|
+ paragraph_ref,
|
|
|
+ scope_ref="script-build://scopes/full/opening",
|
|
|
+ task_kind="paragraph",
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ base_artifact_ref=_artifact_ref_payload(structure_ref),
|
|
|
+ supersedes_decision_ids=(paragraph_decision,),
|
|
|
+ )
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ )
|
|
|
+ if paragraph_replacement.status in {TaskStatus.PENDING, TaskStatus.NEEDS_REPLAN}:
|
|
|
+ return self._call(
|
|
|
+ "dispatch_script_tasks", {"task_ids": [paragraph_replacement.task_id]}
|
|
|
+ )
|
|
|
+ if paragraph_replacement.status is TaskStatus.AWAITING_DECISION:
|
|
|
+ return self._accept(paragraph_replacement, ledger)
|
|
|
+ adopted_paragraph_decision, adopted_paragraph_ref = _accepted_ref(
|
|
|
+ ledger, paragraph_replacement
|
|
|
+ )
|
|
|
+
|
|
|
+ element_objective = "place the explored element on the adopted opening"
|
|
|
+ element_replacement = by_objective(element_objective)
|
|
|
+ if element_replacement is None:
|
|
|
+ return self._call(
|
|
|
+ "plan_script_tasks",
|
|
|
+ {
|
|
|
+ "parent_task_id": compose.task_id,
|
|
|
+ "contracts": [
|
|
|
+ _contract(
|
|
|
+ "element-set",
|
|
|
+ scope_ref=scopes["element-set"],
|
|
|
+ write_scope=("script-build://writes/elements",),
|
|
|
+ objective=element_objective,
|
|
|
+ input_decision_refs=(
|
|
|
+ _decision_ref(
|
|
|
+ element_decision,
|
|
|
+ element_ref,
|
|
|
+ scope_ref=scopes["element-set"],
|
|
|
+ task_kind="element-set",
|
|
|
+ ),
|
|
|
+ _decision_ref(
|
|
|
+ adopted_paragraph_decision,
|
|
|
+ adopted_paragraph_ref,
|
|
|
+ scope_ref="script-build://scopes/full/opening",
|
|
|
+ task_kind="paragraph",
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ base_artifact_ref=_artifact_ref_payload(adopted_paragraph_ref),
|
|
|
+ supersedes_decision_ids=(element_decision,),
|
|
|
+ )
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ )
|
|
|
+ if element_replacement.status in {TaskStatus.PENDING, TaskStatus.NEEDS_REPLAN}:
|
|
|
+ return self._call("dispatch_script_tasks", {"task_ids": [element_replacement.task_id]})
|
|
|
+ if element_replacement.status is TaskStatus.AWAITING_DECISION:
|
|
|
+ return self._accept(element_replacement, ledger)
|
|
|
+ adopted_element_decision, adopted_element_ref = _accepted_ref(ledger, element_replacement)
|
|
|
+
|
|
|
+ direction_decision, direction_ref = _accepted_ref(ledger, direction)
|
|
|
+ pins = (
|
|
|
+ _decision_ref(
|
|
|
+ structure_decision,
|
|
|
+ structure_ref,
|
|
|
+ scope_ref=scopes["structure"],
|
|
|
+ task_kind="structure",
|
|
|
+ ),
|
|
|
+ _decision_ref(
|
|
|
+ paragraph_decision,
|
|
|
+ paragraph_ref,
|
|
|
+ scope_ref="script-build://scopes/full/opening",
|
|
|
+ task_kind="paragraph",
|
|
|
+ ),
|
|
|
+ _decision_ref(
|
|
|
+ element_decision,
|
|
|
+ element_ref,
|
|
|
+ scope_ref=scopes["element-set"],
|
|
|
+ task_kind="element-set",
|
|
|
+ ),
|
|
|
+ _decision_ref(
|
|
|
+ adopted_paragraph_decision,
|
|
|
+ adopted_paragraph_ref,
|
|
|
+ scope_ref="script-build://scopes/full/opening",
|
|
|
+ task_kind="paragraph",
|
|
|
+ ),
|
|
|
+ _decision_ref(
|
|
|
+ adopted_element_decision,
|
|
|
+ adopted_element_ref,
|
|
|
+ scope_ref=scopes["element-set"],
|
|
|
+ task_kind="element-set",
|
|
|
+ ),
|
|
|
+ )
|
|
|
+ if compose.current_spec_version == 1:
|
|
|
+ return self._call(
|
|
|
+ "decide_script_task",
|
|
|
+ {
|
|
|
+ "task_id": compose.task_id,
|
|
|
+ "action": "revise",
|
|
|
+ "reason": "formal adoption follows immutable lineage, not exploration order",
|
|
|
+ "replacement_contract": _contract(
|
|
|
+ "compose",
|
|
|
+ scope_ref=FULL_SCOPE,
|
|
|
+ write_scope=("script-build://writes",),
|
|
|
+ input_decision_refs=(
|
|
|
+ _decision_ref(
|
|
|
+ direction_decision,
|
|
|
+ direction_ref,
|
|
|
+ scope_ref=FULL_SCOPE,
|
|
|
+ task_kind="direction",
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ closure=pins,
|
|
|
+ adopted=(
|
|
|
+ structure_decision,
|
|
|
+ adopted_paragraph_decision,
|
|
|
+ adopted_element_decision,
|
|
|
+ ),
|
|
|
+ held=(paragraph_decision, element_decision),
|
|
|
+ compose_order=(
|
|
|
+ structure_decision,
|
|
|
+ adopted_paragraph_decision,
|
|
|
+ adopted_element_decision,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ },
|
|
|
+ )
|
|
|
+ if compose.status in {TaskStatus.PENDING, TaskStatus.NEEDS_REPLAN}:
|
|
|
+ return self._call("dispatch_script_tasks", {"task_ids": [compose.task_id]})
|
|
|
+ if compose.status is TaskStatus.AWAITING_DECISION:
|
|
|
+ return self._accept(compose, ledger)
|
|
|
+
|
|
|
+ compose_decision, compose_ref = _accepted_ref(ledger, compose)
|
|
|
+ if portfolio.current_spec_version == 1:
|
|
|
+ return self._call(
|
|
|
+ "decide_script_task",
|
|
|
+ {
|
|
|
+ "task_id": portfolio.task_id,
|
|
|
+ "action": "revise",
|
|
|
+ "reason": "pin the unique accepted StructuredScript",
|
|
|
+ "replacement_contract": _contract(
|
|
|
+ "candidate-portfolio",
|
|
|
+ scope_ref=FULL_SCOPE,
|
|
|
+ write_scope=("script-build://writes",),
|
|
|
+ closure=(
|
|
|
+ _decision_ref(
|
|
|
+ compose_decision,
|
|
|
+ compose_ref,
|
|
|
+ scope_ref=FULL_SCOPE,
|
|
|
+ task_kind="compose",
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ adopted=(compose_decision,),
|
|
|
+ compose_order=(compose_decision,),
|
|
|
+ ),
|
|
|
+ },
|
|
|
+ )
|
|
|
+ if portfolio.status in {TaskStatus.PENDING, TaskStatus.NEEDS_REPLAN}:
|
|
|
+ return self._call("dispatch_script_tasks", {"task_ids": [portfolio.task_id]})
|
|
|
+ if portfolio.status is TaskStatus.AWAITING_DECISION:
|
|
|
+ return self._accept(portfolio, ledger)
|
|
|
+ return self._call(
|
|
|
+ "decide_script_task",
|
|
|
+ {
|
|
|
+ "task_id": root.task_id,
|
|
|
+ "action": "block",
|
|
|
+ "reason": PHASE_TWO_CANDIDATE_PORTFOLIO_READY,
|
|
|
+ },
|
|
|
+ )
|
|
|
+
|
|
|
+ def _worker(self, messages, names: set[str]) -> dict[str, Any]:
|
|
|
+ prompt = _role_prompt(messages)
|
|
|
+ spec = cast(Mapping[str, Any], prompt["task_spec"])
|
|
|
+ refs = cast(Sequence[str], spec["context_refs"])
|
|
|
+ kind = next(item.rsplit("/", 1)[-1] for item in refs if "/task-kinds/" in item)
|
|
|
+ last = _last_tool_value(messages)
|
|
|
+ if kind == "paragraph":
|
|
|
+ assert "create_script_paragraph" in names
|
|
|
+ assert "create_script_element" not in names
|
|
|
+ if kind not in self.worker_kinds:
|
|
|
+ self.worker_kinds.append(kind)
|
|
|
+ if isinstance(last, dict) and last.get("updated") == 1:
|
|
|
+ return self._call("submit_attempt", {})
|
|
|
+ if not isinstance(last, dict) or "paragraphs" not in last:
|
|
|
+ return self._call("read_attempt_workspace", {})
|
|
|
+ paragraphs = cast(Sequence[Mapping[str, Any]], last["paragraphs"])
|
|
|
+ if not paragraphs:
|
|
|
+ return self._call(
|
|
|
+ "create_script_paragraph",
|
|
|
+ {
|
|
|
+ "paragraph_index": 1,
|
|
|
+ "name": "Exploratory opening",
|
|
|
+ "content_range": {"scope": "opening", "beats": ["setup", "reversal"]},
|
|
|
+ },
|
|
|
+ )
|
|
|
+ return self._call(
|
|
|
+ "batch_update_script_paragraphs",
|
|
|
+ {
|
|
|
+ "updates": [
|
|
|
+ {
|
|
|
+ "paragraph_id": paragraphs[0]["paragraph_id"],
|
|
|
+ "theme": "One visible detail overturns the familiar assumption.",
|
|
|
+ "form": "Concrete setup followed by a concise reversal.",
|
|
|
+ "function": "Give the audience a grounded reason to continue.",
|
|
|
+ "feeling": "Measured surprise.",
|
|
|
+ "description": "A complete opening increment.",
|
|
|
+ "full_description": (
|
|
|
+ "The opening names the visible detail and explains exactly how "
|
|
|
+ "it changes the audience's initial interpretation."
|
|
|
+ ),
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ )
|
|
|
+ if kind == "element-set":
|
|
|
+ assert "create_script_element" in names
|
|
|
+ assert "batch_link_paragraph_elements" in names
|
|
|
+ assert "create_script_paragraph" not in names
|
|
|
+ if kind not in self.worker_kinds:
|
|
|
+ self.worker_kinds.append(kind)
|
|
|
+ if isinstance(last, dict) and "artifact_ref" in last:
|
|
|
+ return self._call("submit_attempt", {})
|
|
|
+ if isinstance(last, dict) and last.get("linked") == 1:
|
|
|
+ return self._call("submit_attempt", {})
|
|
|
+ if not isinstance(last, dict):
|
|
|
+ return self._call(
|
|
|
+ "create_script_element",
|
|
|
+ {
|
|
|
+ "name": "Observable reversal",
|
|
|
+ "dimension_primary": "实质",
|
|
|
+ "dimension_secondary": "opening evidence",
|
|
|
+ "commonality_analysis": {"summary": "visible detail changes belief"},
|
|
|
+ "topic_support": {"strength": "high"},
|
|
|
+ "weight_score": {"value": 0.9},
|
|
|
+ "support_elements": [{"ref": "decode-index://fixture/opening-reversal"}],
|
|
|
+ },
|
|
|
+ )
|
|
|
+ if "element_id" in last:
|
|
|
+ return self._call("read_attempt_workspace", {})
|
|
|
+ if "paragraphs" in last and "elements" in last:
|
|
|
+ paragraphs = cast(Sequence[Mapping[str, Any]], last["paragraphs"])
|
|
|
+ elements = cast(Sequence[Mapping[str, Any]], last["elements"])
|
|
|
+ assert len(elements) == 1
|
|
|
+ if not paragraphs:
|
|
|
+ return self._call("submit_attempt", {})
|
|
|
+ return self._call(
|
|
|
+ "batch_link_paragraph_elements",
|
|
|
+ {
|
|
|
+ "links": [
|
|
|
+ {
|
|
|
+ "paragraph_id": paragraphs[0]["paragraph_id"],
|
|
|
+ "element_ids": [elements[0]["element_id"]],
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ )
|
|
|
+ if kind == "structure":
|
|
|
+ assert "create_script_paragraph" in names
|
|
|
+ assert "create_script_element" not in names
|
|
|
+ return super()._worker(messages, names)
|
|
|
+
|
|
|
+
|
|
|
+class _PrincipalProvider:
|
|
|
+ async def current(self, request_context: Any = None) -> Principal:
|
|
|
+ del request_context
|
|
|
+ return Principal("phase-two-owner")
|
|
|
+
|
|
|
+
|
|
|
+class _Authorizer:
|
|
|
+ async def require_source_access(self, _principal: Principal, **_source: Any) -> None:
|
|
|
+ return None
|
|
|
+
|
|
|
+ async def require_access(self, _principal: Principal, _script_build_id: int) -> None:
|
|
|
+ return None
|
|
|
+
|
|
|
+
|
|
|
+class _DecodeAdapter:
|
|
|
+ async def retrieve(self, *, query: Mapping[str, Any], snapshot: Any) -> RetrievalResult:
|
|
|
+ del snapshot
|
|
|
+ return RetrievalResult(
|
|
|
+ source_refs=("decode-index://fixture/opening-reversal",),
|
|
|
+ summary=f"A grounded opening reverses one expectation: {query['keyword']}",
|
|
|
+ supports=("direction",),
|
|
|
+ confidence="high",
|
|
|
+ metadata={"rank": 1, "score": 0.93},
|
|
|
+ )
|
|
|
+
|
|
|
+ async def require_access(self, _principal: Principal, _script_build_id: int) -> None:
|
|
|
+ return None
|
|
|
+
|
|
|
+
|
|
|
+class _PromptCatalog:
|
|
|
+ def __init__(self) -> None:
|
|
|
+ self._entries = {
|
|
|
+ (str(item["preset"]), str(item["role"])): dict(item)
|
|
|
+ for item in script_build_prompt_manifest()
|
|
|
+ }
|
|
|
+
|
|
|
+ async def load(self, requests: Sequence[Any]) -> tuple[dict[str, Any], ...]:
|
|
|
+ return tuple(dict(self._entries[(str(item.preset), str(item.role))]) for item in requests)
|
|
|
+
|
|
|
+
|
|
|
+class _BlockedAfterPolicyLLM(_PhaseTwoScriptedLLM):
|
|
|
+ """Hold the first Phase2 model call after policy persistence and UNBLOCK."""
|
|
|
+
|
|
|
+ def __init__(self, store: FileSystemTaskStore) -> None:
|
|
|
+ super().__init__(store)
|
|
|
+ self.phase_two_model_call = asyncio.Event()
|
|
|
+ self._never_release = asyncio.Event()
|
|
|
+
|
|
|
+ async def __call__(self, messages, tools, **kwargs):
|
|
|
+ del kwargs
|
|
|
+ if any("script-build-phase-two/v1" in str(item.get("content", "")) for item in messages):
|
|
|
+ self.phase_two_model_call.set()
|
|
|
+ await self._never_release.wait()
|
|
|
+ return await super().__call__(messages, tools)
|
|
|
+
|
|
|
+
|
|
|
+def _persisted_host(
|
|
|
+ *,
|
|
|
+ sessions: Any,
|
|
|
+ tmp_path: Path,
|
|
|
+ llm: Any,
|
|
|
+ enabled_phase: int,
|
|
|
+ model_manifest: Mapping[str, object],
|
|
|
+) -> Any:
|
|
|
+ snapshots = SqlAlchemyInputSnapshotRepository(sessions)
|
|
|
+ input_service = ScriptInputSnapshotService(
|
|
|
+ legacy_input=cast(Any, None),
|
|
|
+ persona_source=cast(Any, None),
|
|
|
+ strategy_source=cast(Any, None),
|
|
|
+ prompt_source=_PromptCatalog(),
|
|
|
+ snapshots=snapshots,
|
|
|
+ )
|
|
|
+ artifacts = SqlAlchemyScriptBusinessArtifactRepository(sessions)
|
|
|
+ task_store = FileSystemTaskStore(str(tmp_path / "ledger"))
|
|
|
+ runner = AgentRunner(
|
|
|
+ trace_store=FileSystemTraceStore(str(tmp_path / "trace")),
|
|
|
+ llm_call=llm,
|
|
|
+ )
|
|
|
+ return compose_host(
|
|
|
+ HostDependencies(
|
|
|
+ runner=runner,
|
|
|
+ task_store=task_store,
|
|
|
+ framework_artifact_store=FileSystemArtifactStore(str(tmp_path / "framework-artifacts")),
|
|
|
+ input_snapshot_service=input_service,
|
|
|
+ input_snapshots=snapshots,
|
|
|
+ bindings=SqlAlchemyMissionBindingRepository(sessions),
|
|
|
+ business_artifacts=artifacts,
|
|
|
+ publications=SqlAlchemyPublicationRepository(sessions),
|
|
|
+ legacy_state=SqlAlchemyLegacyBuildStateRepository(sessions),
|
|
|
+ principal_provider=_PrincipalProvider(),
|
|
|
+ build_authorizer=_Authorizer(),
|
|
|
+ retrieval_adapters={"decode": _DecodeAdapter()},
|
|
|
+ enabled_phase=enabled_phase,
|
|
|
+ agent_data_root=tmp_path / "agent-data",
|
|
|
+ candidate_workspaces=SqlAlchemyCandidateWorkspaceRepository(sessions, artifacts),
|
|
|
+ default_model_manifest=model_manifest,
|
|
|
+ )
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.asyncio
|
|
|
+@pytest.mark.parametrize(
|
|
|
+ "exploration_order",
|
|
|
+ [
|
|
|
+ pytest.param(("element-set", "paragraph", "structure"), id="element-first"),
|
|
|
+ pytest.param(("paragraph", "element-set", "structure"), id="paragraph-first"),
|
|
|
+ pytest.param(("structure", "paragraph", "element-set"), id="structure-first"),
|
|
|
+ ],
|
|
|
+)
|
|
|
+async def test_real_runner_creative_exploration_order_does_not_choose_adoption_order(
|
|
|
+ database: Any,
|
|
|
+ tmp_path: Path,
|
|
|
+ exploration_order: tuple[str, str, str],
|
|
|
+) -> None:
|
|
|
+ _, sessions = database
|
|
|
+ build_states = SqlAlchemyLegacyBuildStateRepository(sessions)
|
|
|
+ script_build_id = await build_states.create(
|
|
|
+ execution_id=711,
|
|
|
+ topic_build_id=712,
|
|
|
+ topic_id=713,
|
|
|
+ agent_type="script_planner",
|
|
|
+ agent_config={},
|
|
|
+ data_source_url=None,
|
|
|
+ strategies_config={},
|
|
|
+ )
|
|
|
+ prompt_manifest = script_build_prompt_manifest()
|
|
|
+ model_manifest = {
|
|
|
+ "presets": {
|
|
|
+ str(item["preset"]): {
|
|
|
+ "model": "scripted-fake-model",
|
|
|
+ "temperature": 0,
|
|
|
+ "max_iterations": 80,
|
|
|
+ }
|
|
|
+ for item in prompt_manifest
|
|
|
+ }
|
|
|
+ }
|
|
|
+ snapshot_repository = SqlAlchemyInputSnapshotRepository(sessions)
|
|
|
+ snapshot = await snapshot_repository.freeze(
|
|
|
+ ScriptBuildInput(
|
|
|
+ script_build_id=script_build_id,
|
|
|
+ execution_id=711,
|
|
|
+ topic_build_id=712,
|
|
|
+ topic_id=713,
|
|
|
+ topic={"topic": {"id": 713, "result": "Order must not determine adoption"}},
|
|
|
+ account={"account_name": "fixture-account"},
|
|
|
+ prompt_manifest=tuple(dict(item) for item in prompt_manifest),
|
|
|
+ model_manifest=model_manifest,
|
|
|
+ )
|
|
|
+ )
|
|
|
+ bindings = SqlAlchemyMissionBindingRepository(sessions)
|
|
|
+ await bindings.create(
|
|
|
+ script_build_id=script_build_id,
|
|
|
+ root_trace_id=ROOT,
|
|
|
+ input_snapshot_id=int(snapshot.snapshot_id),
|
|
|
+ engine_version="phase-two-ordered-real-runner-test",
|
|
|
+ schema_version="v2",
|
|
|
+ )
|
|
|
+ task_store = FileSystemTaskStore(str(tmp_path / "ledger"))
|
|
|
+ trace_store = FileSystemTraceStore(str(tmp_path / "trace"))
|
|
|
+ llm = _OrderedExplorationScriptedLLM(task_store, exploration_order)
|
|
|
+ runner = AgentRunner(trace_store=trace_store, llm_call=llm)
|
|
|
+ business_artifacts = SqlAlchemyScriptBusinessArtifactRepository(sessions)
|
|
|
+ composition = compose_host(
|
|
|
+ HostDependencies(
|
|
|
+ runner=runner,
|
|
|
+ task_store=task_store,
|
|
|
+ framework_artifact_store=FileSystemArtifactStore(str(tmp_path / "framework-artifacts")),
|
|
|
+ input_snapshot_service=cast(Any, snapshot_repository),
|
|
|
+ input_snapshots=snapshot_repository,
|
|
|
+ bindings=bindings,
|
|
|
+ business_artifacts=business_artifacts,
|
|
|
+ publications=SqlAlchemyPublicationRepository(sessions),
|
|
|
+ legacy_state=build_states,
|
|
|
+ principal_provider=_PrincipalProvider(),
|
|
|
+ build_authorizer=_Authorizer(),
|
|
|
+ retrieval_adapters={"decode": _DecodeAdapter()},
|
|
|
+ enabled_phase=2,
|
|
|
+ agent_data_root=tmp_path / "agent-data",
|
|
|
+ candidate_workspaces=SqlAlchemyCandidateWorkspaceRepository(
|
|
|
+ sessions, business_artifacts
|
|
|
+ ),
|
|
|
+ default_model_manifest=model_manifest,
|
|
|
+ )
|
|
|
+ )
|
|
|
+
|
|
|
+ await build_states.set_status(script_build_id, BuildStatus.RUNNING)
|
|
|
+ await composition.mission_service.run(script_build_id)
|
|
|
+
|
|
|
+ ledger = await task_store.load(ROOT)
|
|
|
+ root = ledger.tasks[ledger.root_task_id]
|
|
|
+ assert root.status is TaskStatus.BLOCKED
|
|
|
+ assert root.blocked_reason == PHASE_TWO_CANDIDATE_PORTFOLIO_READY
|
|
|
+ assert await build_states.get_status(script_build_id) is BuildStatus.PARTIAL
|
|
|
+ assert tuple(llm.worker_kinds[2:5]) == exploration_order
|
|
|
+ assert tuple(llm.validator_kinds[2:5]) == exploration_order
|
|
|
+
|
|
|
+ task_by_objective = {
|
|
|
+ task.current_spec.objective: task
|
|
|
+ for task in ledger.tasks.values()
|
|
|
+ if task.task_id != ledger.root_task_id
|
|
|
+ }
|
|
|
+ expected_presets = {
|
|
|
+ "structure": "script_structure_worker",
|
|
|
+ "paragraph": "script_paragraph_worker",
|
|
|
+ "element-set": "script_element_set_worker",
|
|
|
+ }
|
|
|
+ reopened_traces = FileSystemTraceStore(str(tmp_path / "trace"))
|
|
|
+ for kind in exploration_order:
|
|
|
+ task = task_by_objective[f"explore {kind} before formal adoption"]
|
|
|
+ attempt = ledger.attempts[task.attempt_ids[-1]]
|
|
|
+ assert attempt.worker_preset == expected_presets[kind]
|
|
|
+ worker_trace = await reopened_traces.get_trace(attempt.worker_trace_id)
|
|
|
+ assert worker_trace is not None and worker_trace.status == "completed"
|
|
|
+ assert await reopened_traces.get_trace_messages(attempt.worker_trace_id)
|
|
|
+ validation = next(
|
|
|
+ ledger.validations[item]
|
|
|
+ for item in task.validation_ids
|
|
|
+ if ledger.validations[item].attempt_id == attempt.attempt_id
|
|
|
+ )
|
|
|
+ assert validation.verdict is ValidationVerdict.PASSED
|
|
|
+ validator_trace = await reopened_traces.get_trace(validation.validator_trace_id)
|
|
|
+ assert validator_trace is not None and validator_trace.status == "completed"
|
|
|
+ assert await reopened_traces.get_trace_messages(validation.validator_trace_id)
|
|
|
+
|
|
|
+ element_replacement = task_by_objective["place the explored element on the adopted opening"]
|
|
|
+ element_decision, element_ref = _accepted_ref(ledger, element_replacement)
|
|
|
+ element_version = await business_artifacts.read_by_ref(
|
|
|
+ element_ref,
|
|
|
+ script_build_id=script_build_id,
|
|
|
+ task_id=element_replacement.task_id,
|
|
|
+ attempt_id=cast(str, ledger.decisions[element_decision].attempt_id),
|
|
|
+ )
|
|
|
+ assert element_version.artifact_type is ArtifactKind.ELEMENT_SET
|
|
|
+ assert len(cast(Any, element_version.artifact).paragraph_element_links) == 1
|
|
|
+
|
|
|
+ compose = next(
|
|
|
+ task
|
|
|
+ for task in ledger.tasks.values()
|
|
|
+ if task.task_id != ledger.root_task_id and _task_kind(task) == "compose"
|
|
|
+ )
|
|
|
+ assert compose.status is TaskStatus.COMPLETED
|
|
|
+ contract_ref = next(
|
|
|
+ item
|
|
|
+ for item in compose.current_spec.context_refs
|
|
|
+ if item.startswith("script-build://task-contracts/sha256/")
|
|
|
+ )
|
|
|
+ compose_contract = await FileScriptTaskContractStore(tmp_path / "agent-data").read(
|
|
|
+ ROOT, contract_ref
|
|
|
+ )
|
|
|
+ adopted_kinds = tuple(
|
|
|
+ _task_kind(ledger.tasks[ledger.decisions[decision_id].task_id])
|
|
|
+ for decision_id in compose_contract.contract.compose_order
|
|
|
+ )
|
|
|
+ assert adopted_kinds == ("structure", "paragraph", "element-set")
|
|
|
+ portfolio = next(
|
|
|
+ task
|
|
|
+ for task in ledger.tasks.values()
|
|
|
+ if task.task_id != ledger.root_task_id and _task_kind(task) == "candidate-portfolio"
|
|
|
+ )
|
|
|
+ assert portfolio.status is TaskStatus.COMPLETED
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.asyncio
|
|
|
+async def test_mission_service_real_runner_auto_continues_phase_one_to_phase_two_boundary(
|
|
|
+ database: Any, tmp_path: Path
|
|
|
+) -> None:
|
|
|
+ _, sessions = database
|
|
|
+ build_states = SqlAlchemyLegacyBuildStateRepository(sessions)
|
|
|
+ script_build_id = await build_states.create(
|
|
|
+ execution_id=701,
|
|
|
+ topic_build_id=702,
|
|
|
+ topic_id=703,
|
|
|
+ agent_type="script_planner",
|
|
|
+ agent_config={},
|
|
|
+ data_source_url=None,
|
|
|
+ strategies_config={},
|
|
|
+ )
|
|
|
+ prompt_manifest = script_build_prompt_manifest()
|
|
|
+ model_manifest = {
|
|
|
+ "presets": {
|
|
|
+ str(item["preset"]): {
|
|
|
+ "model": "scripted-fake-model",
|
|
|
+ "temperature": 0,
|
|
|
+ "max_iterations": 80,
|
|
|
+ }
|
|
|
+ for item in prompt_manifest
|
|
|
+ }
|
|
|
+ }
|
|
|
+ snapshot_repository = SqlAlchemyInputSnapshotRepository(sessions)
|
|
|
+ snapshot = await snapshot_repository.freeze(
|
|
|
+ ScriptBuildInput(
|
|
|
+ script_build_id=script_build_id,
|
|
|
+ execution_id=701,
|
|
|
+ topic_build_id=702,
|
|
|
+ topic_id=703,
|
|
|
+ topic={"topic": {"id": 703, "result": "A concrete reversal changes a belief"}},
|
|
|
+ account={"account_name": "fixture-account"},
|
|
|
+ prompt_manifest=tuple(dict(item) for item in prompt_manifest),
|
|
|
+ model_manifest=model_manifest,
|
|
|
+ )
|
|
|
+ )
|
|
|
+ bindings = SqlAlchemyMissionBindingRepository(sessions)
|
|
|
+ await bindings.create(
|
|
|
+ script_build_id=script_build_id,
|
|
|
+ root_trace_id=ROOT,
|
|
|
+ input_snapshot_id=int(snapshot.snapshot_id),
|
|
|
+ engine_version="phase-two-real-runner-test",
|
|
|
+ schema_version="v2",
|
|
|
+ )
|
|
|
+ task_store = FileSystemTaskStore(str(tmp_path / "ledger"))
|
|
|
+ trace_store = FileSystemTraceStore(str(tmp_path / "trace"))
|
|
|
+ llm = _PhaseTwoScriptedLLM(task_store)
|
|
|
+ runner = AgentRunner(trace_store=trace_store, llm_call=llm)
|
|
|
+ framework_artifacts = FileSystemArtifactStore(str(tmp_path / "framework-artifacts"))
|
|
|
+ business_artifacts = SqlAlchemyScriptBusinessArtifactRepository(sessions)
|
|
|
+ workspaces = SqlAlchemyCandidateWorkspaceRepository(sessions, business_artifacts)
|
|
|
+ composition = compose_host(
|
|
|
+ HostDependencies(
|
|
|
+ runner=runner,
|
|
|
+ task_store=task_store,
|
|
|
+ framework_artifact_store=framework_artifacts,
|
|
|
+ input_snapshot_service=cast(Any, snapshot_repository),
|
|
|
+ input_snapshots=snapshot_repository,
|
|
|
+ bindings=bindings,
|
|
|
+ business_artifacts=business_artifacts,
|
|
|
+ publications=SqlAlchemyPublicationRepository(sessions),
|
|
|
+ legacy_state=build_states,
|
|
|
+ principal_provider=_PrincipalProvider(),
|
|
|
+ build_authorizer=_Authorizer(),
|
|
|
+ retrieval_adapters={"decode": _DecodeAdapter()},
|
|
|
+ enabled_phase=2,
|
|
|
+ agent_data_root=tmp_path / "agent-data",
|
|
|
+ candidate_workspaces=workspaces,
|
|
|
+ default_model_manifest=model_manifest,
|
|
|
+ )
|
|
|
+ )
|
|
|
+
|
|
|
+ await build_states.set_status(script_build_id, BuildStatus.RUNNING)
|
|
|
+ await composition.mission_service.run(script_build_id)
|
|
|
+
|
|
|
+ ledger = await task_store.load(ROOT)
|
|
|
+ root = ledger.tasks[ledger.root_task_id]
|
|
|
+ assert root.status is TaskStatus.BLOCKED
|
|
|
+ assert root.blocked_reason == PHASE_TWO_CANDIDATE_PORTFOLIO_READY
|
|
|
+ phase_two_blocks = [
|
|
|
+ item
|
|
|
+ for item in ledger.decisions.values()
|
|
|
+ if item.task_id == root.task_id
|
|
|
+ and item.action is DecisionAction.BLOCK
|
|
|
+ and item.reason == PHASE_TWO_CANDIDATE_PORTFOLIO_READY
|
|
|
+ ]
|
|
|
+ phase_one_blocks = [
|
|
|
+ item
|
|
|
+ for item in ledger.decisions.values()
|
|
|
+ if item.task_id == root.task_id
|
|
|
+ and item.action is DecisionAction.BLOCK
|
|
|
+ and item.reason == PHASE_ONE_CAPABILITY_BOUNDARY
|
|
|
+ ]
|
|
|
+ assert len(phase_one_blocks) == len(phase_two_blocks) == 1
|
|
|
+ assert await build_states.get_status(script_build_id) is BuildStatus.PARTIAL
|
|
|
+ assert llm.worker_kinds == [
|
|
|
+ "decode-retrieval",
|
|
|
+ "direction",
|
|
|
+ "structure",
|
|
|
+ "paragraph",
|
|
|
+ "compose",
|
|
|
+ "compare",
|
|
|
+ "candidate-portfolio",
|
|
|
+ ]
|
|
|
+ assert llm.validator_kinds == [
|
|
|
+ "decode-retrieval",
|
|
|
+ "direction",
|
|
|
+ "structure",
|
|
|
+ "paragraph",
|
|
|
+ "compose",
|
|
|
+ "compare",
|
|
|
+ "candidate-portfolio",
|
|
|
+ ]
|
|
|
+ assert llm.submit_attempt_arguments == [{}] * 10
|
|
|
+ assert llm.compose_candidates_saved == 2
|
|
|
+
|
|
|
+ compose_task = next(
|
|
|
+ task
|
|
|
+ for task in ledger.tasks.values()
|
|
|
+ if task.task_id != ledger.root_task_id and _task_kind(task) == "compose"
|
|
|
+ )
|
|
|
+ assert compose_task.current_spec_version == 3
|
|
|
+ assert len(compose_task.attempt_ids) == 2
|
|
|
+ assert [ledger.validations[item].verdict for item in compose_task.validation_ids] == [
|
|
|
+ ValidationVerdict.FAILED,
|
|
|
+ ValidationVerdict.PASSED,
|
|
|
+ ]
|
|
|
+ assert [ledger.decisions[item].action for item in compose_task.decision_ids] == [
|
|
|
+ DecisionAction.REVISE,
|
|
|
+ DecisionAction.SPLIT,
|
|
|
+ DecisionAction.REVISE,
|
|
|
+ DecisionAction.ACCEPT,
|
|
|
+ ]
|
|
|
+
|
|
|
+ paragraph_tasks = [
|
|
|
+ task
|
|
|
+ for task in ledger.tasks.values()
|
|
|
+ if task.task_id != ledger.root_task_id and _task_kind(task) == "paragraph"
|
|
|
+ ]
|
|
|
+ assert len(paragraph_tasks) == 2
|
|
|
+ replacement = next(
|
|
|
+ task for task in paragraph_tasks if "replace the placeholder" in task.current_spec.objective
|
|
|
+ )
|
|
|
+ phase_two_retrieval = next(
|
|
|
+ task
|
|
|
+ for task in ledger.tasks.values()
|
|
|
+ if task.task_id != ledger.root_task_id
|
|
|
+ and _task_kind(task) == "decode-retrieval"
|
|
|
+ and task.parent_task_id == replacement.task_id
|
|
|
+ )
|
|
|
+ replacement_attempt = ledger.attempts[replacement.attempt_ids[-1]]
|
|
|
+ assert replacement_attempt.accepted_child_decision_ids == (
|
|
|
+ phase_two_retrieval.decision_ids[-1],
|
|
|
+ )
|
|
|
+ assert (
|
|
|
+ len(
|
|
|
+ [
|
|
|
+ task
|
|
|
+ for task in ledger.tasks.values()
|
|
|
+ if task.task_id != ledger.root_task_id and _task_kind(task) == "compare"
|
|
|
+ ]
|
|
|
+ )
|
|
|
+ == 1
|
|
|
+ )
|
|
|
+
|
|
|
+ expected_identities = {
|
|
|
+ str(item["preset"]): str(item["content_sha256"]) for item in prompt_manifest
|
|
|
+ }
|
|
|
+ for attempt in ledger.attempts.values():
|
|
|
+ worker_trace = await trace_store.get_trace(attempt.worker_trace_id)
|
|
|
+ assert worker_trace is not None
|
|
|
+ assert (
|
|
|
+ worker_trace.context["role_prompt_identity"]
|
|
|
+ == expected_identities[attempt.worker_preset]
|
|
|
+ )
|
|
|
+ validations = [
|
|
|
+ ledger.validations[item]
|
|
|
+ for item in ledger.tasks[attempt.task_id].validation_ids
|
|
|
+ if ledger.validations[item].attempt_id == attempt.attempt_id
|
|
|
+ ]
|
|
|
+ assert len(validations) == 1
|
|
|
+ validator_trace = await trace_store.get_trace(validations[0].validator_trace_id)
|
|
|
+ assert validator_trace is not None
|
|
|
+ assert (
|
|
|
+ validator_trace.context["role_prompt_identity"]
|
|
|
+ == expected_identities[validations[0].validator_preset]
|
|
|
+ )
|
|
|
+
|
|
|
+ reopened_traces = FileSystemTraceStore(str(tmp_path / "trace"))
|
|
|
+ planner_trace = await reopened_traces.get_trace(ROOT)
|
|
|
+ assert planner_trace is not None
|
|
|
+ assert planner_trace.status == "incomplete"
|
|
|
+ assert planner_trace.error_message == "mission_blocked"
|
|
|
+ planner_messages = await reopened_traces.get_trace_messages(ROOT)
|
|
|
+ phase_two_policies = [
|
|
|
+ item
|
|
|
+ for item in planner_messages
|
|
|
+ if item.role == "system" and "script-build-phase-two/v1" in str(item.content)
|
|
|
+ ]
|
|
|
+ phase_two_continuations = [
|
|
|
+ item
|
|
|
+ for item in planner_messages
|
|
|
+ if item.role == "user"
|
|
|
+ and '"mission": "continue_toward_root"' in str(item.content)
|
|
|
+ and '"phase": 2' in str(item.content)
|
|
|
+ ]
|
|
|
+ assert len(phase_two_policies) == len(phase_two_continuations) == 1
|
|
|
+
|
|
|
+ portfolio_task = next(
|
|
|
+ task
|
|
|
+ for task in ledger.tasks.values()
|
|
|
+ if task.task_id != ledger.root_task_id and _task_kind(task) == "candidate-portfolio"
|
|
|
+ )
|
|
|
+
|
|
|
+ def belongs_to_phase_two(task: Any) -> bool:
|
|
|
+ current = task
|
|
|
+ while current.parent_task_id is not None:
|
|
|
+ if current.task_id == portfolio_task.task_id:
|
|
|
+ return True
|
|
|
+ current = ledger.tasks[current.parent_task_id]
|
|
|
+ return False
|
|
|
+
|
|
|
+ phase_two_attempts = [
|
|
|
+ attempt
|
|
|
+ for attempt in ledger.attempts.values()
|
|
|
+ if belongs_to_phase_two(ledger.tasks[attempt.task_id])
|
|
|
+ ]
|
|
|
+ assert phase_two_attempts
|
|
|
+ for attempt in phase_two_attempts:
|
|
|
+ worker_trace = await reopened_traces.get_trace(attempt.worker_trace_id)
|
|
|
+ assert worker_trace is not None and worker_trace.status == "completed"
|
|
|
+ assert await reopened_traces.get_trace_messages(attempt.worker_trace_id)
|
|
|
+ validation = next(
|
|
|
+ ledger.validations[item]
|
|
|
+ for item in ledger.tasks[attempt.task_id].validation_ids
|
|
|
+ if ledger.validations[item].attempt_id == attempt.attempt_id
|
|
|
+ )
|
|
|
+ validator_trace = await reopened_traces.get_trace(validation.validator_trace_id)
|
|
|
+ assert validator_trace is not None and validator_trace.status == "completed"
|
|
|
+ assert await reopened_traces.get_trace_messages(validation.validator_trace_id)
|
|
|
+
|
|
|
+ async with sessions() as session:
|
|
|
+ rows = (
|
|
|
+ (
|
|
|
+ await session.execute(
|
|
|
+ select(artifact_version_table).where(
|
|
|
+ artifact_version_table.c.script_build_id == script_build_id
|
|
|
+ )
|
|
|
+ )
|
|
|
+ )
|
|
|
+ .mappings()
|
|
|
+ .all()
|
|
|
+ )
|
|
|
+ branch_ids = list(
|
|
|
+ await session.scalars(
|
|
|
+ select(script_build_paragraph.c.branch_id).where(
|
|
|
+ script_build_paragraph.c.script_build_id == script_build_id
|
|
|
+ )
|
|
|
+ )
|
|
|
+ )
|
|
|
+ build = (
|
|
|
+ (
|
|
|
+ await session.execute(
|
|
|
+ select(script_build_record).where(script_build_record.c.id == script_build_id)
|
|
|
+ )
|
|
|
+ )
|
|
|
+ .mappings()
|
|
|
+ .one()
|
|
|
+ )
|
|
|
+ kinds = {str(row["artifact_type"]) for row in rows}
|
|
|
+ assert {
|
|
|
+ ArtifactKind.DIRECTION.value,
|
|
|
+ ArtifactKind.PARAGRAPH.value,
|
|
|
+ ArtifactKind.COMPARISON.value,
|
|
|
+ ArtifactKind.STRUCTURED_SCRIPT.value,
|
|
|
+ ArtifactKind.CANDIDATE_PORTFOLIO.value,
|
|
|
+ } <= kinds
|
|
|
+ assert branch_ids and min(branch_ids) > 0 and 0 not in branch_ids
|
|
|
+ assert BuildStatus(str(build["status"])) is BuildStatus.PARTIAL
|
|
|
+ assert build["error_message"] == PHASE_TWO_CANDIDATE_PORTFOLIO_READY
|
|
|
+ assert build["reson_trace_id"] == ROOT
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.asyncio
|
|
|
+async def test_historical_partial_advances_via_http_and_reenters_after_filestore_reload(
|
|
|
+ database: Any,
|
|
|
+ tmp_path: Path,
|
|
|
+) -> None:
|
|
|
+ """Exercise SQL v1->v2 CAS and the only pre-recovery crash-safe HTTP window."""
|
|
|
+
|
|
|
+ _, sessions = database
|
|
|
+ build_states = SqlAlchemyLegacyBuildStateRepository(sessions)
|
|
|
+ script_build_id = await build_states.create(
|
|
|
+ execution_id=801,
|
|
|
+ topic_build_id=802,
|
|
|
+ topic_id=803,
|
|
|
+ agent_type="script_planner",
|
|
|
+ agent_config={},
|
|
|
+ data_source_url=None,
|
|
|
+ strategies_config={},
|
|
|
+ )
|
|
|
+ phase_one_prompts = phase_one_prompt_manifest()
|
|
|
+ full_prompts = script_build_prompt_manifest()
|
|
|
+ phase_one_models = {
|
|
|
+ "presets": {
|
|
|
+ str(item["preset"]): {
|
|
|
+ "model": "scripted-fake-model",
|
|
|
+ "temperature": 0,
|
|
|
+ "max_iterations": 80,
|
|
|
+ }
|
|
|
+ for item in phase_one_prompts
|
|
|
+ }
|
|
|
+ }
|
|
|
+ full_models = {
|
|
|
+ "presets": {
|
|
|
+ str(item["preset"]): {
|
|
|
+ "model": "scripted-fake-model",
|
|
|
+ "temperature": 0,
|
|
|
+ "max_iterations": 80,
|
|
|
+ }
|
|
|
+ for item in full_prompts
|
|
|
+ }
|
|
|
+ }
|
|
|
+ snapshots = SqlAlchemyInputSnapshotRepository(sessions)
|
|
|
+ phase_one_snapshot = await snapshots.freeze(
|
|
|
+ ScriptBuildInput(
|
|
|
+ script_build_id=script_build_id,
|
|
|
+ execution_id=801,
|
|
|
+ topic_build_id=802,
|
|
|
+ topic_id=803,
|
|
|
+ topic={"topic": {"id": 803, "result": "A historical Phase1 checkpoint"}},
|
|
|
+ account={"account_name": "historical-fixture"},
|
|
|
+ prompt_manifest=tuple(dict(item) for item in phase_one_prompts),
|
|
|
+ model_manifest=phase_one_models,
|
|
|
+ )
|
|
|
+ )
|
|
|
+ bindings = SqlAlchemyMissionBindingRepository(sessions)
|
|
|
+ await bindings.create(
|
|
|
+ script_build_id=script_build_id,
|
|
|
+ root_trace_id=ROOT,
|
|
|
+ input_snapshot_id=int(phase_one_snapshot.snapshot_id),
|
|
|
+ engine_version="phase-two-http-reentry-test",
|
|
|
+ schema_version="v1",
|
|
|
+ )
|
|
|
+
|
|
|
+ phase_one_store = FileSystemTaskStore(str(tmp_path / "ledger"))
|
|
|
+ phase_one_host = _persisted_host(
|
|
|
+ sessions=sessions,
|
|
|
+ tmp_path=tmp_path,
|
|
|
+ llm=_PhaseTwoScriptedLLM(phase_one_store),
|
|
|
+ enabled_phase=1,
|
|
|
+ model_manifest=full_models,
|
|
|
+ )
|
|
|
+ await build_states.set_status(script_build_id, BuildStatus.RUNNING)
|
|
|
+ await phase_one_host.mission_service.run(script_build_id)
|
|
|
+
|
|
|
+ phase_one_ledger = await phase_one_store.load(ROOT)
|
|
|
+ phase_one_root = phase_one_ledger.tasks[phase_one_ledger.root_task_id]
|
|
|
+ assert phase_one_root.status is TaskStatus.BLOCKED
|
|
|
+ assert phase_one_root.blocked_reason == PHASE_ONE_CAPABILITY_BOUNDARY
|
|
|
+ assert await build_states.get_status(script_build_id) is BuildStatus.PARTIAL
|
|
|
+
|
|
|
+ # Reopen every file-backed component to represent the first Phase2 process.
|
|
|
+ transition_store = FileSystemTaskStore(str(tmp_path / "ledger"))
|
|
|
+ blocked_llm = _BlockedAfterPolicyLLM(transition_store)
|
|
|
+ transition_host = _persisted_host(
|
|
|
+ sessions=sessions,
|
|
|
+ tmp_path=tmp_path,
|
|
|
+ llm=blocked_llm,
|
|
|
+ enabled_phase=2,
|
|
|
+ model_manifest=full_models,
|
|
|
+ )
|
|
|
+ async with httpx.AsyncClient(
|
|
|
+ transport=httpx.ASGITransport(app=transition_host.app),
|
|
|
+ base_url="http://test",
|
|
|
+ ) as client:
|
|
|
+ response = await client.post(
|
|
|
+ f"/api/pattern/script_builds/{script_build_id}/phase-two/advance"
|
|
|
+ )
|
|
|
+ assert response.status_code == 200, response.text
|
|
|
+ first_advance = response.json()
|
|
|
+ assert first_advance["root_trace_id"] == ROOT
|
|
|
+ assert first_advance["input_snapshot_id"] != phase_one_snapshot.snapshot_id
|
|
|
+ phase_two_snapshot_id = str(first_advance["input_snapshot_id"])
|
|
|
+
|
|
|
+ await asyncio.wait_for(blocked_llm.phase_two_model_call.wait(), timeout=10)
|
|
|
+ transition_ledger = await transition_store.load(ROOT)
|
|
|
+ assert transition_ledger.tasks[transition_ledger.root_task_id].status is TaskStatus.NEEDS_REPLAN
|
|
|
+ assert await build_states.get_status(script_build_id) is BuildStatus.RUNNING
|
|
|
+ crashed_task = transition_host.mission_service._runs[script_build_id]
|
|
|
+ crashed_task.cancel()
|
|
|
+ with pytest.raises(asyncio.CancelledError):
|
|
|
+ await crashed_task
|
|
|
+
|
|
|
+ reopened_after_crash = FileSystemTraceStore(str(tmp_path / "trace"))
|
|
|
+ persisted_messages = await reopened_after_crash.get_trace_messages(ROOT)
|
|
|
+ persisted_events = await reopened_after_crash.get_events(ROOT, 0)
|
|
|
+ assert _phase_two_policy_count(persisted_messages) == 1
|
|
|
+ assert _phase_two_continuation_count(persisted_messages) == 1
|
|
|
+ assert _policy_migration_count(persisted_events) == 1
|
|
|
+
|
|
|
+ # A fresh Host must take the NEEDS_REPLAN reentry path without rewriting policy.
|
|
|
+ retry_store = FileSystemTaskStore(str(tmp_path / "ledger"))
|
|
|
+ retry_host = _persisted_host(
|
|
|
+ sessions=sessions,
|
|
|
+ tmp_path=tmp_path,
|
|
|
+ llm=_PhaseTwoScriptedLLM(retry_store),
|
|
|
+ enabled_phase=2,
|
|
|
+ model_manifest=full_models,
|
|
|
+ )
|
|
|
+ async with httpx.AsyncClient(
|
|
|
+ transport=httpx.ASGITransport(app=retry_host.app),
|
|
|
+ base_url="http://test",
|
|
|
+ ) as client:
|
|
|
+ retry_response = await client.post(
|
|
|
+ f"/api/pattern/script_builds/{script_build_id}/phase-two/advance"
|
|
|
+ )
|
|
|
+ assert retry_response.status_code == 200, retry_response.text
|
|
|
+ assert retry_response.json()["root_trace_id"] == ROOT
|
|
|
+ assert retry_response.json()["input_snapshot_id"] == phase_two_snapshot_id
|
|
|
+ retry_task = retry_host.mission_service._runs[script_build_id]
|
|
|
+ await asyncio.wait_for(asyncio.shield(retry_task), timeout=60)
|
|
|
+
|
|
|
+ final_binding = await bindings.get_by_build(script_build_id)
|
|
|
+ assert final_binding.root_trace_id == ROOT
|
|
|
+ assert str(final_binding.input_snapshot_id) == phase_two_snapshot_id
|
|
|
+ phase_two_snapshot = await snapshots.get(
|
|
|
+ phase_two_snapshot_id,
|
|
|
+ script_build_id=script_build_id,
|
|
|
+ )
|
|
|
+ assert phase_two_snapshot.parent_snapshot_id == phase_one_snapshot.snapshot_id
|
|
|
+ assert phase_two_snapshot.parent_snapshot_sha256 == phase_one_snapshot.canonical_sha256
|
|
|
+ assert (
|
|
|
+ phase_two_snapshot.business_input_sha256
|
|
|
+ == canonical_sha256(phase_one_snapshot.to_input().business_payload()).wire
|
|
|
+ )
|
|
|
+ assert {str(item["preset"]) for item in phase_two_snapshot.prompt_manifest} == {
|
|
|
+ str(item["preset"]) for item in full_prompts
|
|
|
+ }
|
|
|
+
|
|
|
+ async with sessions() as session:
|
|
|
+ snapshot_rows = (
|
|
|
+ (
|
|
|
+ await session.execute(
|
|
|
+ select(input_snapshot_table)
|
|
|
+ .where(input_snapshot_table.c.script_build_id == script_build_id)
|
|
|
+ .order_by(input_snapshot_table.c.version)
|
|
|
+ )
|
|
|
+ )
|
|
|
+ .mappings()
|
|
|
+ .all()
|
|
|
+ )
|
|
|
+ binding_row = (
|
|
|
+ (
|
|
|
+ await session.execute(
|
|
|
+ select(mission_binding_table).where(
|
|
|
+ mission_binding_table.c.script_build_id == script_build_id
|
|
|
+ )
|
|
|
+ )
|
|
|
+ )
|
|
|
+ .mappings()
|
|
|
+ .one()
|
|
|
+ )
|
|
|
+ assert [int(item["version"]) for item in snapshot_rows] == [1, 2]
|
|
|
+ assert int(binding_row["input_snapshot_id"]) == int(phase_two_snapshot_id)
|
|
|
+ assert str(binding_row["root_trace_id"]) == ROOT
|
|
|
+
|
|
|
+ final_ledger = await retry_store.load(ROOT)
|
|
|
+ final_root = final_ledger.tasks[final_ledger.root_task_id]
|
|
|
+ assert final_root.status is TaskStatus.BLOCKED
|
|
|
+ assert final_root.blocked_reason == PHASE_TWO_CANDIDATE_PORTFOLIO_READY
|
|
|
+ assert await build_states.get_status(script_build_id) is BuildStatus.PARTIAL
|
|
|
+ direction = next(
|
|
|
+ item
|
|
|
+ for item in final_ledger.tasks.values()
|
|
|
+ if item.task_id != final_ledger.root_task_id and _task_kind(item) == "direction"
|
|
|
+ )
|
|
|
+ direction_attempt = final_ledger.attempts[direction.attempt_ids[-1]]
|
|
|
+ direction_spec = next(
|
|
|
+ item for item in direction.specs if item.version == direction_attempt.spec_version
|
|
|
+ )
|
|
|
+ assert f"script-build://inputs/{phase_one_snapshot.snapshot_id}" in direction_spec.context_refs
|
|
|
+
|
|
|
+ final_traces = FileSystemTraceStore(str(tmp_path / "trace"))
|
|
|
+ final_messages = await final_traces.get_trace_messages(ROOT)
|
|
|
+ final_events = await final_traces.get_events(ROOT, 0)
|
|
|
+ assert _phase_two_policy_count(final_messages) == 1
|
|
|
+ assert _phase_two_continuation_count(final_messages) == 1
|
|
|
+ assert _policy_migration_count(final_events) == 1
|
|
|
+
|
|
|
+
|
|
|
+def _phase_two_policy_count(messages: Sequence[Any]) -> int:
|
|
|
+ return len(
|
|
|
+ [
|
|
|
+ item
|
|
|
+ for item in messages
|
|
|
+ if item.role == "system" and "script-build-phase-two/v1" in str(item.content)
|
|
|
+ ]
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+def _phase_two_continuation_count(messages: Sequence[Any]) -> int:
|
|
|
+ return len(
|
|
|
+ [
|
|
|
+ item
|
|
|
+ for item in messages
|
|
|
+ if item.role == "user" and '"mission": "continue_toward_root"' in str(item.content)
|
|
|
+ ]
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+def _policy_migration_count(events: Sequence[Mapping[str, Any]]) -> int:
|
|
|
+ return len(
|
|
|
+ [
|
|
|
+ item
|
|
|
+ for item in events
|
|
|
+ if item.get("event") == "planner_policy_migrated"
|
|
|
+ or item.get("event_type") == "planner_policy_migrated"
|
|
|
+ ]
|
|
|
+ )
|