|
|
@@ -0,0 +1,660 @@
|
|
|
+"""Bounded read and command surface for one script-build Mission."""
|
|
|
+
|
|
|
+from __future__ import annotations
|
|
|
+
|
|
|
+import json
|
|
|
+from collections import Counter
|
|
|
+from collections.abc import Mapping
|
|
|
+from dataclasses import asdict, is_dataclass
|
|
|
+from hashlib import sha256
|
|
|
+from typing import Any, cast
|
|
|
+
|
|
|
+from agent.failures import FailureDetail, FailureDisposition, ToolExecutionError
|
|
|
+from agent.orchestration import AgentRole, DecisionAction, RoleContextRequest, TaskStatus
|
|
|
+
|
|
|
+from script_build_host.domain.phase_two_ports import ScriptTaskContractStore
|
|
|
+from script_build_host.domain.ports import (
|
|
|
+ InputSnapshotRepository,
|
|
|
+ MissionBindingRepository,
|
|
|
+ ScriptBusinessArtifactRepository,
|
|
|
+)
|
|
|
+from script_build_host.domain.task_contracts import (
|
|
|
+ ScriptTaskContractV1,
|
|
|
+ ScriptTaskKind,
|
|
|
+ task_kind_from_context_refs,
|
|
|
+)
|
|
|
+from script_build_host.domain.workbench import (
|
|
|
+ NotModified,
|
|
|
+ PlannerState,
|
|
|
+ ValidatorState,
|
|
|
+ WorkerState,
|
|
|
+ paragraph_target_key,
|
|
|
+ protect_model_value,
|
|
|
+ semantic_handle,
|
|
|
+ strategy_handle,
|
|
|
+)
|
|
|
+
|
|
|
+TASK_CONTRACT_REF_PREFIX = "script-build://task-contracts/sha256/"
|
|
|
+_ACTIVE = {
|
|
|
+ TaskStatus.PENDING,
|
|
|
+ TaskStatus.RUNNING,
|
|
|
+ TaskStatus.AWAITING_VALIDATION,
|
|
|
+ TaskStatus.VALIDATING,
|
|
|
+ TaskStatus.AWAITING_DECISION,
|
|
|
+ TaskStatus.NEEDS_REPLAN,
|
|
|
+ TaskStatus.WAITING_CHILDREN,
|
|
|
+ TaskStatus.BLOCKED,
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+class WorkbenchError(RuntimeError):
|
|
|
+ def __init__(self, code: str, message: str) -> None:
|
|
|
+ super().__init__(message)
|
|
|
+ self.code = code
|
|
|
+ self.message = message
|
|
|
+
|
|
|
+
|
|
|
+class MissionWorkbench:
|
|
|
+ """Projects durable Mission state without becoming a second source of truth."""
|
|
|
+
|
|
|
+ def __init__(
|
|
|
+ self,
|
|
|
+ *,
|
|
|
+ task_store: Any,
|
|
|
+ bindings: MissionBindingRepository,
|
|
|
+ snapshots: InputSnapshotRepository,
|
|
|
+ artifacts: ScriptBusinessArtifactRepository,
|
|
|
+ contracts: ScriptTaskContractStore,
|
|
|
+ candidates: Any | None = None,
|
|
|
+ ) -> None:
|
|
|
+ self._task_store = task_store
|
|
|
+ self._bindings = bindings
|
|
|
+ self._snapshots = snapshots
|
|
|
+ self._artifacts = artifacts
|
|
|
+ self._contracts = contracts
|
|
|
+ self._candidates = candidates
|
|
|
+
|
|
|
+ async def render(self, root_trace_id: str, ledger: Any) -> str:
|
|
|
+ state = await self.planner_state(root_trace_id, ledger=ledger)
|
|
|
+ return _bounded_json(state.to_payload(), 24_000, "PlannerState")
|
|
|
+
|
|
|
+ async def build(self, request: RoleContextRequest) -> Mapping[str, Any]:
|
|
|
+ try:
|
|
|
+ if request.role is AgentRole.WORKER:
|
|
|
+ return _json_safe((await self.worker_state(request)).to_payload())
|
|
|
+ if request.role is AgentRole.VALIDATOR:
|
|
|
+ return _json_safe((await self.validator_state(request)).to_payload())
|
|
|
+ return {}
|
|
|
+ except WorkbenchError as exc:
|
|
|
+ raise ToolExecutionError(
|
|
|
+ FailureDetail(
|
|
|
+ code=exc.code,
|
|
|
+ message=exc.message,
|
|
|
+ disposition=FailureDisposition.ABORT_RUN,
|
|
|
+ source_tool="mission_workbench_role_context",
|
|
|
+ details={"task_id": request.task_id, "role": request.role.value},
|
|
|
+ )
|
|
|
+ ) from exc
|
|
|
+
|
|
|
+ async def planner_state(
|
|
|
+ self,
|
|
|
+ root_trace_id: str,
|
|
|
+ *,
|
|
|
+ known_revision: str | None = None,
|
|
|
+ ledger: Any | None = None,
|
|
|
+ ) -> PlannerState | NotModified:
|
|
|
+ ledger = ledger or await self._task_store.load(root_trace_id)
|
|
|
+ revision = f"ledger:{ledger.revision}"
|
|
|
+ if known_revision == revision:
|
|
|
+ return NotModified(revision)
|
|
|
+ binding = await self._bindings.get_by_root(root_trace_id)
|
|
|
+ counts = Counter(task.status.value for task in ledger.tasks.values())
|
|
|
+ included = self._active_subgraph_ids(ledger)
|
|
|
+ active_tasks: list[dict[str, Any]] = []
|
|
|
+ accepted: list[dict[str, Any]] = []
|
|
|
+ covered_goals: set[str] = set()
|
|
|
+ all_goals: tuple[str, ...] = ()
|
|
|
+ for task in sorted(ledger.tasks.values(), key=lambda item: item.display_path):
|
|
|
+ contract = (
|
|
|
+ None
|
|
|
+ if task.task_id == ledger.root_task_id
|
|
|
+ else await self._contract(root_trace_id, task)
|
|
|
+ )
|
|
|
+ if contract is not None:
|
|
|
+ covered_goals.update(
|
|
|
+ contract.goal_ids if self._accepted_decision(ledger, task) is not None else ()
|
|
|
+ )
|
|
|
+ if task.task_id in included:
|
|
|
+ active_tasks.append(self._task_summary(task, contract))
|
|
|
+ decision = self._accepted_decision(ledger, task)
|
|
|
+ if decision is not None and contract is not None:
|
|
|
+ accepted.append(
|
|
|
+ {
|
|
|
+ "decision_id": decision.decision_id,
|
|
|
+ "task_id": task.task_id,
|
|
|
+ "task_kind": contract.task_kind.value,
|
|
|
+ "scope_selector": _scope_selector(contract.scope_ref),
|
|
|
+ "goal_ids": list(contract.goal_ids),
|
|
|
+ "summary": decision.reason[:300],
|
|
|
+ }
|
|
|
+ )
|
|
|
+ if binding.active_direction_artifact_version_id is not None:
|
|
|
+ version = await self._artifacts.get_by_id(
|
|
|
+ binding.active_direction_artifact_version_id,
|
|
|
+ script_build_id=binding.script_build_id,
|
|
|
+ )
|
|
|
+ goals = getattr(version.artifact, "goals", ())
|
|
|
+ all_goals = tuple(str(item.goal_id) for item in goals)
|
|
|
+ root = ledger.tasks[ledger.root_task_id]
|
|
|
+ failures = self._latest_failures(ledger)
|
|
|
+ operations = tuple(
|
|
|
+ {
|
|
|
+ "operation_id": item.operation_id,
|
|
|
+ "kind": item.kind.value,
|
|
|
+ "status": item.status.value,
|
|
|
+ "task_count": len(item.task_ids),
|
|
|
+ }
|
|
|
+ for item in ledger.operations.values()
|
|
|
+ if item.status.value in {"pending", "running", "stop_requested"}
|
|
|
+ )
|
|
|
+ state = PlannerState(
|
|
|
+ state_revision=revision,
|
|
|
+ phase=_phase(ledger, root, binding.active_direction_artifact_version_id),
|
|
|
+ root={
|
|
|
+ "task_id": root.task_id,
|
|
|
+ "status": root.status.value,
|
|
|
+ "blocked_reason": root.blocked_reason,
|
|
|
+ "objective": root.current_spec.objective,
|
|
|
+ },
|
|
|
+ focused_task_id=ledger.focused_task_id,
|
|
|
+ active_subgraph=tuple(active_tasks[:24]),
|
|
|
+ accepted_frontier=tuple(accepted[-32:]),
|
|
|
+ goal_progress={
|
|
|
+ "total": len(all_goals),
|
|
|
+ "covered": len(set(all_goals) & covered_goals),
|
|
|
+ "uncovered_goal_ids": [item for item in all_goals if item not in covered_goals],
|
|
|
+ },
|
|
|
+ counts_by_status=dict(sorted(counts.items())),
|
|
|
+ active_operations=operations,
|
|
|
+ latest_failures=failures,
|
|
|
+ retired_task_count=sum(
|
|
|
+ 1 for item in ledger.tasks.values() if item.task_id not in included
|
|
|
+ ),
|
|
|
+ changes=self._recent_changes(ledger),
|
|
|
+ )
|
|
|
+ _bounded_json(state.to_payload(), 24_000, "PlannerState")
|
|
|
+ return state
|
|
|
+
|
|
|
+ async def worker_state(self, request: RoleContextRequest) -> WorkerState:
|
|
|
+ ledger = await self._task_store.load(request.root_trace_id)
|
|
|
+ task = ledger.tasks[request.task_id]
|
|
|
+ contract = await self._contract(request.root_trace_id, task)
|
|
|
+ if contract is None:
|
|
|
+ raise WorkbenchError("TASK_CONTRACT_INVALID", "Worker Task has no contract")
|
|
|
+ binding = await self._bindings.get_by_root(request.root_trace_id)
|
|
|
+ snapshot = await self._snapshots.get(
|
|
|
+ str(binding.input_snapshot_id), script_build_id=binding.script_build_id
|
|
|
+ )
|
|
|
+ semantic_ref_values: list[Any] = []
|
|
|
+ seen_decisions: set[str] = set()
|
|
|
+ for item in (*contract.input_decision_refs, *contract.comparison_decision_refs):
|
|
|
+ if item.decision_id in seen_decisions:
|
|
|
+ continue
|
|
|
+ seen_decisions.add(item.decision_id)
|
|
|
+ semantic_ref_values.append(item)
|
|
|
+ semantic_refs = tuple(semantic_ref_values)
|
|
|
+ accepted_values = [self._accepted_input_handle(item) for item in semantic_refs]
|
|
|
+ for index, item in enumerate(semantic_refs):
|
|
|
+ version = await self._artifacts.read_by_ref(
|
|
|
+ item.artifact_ref, script_build_id=binding.script_build_id
|
|
|
+ )
|
|
|
+ raw_ref = getattr(version.artifact, "raw_artifact_ref", None)
|
|
|
+ if isinstance(raw_ref, str) and raw_ref:
|
|
|
+ accepted_values[index]["image_handle"] = semantic_handle("image", raw_ref)
|
|
|
+ for child in request.accepted_child_results:
|
|
|
+ submission = child.get("submission")
|
|
|
+ if not isinstance(submission, Mapping):
|
|
|
+ continue
|
|
|
+ for raw in [
|
|
|
+ *submission.get("artifact_refs", ()),
|
|
|
+ *submission.get("evidence_refs", ()),
|
|
|
+ ]:
|
|
|
+ if not isinstance(raw, Mapping):
|
|
|
+ continue
|
|
|
+ accepted_values.append(
|
|
|
+ {
|
|
|
+ "decision_id": child.get("decision_id"),
|
|
|
+ "source_handle": _source_handle(
|
|
|
+ str(raw.get("uri", "")),
|
|
|
+ str(raw.get("digest") or raw.get("version") or ""),
|
|
|
+ ),
|
|
|
+ "task_kind": task_kind_from_context_refs(
|
|
|
+ tuple(child.get("task_spec", {}).get("context_refs", ()))
|
|
|
+ ),
|
|
|
+ "summary": raw.get("summary"),
|
|
|
+ }
|
|
|
+ )
|
|
|
+ accepted = tuple(accepted_values)
|
|
|
+ workspace = await self._workspace(request, contract)
|
|
|
+ workspace_digest = _digest(workspace)
|
|
|
+ paragraphs = tuple(
|
|
|
+ {
|
|
|
+ "paragraph_target_key": paragraph_target_key(
|
|
|
+ request.root_trace_id,
|
|
|
+ request.task_id,
|
|
|
+ request.attempt_id,
|
|
|
+ int(item["paragraph_id"]),
|
|
|
+ ),
|
|
|
+ "name": item.get("name"),
|
|
|
+ "paragraph_index": item.get("paragraph_index"),
|
|
|
+ "level": item.get("level"),
|
|
|
+ "content_range": item.get("content_range"),
|
|
|
+ "content_complete": all(
|
|
|
+ bool(str(item.get(field) or "").strip())
|
|
|
+ for field in (
|
|
|
+ "theme",
|
|
|
+ "form",
|
|
|
+ "function",
|
|
|
+ "feeling",
|
|
|
+ "description",
|
|
|
+ "full_description",
|
|
|
+ )
|
|
|
+ ),
|
|
|
+ }
|
|
|
+ for item in workspace.get("paragraphs", ())
|
|
|
+ )
|
|
|
+ revision = _digest(
|
|
|
+ {
|
|
|
+ "ledger": ledger.revision,
|
|
|
+ "workspace": workspace_digest,
|
|
|
+ "contract": contract.to_payload(),
|
|
|
+ }
|
|
|
+ )
|
|
|
+ latest = ledger.attempts.get(task.attempt_ids[-2]) if len(task.attempt_ids) > 1 else None
|
|
|
+ state = WorkerState(
|
|
|
+ state_revision=revision,
|
|
|
+ task=_semantic_contract(contract),
|
|
|
+ input_summary=_input_summary(snapshot),
|
|
|
+ accepted_inputs=accepted,
|
|
|
+ source_handles=accepted,
|
|
|
+ paragraph_targets=paragraphs,
|
|
|
+ workspace_summary={
|
|
|
+ "paragraph_count": len(workspace.get("paragraphs", ())),
|
|
|
+ "element_count": len(workspace.get("elements", ())),
|
|
|
+ "link_count": len(workspace.get("paragraph_element_links", ())),
|
|
|
+ "workspace_digest": workspace_digest,
|
|
|
+ },
|
|
|
+ latest_failure=(
|
|
|
+ latest.failure.to_dict()
|
|
|
+ if latest is not None and latest.failure is not None
|
|
|
+ else None
|
|
|
+ ),
|
|
|
+ )
|
|
|
+ _bounded_json(state.to_payload(), 32_000, "WorkerState")
|
|
|
+ return state
|
|
|
+
|
|
|
+ async def validator_state(self, request: RoleContextRequest) -> ValidatorState:
|
|
|
+ ledger = await self._task_store.load(request.root_trace_id)
|
|
|
+ task = ledger.tasks[request.task_id]
|
|
|
+ contract = await self._contract(request.root_trace_id, task)
|
|
|
+ if contract is None or request.artifact_snapshot is None:
|
|
|
+ raise WorkbenchError("TASK_CONTRACT_INVALID", "Validator context is incomplete")
|
|
|
+ binding = await self._bindings.get_by_root(request.root_trace_id)
|
|
|
+ refs = [
|
|
|
+ *request.artifact_snapshot.get("artifact_refs", ()),
|
|
|
+ *request.artifact_snapshot.get("evidence_refs", ()),
|
|
|
+ *(asdict(item.artifact_ref) for item in contract.input_decision_refs),
|
|
|
+ ]
|
|
|
+ handles: list[dict[str, Any]] = []
|
|
|
+ artifacts: list[dict[str, Any]] = []
|
|
|
+ seen_uris: set[str] = set()
|
|
|
+ for raw in refs:
|
|
|
+ uri = str(raw.get("uri", ""))
|
|
|
+ if not uri or uri in seen_uris:
|
|
|
+ continue
|
|
|
+ seen_uris.add(uri)
|
|
|
+ handle = _source_handle(uri, str(raw.get("digest") or raw.get("version") or ""))
|
|
|
+ handles.append(
|
|
|
+ {"evidence_handle": handle, "kind": raw.get("kind"), "summary": raw.get("summary")}
|
|
|
+ )
|
|
|
+ ref = _artifact_ref(raw)
|
|
|
+ version = await self._artifacts.read_by_ref(
|
|
|
+ ref, script_build_id=binding.script_build_id
|
|
|
+ )
|
|
|
+ artifacts.append(
|
|
|
+ {
|
|
|
+ "source_handle": handle,
|
|
|
+ "content": _protected_artifact_payload(version.artifact, handle),
|
|
|
+ }
|
|
|
+ )
|
|
|
+ artifact_payload = {"items": artifacts}
|
|
|
+ if len(_json(artifact_payload)) > 80_000:
|
|
|
+ raise WorkbenchError(
|
|
|
+ "CONTEXT_BUDGET_EXCEEDED", "Validator Artifact exceeds the protected role budget"
|
|
|
+ )
|
|
|
+ state = ValidatorState(
|
|
|
+ state_revision=_digest(
|
|
|
+ {
|
|
|
+ "ledger": ledger.revision,
|
|
|
+ "snapshot": request.artifact_snapshot,
|
|
|
+ "contract": contract.to_payload(),
|
|
|
+ }
|
|
|
+ ),
|
|
|
+ task=_semantic_contract(contract),
|
|
|
+ artifact=artifact_payload,
|
|
|
+ evidence_handles=tuple(handles),
|
|
|
+ accepted_closure=tuple(
|
|
|
+ self._accepted_input_handle(item) for item in contract.input_decision_refs
|
|
|
+ ),
|
|
|
+ )
|
|
|
+ metadata = state.to_payload() | {"artifact": {"item_count": len(artifacts)}}
|
|
|
+ _bounded_json(metadata, 16_000, "ValidatorState metadata")
|
|
|
+ return state
|
|
|
+
|
|
|
+ async def detail(
|
|
|
+ self,
|
|
|
+ *,
|
|
|
+ root_trace_id: str,
|
|
|
+ view: str,
|
|
|
+ handle: str | None = None,
|
|
|
+ known_revision: str | None = None,
|
|
|
+ cursor: int = 0,
|
|
|
+ ) -> Mapping[str, Any]:
|
|
|
+ ledger = await self._task_store.load(root_trace_id)
|
|
|
+ revision = f"ledger:{ledger.revision}"
|
|
|
+ if known_revision == revision:
|
|
|
+ return NotModified(revision).to_payload()
|
|
|
+ if view == "source":
|
|
|
+ if not handle:
|
|
|
+ raise WorkbenchError("INVALID_TOOL_ARGUMENT", "source view requires a handle")
|
|
|
+ binding = await self._bindings.get_by_root(root_trace_id)
|
|
|
+ accepted_attempts = {
|
|
|
+ item.attempt_id
|
|
|
+ for item in ledger.decisions.values()
|
|
|
+ if item.action is DecisionAction.ACCEPT and item.attempt_id is not None
|
|
|
+ }
|
|
|
+ selected = None
|
|
|
+ for attempt_id in accepted_attempts:
|
|
|
+ attempt = ledger.attempts.get(attempt_id)
|
|
|
+ if attempt is None or attempt.submission is None:
|
|
|
+ continue
|
|
|
+ for ref in [*attempt.submission.artifact_refs, *attempt.submission.evidence_refs]:
|
|
|
+ if _source_handle(ref.uri, ref.digest or ref.version or "") == handle:
|
|
|
+ selected = ref
|
|
|
+ break
|
|
|
+ if selected is not None:
|
|
|
+ break
|
|
|
+ if selected is None:
|
|
|
+ raise WorkbenchError("CHILD_DECISION_INVALID", "source handle is stale or unknown")
|
|
|
+ version = await self._artifacts.read_by_ref(
|
|
|
+ selected, script_build_id=binding.script_build_id
|
|
|
+ )
|
|
|
+ content = _json(_protected_artifact_payload(version.artifact, handle))
|
|
|
+ start = max(cursor, 0)
|
|
|
+ fragment = content[start : start + 7_000]
|
|
|
+ payload = {
|
|
|
+ "state_revision": revision,
|
|
|
+ "view": view,
|
|
|
+ "source_handle": handle,
|
|
|
+ "content_fragment": fragment,
|
|
|
+ "next_cursor": start + len(fragment)
|
|
|
+ if start + len(fragment) < len(content)
|
|
|
+ else None,
|
|
|
+ }
|
|
|
+ _bounded_json(payload, 8_000, "Workbench detail")
|
|
|
+ return payload
|
|
|
+ if view != "task_tree":
|
|
|
+ raise WorkbenchError("INVALID_TOOL_ARGUMENT", "unsupported Workbench detail view")
|
|
|
+ tasks = sorted(ledger.tasks.values(), key=lambda item: item.display_path)
|
|
|
+ page = tasks[max(cursor, 0) : max(cursor, 0) + 20]
|
|
|
+ tree_payload: dict[str, Any] = {
|
|
|
+ "state_revision": revision,
|
|
|
+ "view": view,
|
|
|
+ "items": [
|
|
|
+ {
|
|
|
+ "task_id": item.task_id,
|
|
|
+ "parent_task_id": item.parent_task_id,
|
|
|
+ "display_path": item.display_path,
|
|
|
+ "status": item.status.value,
|
|
|
+ "objective": item.current_spec.objective[:250],
|
|
|
+ }
|
|
|
+ for item in page
|
|
|
+ ],
|
|
|
+ "next_cursor": cursor + len(page) if cursor + len(page) < len(tasks) else None,
|
|
|
+ }
|
|
|
+ _bounded_json(tree_payload, 8_000, "Workbench detail")
|
|
|
+ return tree_payload
|
|
|
+
|
|
|
+ async def _contract(self, root_trace_id: str, task: Any) -> ScriptTaskContractV1 | None:
|
|
|
+ refs = [
|
|
|
+ item
|
|
|
+ for item in task.current_spec.context_refs
|
|
|
+ if item.startswith(TASK_CONTRACT_REF_PREFIX)
|
|
|
+ ]
|
|
|
+ if len(refs) != 1:
|
|
|
+ return None
|
|
|
+ return (await self._contracts.read(root_trace_id, refs[0])).contract
|
|
|
+
|
|
|
+ async def _workspace(
|
|
|
+ self, request: RoleContextRequest, contract: ScriptTaskContractV1
|
|
|
+ ) -> Mapping[str, Any]:
|
|
|
+ if self._candidates is None or contract.task_kind not in {
|
|
|
+ ScriptTaskKind.STRUCTURE,
|
|
|
+ ScriptTaskKind.PARAGRAPH,
|
|
|
+ ScriptTaskKind.ELEMENT_SET,
|
|
|
+ }:
|
|
|
+ return {}
|
|
|
+ try:
|
|
|
+ return cast(
|
|
|
+ Mapping[str, Any],
|
|
|
+ await self._candidates.read_attempt_workspace(
|
|
|
+ context={
|
|
|
+ "root_trace_id": request.root_trace_id,
|
|
|
+ "task_id": request.task_id,
|
|
|
+ "attempt_id": request.attempt_id,
|
|
|
+ "spec_version": request.spec_version,
|
|
|
+ }
|
|
|
+ ),
|
|
|
+ )
|
|
|
+ except Exception as exc:
|
|
|
+ if getattr(exc, "code", None) in {
|
|
|
+ "LEGACY_ARTIFACT_NOT_FOUND",
|
|
|
+ "TASK_KIND_PRESET_MISMATCH",
|
|
|
+ }:
|
|
|
+ return {}
|
|
|
+ raise
|
|
|
+
|
|
|
+ @staticmethod
|
|
|
+ def _accepted_decision(ledger: Any, task: Any) -> Any | None:
|
|
|
+ if not task.decision_ids:
|
|
|
+ return None
|
|
|
+ decision = ledger.decisions.get(task.decision_ids[-1])
|
|
|
+ if decision is not None and decision.action is DecisionAction.ACCEPT:
|
|
|
+ return decision
|
|
|
+ return None
|
|
|
+
|
|
|
+ @staticmethod
|
|
|
+ def _active_subgraph_ids(ledger: Any) -> set[str]:
|
|
|
+ included = {ledger.root_task_id}
|
|
|
+ for task in ledger.tasks.values():
|
|
|
+ if task.status not in _ACTIVE:
|
|
|
+ continue
|
|
|
+ current = task
|
|
|
+ while current is not None and current.task_id not in included:
|
|
|
+ included.add(current.task_id)
|
|
|
+ current = ledger.tasks.get(current.parent_task_id)
|
|
|
+ return {item for item in included if item is not None}
|
|
|
+
|
|
|
+ @staticmethod
|
|
|
+ def _task_summary(task: Any, contract: ScriptTaskContractV1 | None) -> dict[str, Any]:
|
|
|
+ return {
|
|
|
+ "task_id": task.task_id,
|
|
|
+ "parent_task_id": task.parent_task_id,
|
|
|
+ "display_path": task.display_path,
|
|
|
+ "status": task.status.value,
|
|
|
+ "task_kind": contract.task_kind.value if contract is not None else "root",
|
|
|
+ "scope_selector": _scope_selector(contract.scope_ref) if contract is not None else None,
|
|
|
+ "objective": task.current_spec.objective,
|
|
|
+ "attempt_count": len(task.attempt_ids),
|
|
|
+ "blocked_reason": task.blocked_reason,
|
|
|
+ }
|
|
|
+
|
|
|
+ @staticmethod
|
|
|
+ def _latest_failures(ledger: Any) -> tuple[Mapping[str, Any], ...]:
|
|
|
+ values: list[dict[str, Any]] = []
|
|
|
+ for attempt in reversed(list(ledger.attempts.values())):
|
|
|
+ if attempt.failure is None:
|
|
|
+ continue
|
|
|
+ values.append({"task_id": attempt.task_id, **attempt.failure.to_dict()})
|
|
|
+ if len(values) == 5:
|
|
|
+ break
|
|
|
+ return tuple(values)
|
|
|
+
|
|
|
+ @staticmethod
|
|
|
+ def _recent_changes(ledger: Any) -> tuple[Mapping[str, Any], ...]:
|
|
|
+ changes: list[dict[str, Any]] = []
|
|
|
+ for decision in list(ledger.decisions.values())[-5:]:
|
|
|
+ changes.append(
|
|
|
+ {
|
|
|
+ "kind": "decision",
|
|
|
+ "task_id": decision.task_id,
|
|
|
+ "action": decision.action.value,
|
|
|
+ }
|
|
|
+ )
|
|
|
+ for attempt in list(ledger.attempts.values())[-5:]:
|
|
|
+ changes.append(
|
|
|
+ {
|
|
|
+ "kind": "attempt",
|
|
|
+ "task_id": attempt.task_id,
|
|
|
+ "status": attempt.status.value,
|
|
|
+ "has_submission": attempt.submission is not None,
|
|
|
+ }
|
|
|
+ )
|
|
|
+ return tuple(changes[-8:])
|
|
|
+
|
|
|
+ @staticmethod
|
|
|
+ def _accepted_input_handle(item: Any) -> dict[str, Any]:
|
|
|
+ return {
|
|
|
+ "decision_id": item.decision_id,
|
|
|
+ "source_handle": _source_handle(
|
|
|
+ item.artifact_ref.uri, item.artifact_ref.digest or item.artifact_ref.version or ""
|
|
|
+ ),
|
|
|
+ "task_kind": item.expected_task_kind.value,
|
|
|
+ "scope_selector": _scope_selector(item.scope_ref),
|
|
|
+ "summary": item.artifact_ref.summary,
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+def _semantic_contract(contract: ScriptTaskContractV1) -> dict[str, Any]:
|
|
|
+ return {
|
|
|
+ "task_kind": contract.task_kind.value,
|
|
|
+ "scope_selector": _scope_selector(contract.scope_ref),
|
|
|
+ "intent_class": contract.intent_class.value,
|
|
|
+ "objective": contract.objective,
|
|
|
+ "goal_ids": list(contract.goal_ids),
|
|
|
+ "criteria": [item.to_payload() for item in contract.criteria],
|
|
|
+ "input_decision_ids": [item.decision_id for item in contract.input_decision_refs],
|
|
|
+ "comparison_decision_ids": [item.decision_id for item in contract.comparison_decision_refs],
|
|
|
+ "base_decision_id": next(
|
|
|
+ (
|
|
|
+ item.decision_id
|
|
|
+ for item in contract.input_decision_refs
|
|
|
+ if contract.base_artifact_ref is not None
|
|
|
+ and item.artifact_ref.uri == contract.base_artifact_ref.uri
|
|
|
+ ),
|
|
|
+ None,
|
|
|
+ ),
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+def _input_summary(snapshot: Any) -> dict[str, Any]:
|
|
|
+ topic = snapshot.topic.get("topic") if isinstance(snapshot.topic, Mapping) else None
|
|
|
+ return {
|
|
|
+ "snapshot_id": snapshot.snapshot_id,
|
|
|
+ "topic_id": snapshot.topic_id,
|
|
|
+ "topic": protect_model_value(
|
|
|
+ topic if isinstance(topic, Mapping) else snapshot.topic,
|
|
|
+ namespace=f"input:{snapshot.snapshot_id}",
|
|
|
+ ),
|
|
|
+ "account_name": snapshot.account.get("account_name"),
|
|
|
+ "persona_point_count": len(snapshot.persona_points),
|
|
|
+ "section_pattern_count": len(snapshot.section_patterns),
|
|
|
+ "strategies": [
|
|
|
+ {
|
|
|
+ "strategy_handle": strategy_handle(snapshot.snapshot_id, index, item),
|
|
|
+ "name": item.get("name") or item.get("strategy_name"),
|
|
|
+ "mode": item.get("mode"),
|
|
|
+ }
|
|
|
+ for index, item in enumerate(snapshot.strategies)
|
|
|
+ ],
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+def _phase(ledger: Any, root: Any, active_direction_id: int | None) -> int:
|
|
|
+ if root.blocked_reason == "PHASE_ONE_CAPABILITY_BOUNDARY":
|
|
|
+ return 1
|
|
|
+ if root.blocked_reason == "PHASE_TWO_CANDIDATE_PORTFOLIO_READY":
|
|
|
+ return 2
|
|
|
+ if any(
|
|
|
+ task_kind_from_context_refs(item.current_spec.context_refs)
|
|
|
+ == ScriptTaskKind.ROOT_DELIVERY.value
|
|
|
+ for item in ledger.tasks.values()
|
|
|
+ ):
|
|
|
+ return 3
|
|
|
+ return 2 if active_direction_id is not None else 1
|
|
|
+
|
|
|
+
|
|
|
+def _scope_selector(scope_ref: str) -> dict[str, Any]:
|
|
|
+ prefix = "script-build://scopes/"
|
|
|
+ path = scope_ref.removeprefix(prefix).split("/") if scope_ref.startswith(prefix) else []
|
|
|
+ return {"anchor": "mission", "path": [item for item in path if item]}
|
|
|
+
|
|
|
+
|
|
|
+def _source_handle(uri: str, immutable: str) -> str:
|
|
|
+ return semantic_handle("src", uri, immutable)
|
|
|
+
|
|
|
+
|
|
|
+def _artifact_ref(raw: Mapping[str, Any]) -> Any:
|
|
|
+ from agent.orchestration import ArtifactRef
|
|
|
+
|
|
|
+ return ArtifactRef.from_dict(dict(raw))
|
|
|
+
|
|
|
+
|
|
|
+def _artifact_payload(artifact: Any) -> Mapping[str, Any]:
|
|
|
+ content = getattr(artifact, "content_payload", None)
|
|
|
+ if callable(content):
|
|
|
+ return cast(Mapping[str, Any], content())
|
|
|
+ if is_dataclass(artifact):
|
|
|
+ return cast(Mapping[str, Any], asdict(cast(Any, artifact)))
|
|
|
+ return {"value": str(artifact)}
|
|
|
+
|
|
|
+
|
|
|
+def _protected_artifact_payload(artifact: Any, source_handle: str) -> Mapping[str, Any]:
|
|
|
+ """Expose semantics while replacing persisted identities and references with handles."""
|
|
|
+
|
|
|
+ return cast(
|
|
|
+ Mapping[str, Any],
|
|
|
+ protect_model_value(_artifact_payload(artifact), namespace=source_handle),
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+def _digest(value: Any) -> str:
|
|
|
+ return "sha256:" + sha256(_json(value).encode()).hexdigest()
|
|
|
+
|
|
|
+
|
|
|
+def _json(value: Any) -> str:
|
|
|
+ return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), default=str)
|
|
|
+
|
|
|
+
|
|
|
+def _json_safe(value: Any) -> Mapping[str, Any]:
|
|
|
+ return cast(Mapping[str, Any], json.loads(_json(value)))
|
|
|
+
|
|
|
+
|
|
|
+def _bounded_json(value: Any, limit: int, label: str) -> str:
|
|
|
+ encoded = _json(value)
|
|
|
+ if len(encoded) > limit:
|
|
|
+ raise WorkbenchError("CONTEXT_BUDGET_EXCEEDED", f"{label} exceeds {limit} characters")
|
|
|
+ return encoded
|
|
|
+
|
|
|
+
|
|
|
+__all__ = ["MissionWorkbench", "WorkbenchError"]
|