|
@@ -13,7 +13,16 @@ from hashlib import sha256
|
|
|
from typing import Any, cast
|
|
from typing import Any, cast
|
|
|
|
|
|
|
|
from agent.failures import ToolExecutionError
|
|
from agent.failures import ToolExecutionError
|
|
|
-from agent.orchestration import AgentRole, DecisionAction, RoleContextRequest, TaskStatus
|
|
|
|
|
|
|
+from agent.orchestration import (
|
|
|
|
|
+ AgentRole,
|
|
|
|
|
+ ArtifactRef,
|
|
|
|
|
+ DecisionAction,
|
|
|
|
|
+ RoleContextRequest,
|
|
|
|
|
+ TaskStatus,
|
|
|
|
|
+ ValidationContextBootstrap,
|
|
|
|
|
+ ValidationEvidenceGrant,
|
|
|
|
|
+)
|
|
|
|
|
+from agent.orchestration.validation_evidence import evidence_handle
|
|
|
|
|
|
|
|
from script_build_host.application.context_semantics import AdaptiveSemanticSelector
|
|
from script_build_host.application.context_semantics import AdaptiveSemanticSelector
|
|
|
from script_build_host.application.failure_policy import classify_script_failure
|
|
from script_build_host.application.failure_policy import classify_script_failure
|
|
@@ -44,8 +53,10 @@ from script_build_host.domain.ports import (
|
|
|
MissionBindingRepository,
|
|
MissionBindingRepository,
|
|
|
ScriptBusinessArtifactRepository,
|
|
ScriptBusinessArtifactRepository,
|
|
|
)
|
|
)
|
|
|
|
|
+from script_build_host.domain.task_capabilities import TaskCapabilityCatalog
|
|
|
from script_build_host.domain.task_contracts import (
|
|
from script_build_host.domain.task_contracts import (
|
|
|
ScriptTaskContractV1,
|
|
ScriptTaskContractV1,
|
|
|
|
|
+ ScriptTaskContractV2,
|
|
|
ScriptTaskKind,
|
|
ScriptTaskKind,
|
|
|
task_kind_from_context_refs,
|
|
task_kind_from_context_refs,
|
|
|
)
|
|
)
|
|
@@ -110,8 +121,6 @@ class ContextBroker:
|
|
|
self._receipt_store = receipt_store
|
|
self._receipt_store = receipt_store
|
|
|
self._receipt_lock = asyncio.Lock()
|
|
self._receipt_lock = asyncio.Lock()
|
|
|
self._cursor_codec = ContextCursorCodec(cursor_secret or secrets.token_bytes(32))
|
|
self._cursor_codec = ContextCursorCodec(cursor_secret or secrets.token_bytes(32))
|
|
|
- self._required_reads: dict[tuple[str, str, str], set[str]] = {}
|
|
|
|
|
- self._required_read_offsets: dict[tuple[str, str, str, str], int] = {}
|
|
|
|
|
self._authorized_artifact_refs: dict[
|
|
self._authorized_artifact_refs: dict[
|
|
|
tuple[str, str, str], dict[str, Mapping[str, Any]]
|
|
tuple[str, str, str], dict[str, Mapping[str, Any]]
|
|
|
] = {}
|
|
] = {}
|
|
@@ -140,6 +149,18 @@ class ContextBroker:
|
|
|
)
|
|
)
|
|
|
) from exc
|
|
) from exc
|
|
|
|
|
|
|
|
|
|
+ async def build_validation_context(
|
|
|
|
|
+ self, request: RoleContextRequest
|
|
|
|
|
+ ) -> ValidationContextBootstrap:
|
|
|
|
|
+ """Project evidence once; Coordinator becomes the durable authority."""
|
|
|
|
|
+
|
|
|
|
|
+ state, grants = await self._validator_projection(request)
|
|
|
|
|
+ return ValidationContextBootstrap(
|
|
|
|
|
+ role_context=_json_safe(state.to_payload()),
|
|
|
|
|
+ evidence_grants=grants,
|
|
|
|
|
+ required_handles=tuple(state.required_handles),
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
async def planner_state(
|
|
async def planner_state(
|
|
|
self,
|
|
self,
|
|
|
root_trace_id: str,
|
|
root_trace_id: str,
|
|
@@ -296,7 +317,7 @@ class ContextBroker:
|
|
|
},
|
|
},
|
|
|
phase_protocol=(await self._phase_protocol(ledger, phase)).to_payload(),
|
|
phase_protocol=(await self._phase_protocol(ledger, phase)).to_payload(),
|
|
|
)
|
|
)
|
|
|
- return _fit_planner_state(state)
|
|
|
|
|
|
|
+ return state
|
|
|
|
|
|
|
|
async def _phase_protocol(self, ledger: Any, phase: int) -> PhaseProtocolSnapshot:
|
|
async def _phase_protocol(self, ledger: Any, phase: int) -> PhaseProtocolSnapshot:
|
|
|
root = ledger.tasks[ledger.root_task_id]
|
|
root = ledger.tasks[ledger.root_task_id]
|
|
@@ -584,10 +605,7 @@ class ContextBroker:
|
|
|
8_000
|
|
8_000
|
|
|
if contract.task_kind
|
|
if contract.task_kind
|
|
|
in {
|
|
in {
|
|
|
- ScriptTaskKind.PATTERN_RETRIEVAL,
|
|
|
|
|
- ScriptTaskKind.DECODE_RETRIEVAL,
|
|
|
|
|
- ScriptTaskKind.EXTERNAL_RETRIEVAL,
|
|
|
|
|
- ScriptTaskKind.KNOWLEDGE_RETRIEVAL,
|
|
|
|
|
|
|
+ ScriptTaskKind.EVIDENCE_RETRIEVAL,
|
|
|
ScriptTaskKind.DIRECTION,
|
|
ScriptTaskKind.DIRECTION,
|
|
|
}
|
|
}
|
|
|
else 12_000
|
|
else 12_000
|
|
@@ -666,12 +684,15 @@ class ContextBroker:
|
|
|
else None
|
|
else None
|
|
|
),
|
|
),
|
|
|
)
|
|
)
|
|
|
- _bounded_context(
|
|
|
|
|
- state.to_payload(), char_limit=32_000, token_limit=token_budget, label="WorkerState"
|
|
|
|
|
- )
|
|
|
|
|
return state
|
|
return state
|
|
|
|
|
|
|
|
async def validator_state(self, request: RoleContextRequest) -> ValidatorState:
|
|
async def validator_state(self, request: RoleContextRequest) -> ValidatorState:
|
|
|
|
|
+ state, _ = await self._validator_projection(request)
|
|
|
|
|
+ return state
|
|
|
|
|
+
|
|
|
|
|
+ async def _validator_projection(
|
|
|
|
|
+ self, request: RoleContextRequest
|
|
|
|
|
+ ) -> tuple[ValidatorState, tuple[ValidationEvidenceGrant, ...]]:
|
|
|
ledger = await self._task_store.load(request.root_trace_id)
|
|
ledger = await self._task_store.load(request.root_trace_id)
|
|
|
task = ledger.tasks[request.task_id]
|
|
task = ledger.tasks[request.task_id]
|
|
|
contract = await self._contract(request.root_trace_id, task)
|
|
contract = await self._contract(request.root_trace_id, task)
|
|
@@ -698,69 +719,63 @@ class ContextBroker:
|
|
|
if resolver is not None and hasattr(resolver, "validation_evidence_refs"):
|
|
if resolver is not None and hasattr(resolver, "validation_evidence_refs"):
|
|
|
for item in await resolver.validation_evidence_refs(context=context):
|
|
for item in await resolver.validation_evidence_refs(context=context):
|
|
|
refs.append(asdict(item))
|
|
refs.append(asdict(item))
|
|
|
- handles: list[dict[str, Any]] = []
|
|
|
|
|
- artifacts: list[dict[str, Any]] = []
|
|
|
|
|
- seen_uris: set[str] = set()
|
|
|
|
|
|
|
+ grants_by_uri: dict[str, ValidationEvidenceGrant] = {}
|
|
|
|
|
+ raw_by_uri: dict[str, Mapping[str, Any]] = {}
|
|
|
for raw in refs:
|
|
for raw in refs:
|
|
|
uri = str(raw.get("uri", ""))
|
|
uri = str(raw.get("uri", ""))
|
|
|
- if not uri or uri in seen_uris:
|
|
|
|
|
|
|
+ if not uri:
|
|
|
continue
|
|
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
|
|
|
|
|
|
|
+ grant = ValidationEvidenceGrant(
|
|
|
|
|
+ artifact_ref=_artifact_ref(raw),
|
|
|
|
|
+ handle=evidence_handle(_artifact_ref(raw)),
|
|
|
|
|
+ source="host_validation_closure",
|
|
|
)
|
|
)
|
|
|
- artifacts.append(
|
|
|
|
|
- {
|
|
|
|
|
- "source_handle": handle,
|
|
|
|
|
- "content": _protected_artifact_payload(version.artifact, handle),
|
|
|
|
|
- }
|
|
|
|
|
- )
|
|
|
|
|
- self._authorized_artifact_refs[
|
|
|
|
|
- (request.root_trace_id, request.task_id, request.attempt_id)
|
|
|
|
|
- ] = {
|
|
|
|
|
- _source_handle(
|
|
|
|
|
- str(raw.get("uri", "")),
|
|
|
|
|
- str(raw.get("digest") or raw.get("version") or ""),
|
|
|
|
|
- ): raw
|
|
|
|
|
- for raw in refs
|
|
|
|
|
- if raw.get("uri")
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ prior = grants_by_uri.get(uri)
|
|
|
|
|
+ if prior is not None and prior.artifact_ref.digest != grant.artifact_ref.digest:
|
|
|
|
|
+ raise WorkbenchError(
|
|
|
|
|
+ "VALIDATION_EVIDENCE_CONFLICT",
|
|
|
|
|
+ "one evidence URI has multiple immutable digests",
|
|
|
|
|
+ )
|
|
|
|
|
+ grants_by_uri[uri] = grant
|
|
|
|
|
+ raw_by_uri[uri] = raw
|
|
|
|
|
+ grants = tuple(grants_by_uri.values())
|
|
|
|
|
+ loaded = await self._load_unique_versions(
|
|
|
|
|
+ tuple(raw_by_uri.values()),
|
|
|
|
|
+ script_build_id=binding.script_build_id,
|
|
|
|
|
+ )
|
|
|
|
|
+ handles = tuple(
|
|
|
|
|
+ {
|
|
|
|
|
+ "evidence_handle": evidence_handle(_artifact_ref(raw)),
|
|
|
|
|
+ "kind": raw.get("kind"),
|
|
|
|
|
+ "summary": raw.get("summary"),
|
|
|
|
|
+ }
|
|
|
|
|
+ for raw, _ in loaded
|
|
|
|
|
+ )
|
|
|
|
|
+ artifacts = tuple(
|
|
|
|
|
+ {
|
|
|
|
|
+ "source_handle": evidence_handle(_artifact_ref(raw)),
|
|
|
|
|
+ "content": _protected_artifact_payload(
|
|
|
|
|
+ version.artifact,
|
|
|
|
|
+ evidence_handle(_artifact_ref(raw)),
|
|
|
|
|
+ ),
|
|
|
|
|
+ }
|
|
|
|
|
+ for raw, version in loaded
|
|
|
|
|
+ )
|
|
|
cards = tuple(
|
|
cards = tuple(
|
|
|
self._artifact_card(
|
|
self._artifact_card(
|
|
|
version.artifact,
|
|
version.artifact,
|
|
|
- handle=_source_handle(
|
|
|
|
|
- str(raw.get("uri", "")),
|
|
|
|
|
- str(raw.get("digest") or raw.get("version") or ""),
|
|
|
|
|
- ),
|
|
|
|
|
|
|
+ handle=evidence_handle(_artifact_ref(raw)),
|
|
|
source_type=str(raw.get("kind") or "artifact"),
|
|
source_type=str(raw.get("kind") or "artifact"),
|
|
|
)
|
|
)
|
|
|
- for raw, version in await self._load_unique_versions(
|
|
|
|
|
- refs, script_build_id=binding.script_build_id
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ for raw, version in loaded
|
|
|
|
|
+ )
|
|
|
|
|
+ selected_cards = cards
|
|
|
|
|
+ used_tokens = estimate_context_tokens(
|
|
|
|
|
+ {"cards": [item.to_payload() for item in cards]}
|
|
|
)
|
|
)
|
|
|
- selected_cards, used_tokens, omitted = fit_context_cards(cards, 32_000)
|
|
|
|
|
full_artifact_payload = {"items": artifacts}
|
|
full_artifact_payload = {"items": artifacts}
|
|
|
full_artifact_tokens = estimate_context_tokens(full_artifact_payload)
|
|
full_artifact_tokens = estimate_context_tokens(full_artifact_payload)
|
|
|
- full_content_in_bootstrap = not omitted and full_artifact_tokens + used_tokens <= 32_000
|
|
|
|
|
- required_handles = () if full_content_in_bootstrap else tuple(card.handle for card in cards)
|
|
|
|
|
- read_key = (request.root_trace_id, request.task_id, request.attempt_id)
|
|
|
|
|
- if required_handles:
|
|
|
|
|
- self._required_reads[read_key] = set(required_handles)
|
|
|
|
|
- for handle in required_handles:
|
|
|
|
|
- self._required_read_offsets[(*read_key, handle)] = 0
|
|
|
|
|
- else:
|
|
|
|
|
- self._required_reads.pop(read_key, None)
|
|
|
|
|
- await self._record_required_context(
|
|
|
|
|
- request.root_trace_id,
|
|
|
|
|
- task_id=request.task_id,
|
|
|
|
|
- attempt_id=request.attempt_id,
|
|
|
|
|
- required_handles=required_handles,
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ required_handles: tuple[str, ...] = ()
|
|
|
bundle = ContextBundle(
|
|
bundle = ContextBundle(
|
|
|
revision=_digest({"ledger": ledger.revision, "refs": refs}),
|
|
revision=_digest({"ledger": ledger.revision, "refs": refs}),
|
|
|
cards=selected_cards,
|
|
cards=selected_cards,
|
|
@@ -778,10 +793,8 @@ class ContextBroker:
|
|
|
revision=_digest({"ledger": ledger.revision, "refs": refs}),
|
|
revision=_digest({"ledger": ledger.revision, "refs": refs}),
|
|
|
candidate_count=len(cards),
|
|
candidate_count=len(cards),
|
|
|
selected_count=len(selected_cards),
|
|
selected_count=len(selected_cards),
|
|
|
- estimated_tokens=(
|
|
|
|
|
- used_tokens + full_artifact_tokens if full_content_in_bootstrap else used_tokens
|
|
|
|
|
- ),
|
|
|
|
|
- omitted_count=(0 if full_content_in_bootstrap else len(cards)),
|
|
|
|
|
|
|
+ estimated_tokens=used_tokens + full_artifact_tokens,
|
|
|
|
|
+ omitted_count=0,
|
|
|
),
|
|
),
|
|
|
)
|
|
)
|
|
|
await self._emit_receipt(
|
|
await self._emit_receipt(
|
|
@@ -789,12 +802,11 @@ class ContextBroker:
|
|
|
role="validator",
|
|
role="validator",
|
|
|
task_kind=contract.task_kind.value,
|
|
task_kind=contract.task_kind.value,
|
|
|
receipt=bundle.receipt,
|
|
receipt=bundle.receipt,
|
|
|
- has_more=not full_content_in_bootstrap,
|
|
|
|
|
|
|
+ has_more=False,
|
|
|
)
|
|
)
|
|
|
artifact_payload = {
|
|
artifact_payload = {
|
|
|
- "items": artifacts if full_content_in_bootstrap else [],
|
|
|
|
|
- "cards": [card.to_payload() for card in selected_cards],
|
|
|
|
|
- "full_content_in_bootstrap": full_content_in_bootstrap,
|
|
|
|
|
|
|
+ "items": artifacts,
|
|
|
|
|
+ "full_content_in_bootstrap": True,
|
|
|
}
|
|
}
|
|
|
state = ValidatorState(
|
|
state = ValidatorState(
|
|
|
state_revision=_digest(
|
|
state_revision=_digest(
|
|
@@ -806,17 +818,14 @@ class ContextBroker:
|
|
|
),
|
|
),
|
|
|
task=_semantic_contract(contract),
|
|
task=_semantic_contract(contract),
|
|
|
artifact=artifact_payload,
|
|
artifact=artifact_payload,
|
|
|
- evidence_handles=tuple(handles),
|
|
|
|
|
|
|
+ evidence_handles=handles,
|
|
|
accepted_closure=tuple(
|
|
accepted_closure=tuple(
|
|
|
self._accepted_input_handle(item) for item in _context_decision_refs(contract)
|
|
self._accepted_input_handle(item) for item in _context_decision_refs(contract)
|
|
|
),
|
|
),
|
|
|
required_handles=required_handles,
|
|
required_handles=required_handles,
|
|
|
context_bundle=bundle.to_payload(),
|
|
context_bundle=bundle.to_payload(),
|
|
|
)
|
|
)
|
|
|
- metadata = state.to_payload() | {"artifact": {"item_count": len(artifacts)}}
|
|
|
|
|
- _bounded_json(metadata, 16_000, "ValidatorState metadata")
|
|
|
|
|
- _bounded_tokens(state.to_payload(), 32_000, "ValidatorState")
|
|
|
|
|
- return state
|
|
|
|
|
|
|
+ return state, grants
|
|
|
|
|
|
|
|
async def search_mission_context(
|
|
async def search_mission_context(
|
|
|
self,
|
|
self,
|
|
@@ -1074,18 +1083,6 @@ class ContextBroker:
|
|
|
raise WorkbenchError(code, str(exc)) from exc
|
|
raise WorkbenchError(code, str(exc)) from exc
|
|
|
fragment = _context_fragment(encoded, start=start, token_budget=1_500)
|
|
fragment = _context_fragment(encoded, start=start, token_budget=1_500)
|
|
|
next_offset = start + len(fragment)
|
|
next_offset = start + len(fragment)
|
|
|
- read_offset_key = (
|
|
|
|
|
- root_trace_id,
|
|
|
|
|
- task_id or "",
|
|
|
|
|
- attempt_id or "",
|
|
|
|
|
- handle,
|
|
|
|
|
- )
|
|
|
|
|
- expected_offset = self._required_read_offsets.get(read_offset_key)
|
|
|
|
|
- if section == "content" and expected_offset is not None and start != expected_offset:
|
|
|
|
|
- raise WorkbenchError(
|
|
|
|
|
- "CONTEXT_CURSOR_OUT_OF_SEQUENCE",
|
|
|
|
|
- "required context pages must be read once in order",
|
|
|
|
|
- )
|
|
|
|
|
next_cursor = (
|
|
next_cursor = (
|
|
|
self._cursor_codec.encode(
|
|
self._cursor_codec.encode(
|
|
|
root_key=root_trace_id,
|
|
root_key=root_trace_id,
|
|
@@ -1105,28 +1102,6 @@ class ContextBroker:
|
|
|
"next_cursor": next_cursor,
|
|
"next_cursor": next_cursor,
|
|
|
"exhausted": next_offset >= len(encoded),
|
|
"exhausted": next_offset >= len(encoded),
|
|
|
}
|
|
}
|
|
|
- if (
|
|
|
|
|
- payload["exhausted"]
|
|
|
|
|
- and section == "content"
|
|
|
|
|
- and task_id is not None
|
|
|
|
|
- and attempt_id is not None
|
|
|
|
|
- ):
|
|
|
|
|
- key = (root_trace_id, task_id, attempt_id)
|
|
|
|
|
- pending = self._required_reads.get(key)
|
|
|
|
|
- if pending is not None:
|
|
|
|
|
- self._required_read_offsets[read_offset_key] = next_offset
|
|
|
|
|
- pending.discard(handle)
|
|
|
|
|
- if not pending:
|
|
|
|
|
- self._required_reads.pop(key, None)
|
|
|
|
|
- self._required_read_offsets.pop(read_offset_key, None)
|
|
|
|
|
- await self._record_required_source_read(
|
|
|
|
|
- root_trace_id,
|
|
|
|
|
- task_id=task_id,
|
|
|
|
|
- attempt_id=attempt_id,
|
|
|
|
|
- handle=handle,
|
|
|
|
|
- )
|
|
|
|
|
- elif section == "content" and expected_offset is not None:
|
|
|
|
|
- self._required_read_offsets[read_offset_key] = next_offset
|
|
|
|
|
await self._emit_receipt(
|
|
await self._emit_receipt(
|
|
|
root_trace_id,
|
|
root_trace_id,
|
|
|
role=role,
|
|
role=role,
|
|
@@ -1149,61 +1124,78 @@ class ContextBroker:
|
|
|
_bounded_json(payload, 8_000, "Context detail page")
|
|
_bounded_json(payload, 8_000, "Context detail page")
|
|
|
return payload
|
|
return payload
|
|
|
|
|
|
|
|
- def assert_context_exhausted(
|
|
|
|
|
- self, *, root_trace_id: str, task_id: str, attempt_id: str
|
|
|
|
|
- ) -> None:
|
|
|
|
|
- pending = self._required_reads.get((root_trace_id, task_id, attempt_id), set())
|
|
|
|
|
- if pending:
|
|
|
|
|
- raise WorkbenchError(
|
|
|
|
|
- "CONTEXT_NOT_EXHAUSTED",
|
|
|
|
|
- f"Validator must finish {len(pending)} required context source(s)",
|
|
|
|
|
- )
|
|
|
|
|
-
|
|
|
|
|
- async def verify_context_exhausted(
|
|
|
|
|
- self, *, root_trace_id: str, task_id: str, attempt_id: str
|
|
|
|
|
- ) -> None:
|
|
|
|
|
- """Verify required reads from durable Trace receipts when available."""
|
|
|
|
|
|
|
+ async def read_validation_evidence(
|
|
|
|
|
+ self,
|
|
|
|
|
+ *,
|
|
|
|
|
+ root_trace_id: str,
|
|
|
|
|
+ validation_id: str,
|
|
|
|
|
+ artifact_ref: ArtifactRef,
|
|
|
|
|
+ handle: str,
|
|
|
|
|
+ section: str = "content",
|
|
|
|
|
+ cursor: str | None = None,
|
|
|
|
|
+ ) -> Mapping[str, Any]:
|
|
|
|
|
+ """Read a Coordinator-authorized immutable ref without local ACL state."""
|
|
|
|
|
|
|
|
- get_events = getattr(self._receipt_store, "get_events", None)
|
|
|
|
|
- if not callable(get_events):
|
|
|
|
|
- self.assert_context_exhausted(
|
|
|
|
|
- root_trace_id=root_trace_id, task_id=task_id, attempt_id=attempt_id
|
|
|
|
|
- )
|
|
|
|
|
- return
|
|
|
|
|
- events = await get_events(root_trace_id)
|
|
|
|
|
- required_event = next(
|
|
|
|
|
- (
|
|
|
|
|
- item
|
|
|
|
|
- for item in reversed(events)
|
|
|
|
|
- if item.get("event") == "context_broker_required_context"
|
|
|
|
|
- and item.get("task_id") == task_id
|
|
|
|
|
- and item.get("attempt_id") == attempt_id
|
|
|
|
|
- ),
|
|
|
|
|
- None,
|
|
|
|
|
|
|
+ if not _SEMANTIC_HANDLE.fullmatch(handle):
|
|
|
|
|
+ raise WorkbenchError("CONTEXT_HANDLE_FORMAT_INVALID", "invalid evidence handle")
|
|
|
|
|
+ if section not in {"content", "outline"}:
|
|
|
|
|
+ raise WorkbenchError("INVALID_TOOL_ARGUMENT", "unsupported context section")
|
|
|
|
|
+ binding = await self._bindings.get_by_root(root_trace_id)
|
|
|
|
|
+ version = await self._artifacts.read_by_ref(
|
|
|
|
|
+ artifact_ref, script_build_id=binding.script_build_id
|
|
|
|
|
+ )
|
|
|
|
|
+ content = protect_model_value(
|
|
|
|
|
+ _artifact_payload(version.artifact), namespace=handle
|
|
|
)
|
|
)
|
|
|
- if required_event is None:
|
|
|
|
|
- self.assert_context_exhausted(
|
|
|
|
|
- root_trace_id=root_trace_id, task_id=task_id, attempt_id=attempt_id
|
|
|
|
|
|
|
+ if section == "outline":
|
|
|
|
|
+ content = _outline(content)
|
|
|
|
|
+ encoded = _json(content)
|
|
|
|
|
+ revision = str(artifact_ref.digest or artifact_ref.version or handle)
|
|
|
|
|
+ filter_digest = _digest(
|
|
|
|
|
+ {
|
|
|
|
|
+ "handle": handle,
|
|
|
|
|
+ "section": section,
|
|
|
|
|
+ "validation_id": validation_id,
|
|
|
|
|
+ }
|
|
|
|
|
+ )
|
|
|
|
|
+ start = 0
|
|
|
|
|
+ if cursor is not None:
|
|
|
|
|
+ try:
|
|
|
|
|
+ decoded = self._cursor_codec.decode(
|
|
|
|
|
+ cursor,
|
|
|
|
|
+ expected_root_key=root_trace_id,
|
|
|
|
|
+ expected_revision=revision,
|
|
|
|
|
+ expected_view=f"validation:{validation_id}:{handle}:{section}",
|
|
|
|
|
+ expected_filter_digest=filter_digest,
|
|
|
|
|
+ )
|
|
|
|
|
+ start = int(decoded.last_sort_key[0])
|
|
|
|
|
+ except (ContextCursorError, ValueError) as exc:
|
|
|
|
|
+ raise WorkbenchError(
|
|
|
|
|
+ getattr(exc, "code", "CONTEXT_CURSOR_INVALID"), str(exc)
|
|
|
|
|
+ ) from exc
|
|
|
|
|
+ fragment = _context_fragment(encoded, start=start, token_budget=1_500)
|
|
|
|
|
+ next_offset = start + len(fragment)
|
|
|
|
|
+ next_cursor = (
|
|
|
|
|
+ self._cursor_codec.encode(
|
|
|
|
|
+ root_key=root_trace_id,
|
|
|
|
|
+ revision=revision,
|
|
|
|
|
+ view=f"validation:{validation_id}:{handle}:{section}",
|
|
|
|
|
+ filter_digest=filter_digest,
|
|
|
|
|
+ last_sort_key=(str(next_offset),),
|
|
|
)
|
|
)
|
|
|
- return
|
|
|
|
|
- required = set(required_event.get("required_source_keys") or ())
|
|
|
|
|
- completed = {
|
|
|
|
|
- str(item.get("source_key"))
|
|
|
|
|
- for item in events
|
|
|
|
|
- if int(item.get("event_id") or 0) > int(required_event.get("event_id") or 0)
|
|
|
|
|
- and item.get("event") == "context_broker_required_source_read"
|
|
|
|
|
- and item.get("task_id") == task_id
|
|
|
|
|
- and item.get("attempt_id") == attempt_id
|
|
|
|
|
|
|
+ if next_offset < len(encoded)
|
|
|
|
|
+ else None
|
|
|
|
|
+ )
|
|
|
|
|
+ payload = {
|
|
|
|
|
+ "state_revision": revision,
|
|
|
|
|
+ "source_handle": handle,
|
|
|
|
|
+ "section": section,
|
|
|
|
|
+ "content_fragment": fragment,
|
|
|
|
|
+ "next_cursor": next_cursor,
|
|
|
|
|
+ "exhausted": next_cursor is None,
|
|
|
}
|
|
}
|
|
|
- pending = required - completed
|
|
|
|
|
- if pending:
|
|
|
|
|
- raise WorkbenchError(
|
|
|
|
|
- "CONTEXT_NOT_EXHAUSTED",
|
|
|
|
|
- f"Validator must finish {len(pending)} required context source(s)",
|
|
|
|
|
- )
|
|
|
|
|
-
|
|
|
|
|
- def pending_required_context(self, *, root_trace_id: str, task_id: str, attempt_id: str) -> int:
|
|
|
|
|
- return len(self._required_reads.get((root_trace_id, task_id, attempt_id), ()))
|
|
|
|
|
|
|
+ _bounded_json(payload, 8_000, "Validation evidence page")
|
|
|
|
|
+ return payload
|
|
|
|
|
|
|
|
async def _accepted_decision_card(
|
|
async def _accepted_decision_card(
|
|
|
self,
|
|
self,
|
|
@@ -1672,64 +1664,7 @@ class ContextBroker:
|
|
|
for item in events
|
|
for item in events
|
|
|
if item.get("event") == "context_broker_receipt" and item.get("progress_key")
|
|
if item.get("event") == "context_broker_receipt" and item.get("progress_key")
|
|
|
}
|
|
}
|
|
|
- required = {
|
|
|
|
|
- str(key)
|
|
|
|
|
- for item in events
|
|
|
|
|
- if item.get("event") == "context_broker_required_context"
|
|
|
|
|
- for key in item.get("required_source_keys") or ()
|
|
|
|
|
- }
|
|
|
|
|
- completed = {
|
|
|
|
|
- str(item.get("source_key"))
|
|
|
|
|
- for item in events
|
|
|
|
|
- if item.get("event") == "context_broker_required_source_read"
|
|
|
|
|
- and item.get("source_key")
|
|
|
|
|
- }
|
|
|
|
|
- return {
|
|
|
|
|
- "unique_observations": sorted(progress_keys),
|
|
|
|
|
- "required_remaining": sorted(required - completed),
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- async def _record_required_context(
|
|
|
|
|
- self,
|
|
|
|
|
- root_trace_id: str,
|
|
|
|
|
- *,
|
|
|
|
|
- task_id: str,
|
|
|
|
|
- attempt_id: str,
|
|
|
|
|
- required_handles: Sequence[str],
|
|
|
|
|
- ) -> None:
|
|
|
|
|
- append = getattr(self._receipt_store, "append_event", None)
|
|
|
|
|
- if callable(append):
|
|
|
|
|
- await self._append_trace_event(
|
|
|
|
|
- root_trace_id,
|
|
|
|
|
- "context_broker_required_context",
|
|
|
|
|
- {
|
|
|
|
|
- "task_id": task_id,
|
|
|
|
|
- "attempt_id": attempt_id,
|
|
|
|
|
- "required_source_keys": [
|
|
|
|
|
- _digest({"source_handle": item}) for item in required_handles
|
|
|
|
|
- ],
|
|
|
|
|
- },
|
|
|
|
|
- )
|
|
|
|
|
-
|
|
|
|
|
- async def _record_required_source_read(
|
|
|
|
|
- self,
|
|
|
|
|
- root_trace_id: str,
|
|
|
|
|
- *,
|
|
|
|
|
- task_id: str,
|
|
|
|
|
- attempt_id: str,
|
|
|
|
|
- handle: str,
|
|
|
|
|
- ) -> None:
|
|
|
|
|
- append = getattr(self._receipt_store, "append_event", None)
|
|
|
|
|
- if callable(append):
|
|
|
|
|
- await self._append_trace_event(
|
|
|
|
|
- root_trace_id,
|
|
|
|
|
- "context_broker_required_source_read",
|
|
|
|
|
- {
|
|
|
|
|
- "task_id": task_id,
|
|
|
|
|
- "attempt_id": attempt_id,
|
|
|
|
|
- "source_key": _digest({"source_handle": handle}),
|
|
|
|
|
- },
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ return {"unique_observations": sorted(progress_keys)}
|
|
|
|
|
|
|
|
async def _append_trace_event(
|
|
async def _append_trace_event(
|
|
|
self, root_trace_id: str, event: str, payload: dict[str, Any]
|
|
self, root_trace_id: str, event: str, payload: dict[str, Any]
|
|
@@ -1862,6 +1797,11 @@ class MissionWorkbench:
|
|
|
async def build(self, request: RoleContextRequest) -> Mapping[str, Any]:
|
|
async def build(self, request: RoleContextRequest) -> Mapping[str, Any]:
|
|
|
return await self.broker.build(request)
|
|
return await self.broker.build(request)
|
|
|
|
|
|
|
|
|
|
+ async def build_validation_context(
|
|
|
|
|
+ self, request: RoleContextRequest
|
|
|
|
|
+ ) -> ValidationContextBootstrap:
|
|
|
|
|
+ return await self.broker.build_validation_context(request)
|
|
|
|
|
+
|
|
|
async def planner_state(self, root_trace_id: str, **kwargs: Any) -> PlannerState | NotModified:
|
|
async def planner_state(self, root_trace_id: str, **kwargs: Any) -> PlannerState | NotModified:
|
|
|
return await self.broker.planner_state(root_trace_id, **kwargs)
|
|
return await self.broker.planner_state(root_trace_id, **kwargs)
|
|
|
|
|
|
|
@@ -1880,36 +1820,21 @@ class MissionWorkbench:
|
|
|
async def read_mission_context(self, **kwargs: Any) -> Mapping[str, Any]:
|
|
async def read_mission_context(self, **kwargs: Any) -> Mapping[str, Any]:
|
|
|
return await self.broker.read_mission_context(**kwargs)
|
|
return await self.broker.read_mission_context(**kwargs)
|
|
|
|
|
|
|
|
- def assert_context_exhausted(self, **kwargs: Any) -> None:
|
|
|
|
|
- self.broker.assert_context_exhausted(**kwargs)
|
|
|
|
|
-
|
|
|
|
|
- async def verify_context_exhausted(self, **kwargs: Any) -> None:
|
|
|
|
|
- await self.broker.verify_context_exhausted(**kwargs)
|
|
|
|
|
-
|
|
|
|
|
- def pending_required_context(self, **kwargs: Any) -> int:
|
|
|
|
|
- return self.broker.pending_required_context(**kwargs)
|
|
|
|
|
-
|
|
|
|
|
|
|
+ async def read_validation_evidence(self, **kwargs: Any) -> Mapping[str, Any]:
|
|
|
|
|
+ return await self.broker.read_validation_evidence(**kwargs)
|
|
|
|
|
|
|
|
def _semantic_contract(contract: ScriptTaskContractV1) -> dict[str, Any]:
|
|
def _semantic_contract(contract: ScriptTaskContractV1) -> dict[str, Any]:
|
|
|
- return {
|
|
|
|
|
|
|
+ value = {
|
|
|
"task_kind": contract.task_kind.value,
|
|
"task_kind": contract.task_kind.value,
|
|
|
- "scope_selector": _scope_selector(contract.scope_ref),
|
|
|
|
|
- "intent_class": contract.intent_class.value,
|
|
|
|
|
"objective": contract.objective,
|
|
"objective": contract.objective,
|
|
|
"goal_ids": list(contract.goal_ids),
|
|
"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,
|
|
|
|
|
- ),
|
|
|
|
|
|
|
+ "decision_ids": [
|
|
|
|
|
+ item.decision_id for item in _context_decision_refs(contract)
|
|
|
|
|
+ ],
|
|
|
}
|
|
}
|
|
|
|
|
+ if isinstance(contract, ScriptTaskContractV2):
|
|
|
|
|
+ value["branch_ref"] = contract.branch_ref
|
|
|
|
|
+ return value
|
|
|
|
|
|
|
|
|
|
|
|
|
def _context_decision_refs(contract: ScriptTaskContractV1) -> tuple[Any, ...]:
|
|
def _context_decision_refs(contract: ScriptTaskContractV1) -> tuple[Any, ...]:
|
|
@@ -1942,25 +1867,17 @@ def _input_summary(snapshot: Any) -> dict[str, Any]:
|
|
|
|
|
|
|
|
|
|
|
|
|
def required_context_source_types(task_kind: ScriptTaskKind) -> tuple[str, ...]:
|
|
def required_context_source_types(task_kind: ScriptTaskKind) -> tuple[str, ...]:
|
|
|
- return {
|
|
|
|
|
- ScriptTaskKind.DIRECTION: ("topic", "persona", "section_pattern", "strategy"),
|
|
|
|
|
- ScriptTaskKind.STRUCTURE: ("direction", "section_pattern"),
|
|
|
|
|
- ScriptTaskKind.PARAGRAPH: ("direction", "structure", "persona"),
|
|
|
|
|
- ScriptTaskKind.ELEMENT_SET: ("paragraph",),
|
|
|
|
|
- ScriptTaskKind.COMPARE: (),
|
|
|
|
|
- ScriptTaskKind.ROOT_DELIVERY: ("direction", "candidate-portfolio", "structured-script"),
|
|
|
|
|
- }.get(task_kind, ("topic",))
|
|
|
|
|
|
|
+ try:
|
|
|
|
|
+ return TaskCapabilityCatalog().get(task_kind).context_source_types
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ return ("topic",)
|
|
|
|
|
|
|
|
|
|
|
|
|
def _persona_limit(task_kind: ScriptTaskKind) -> int:
|
|
def _persona_limit(task_kind: ScriptTaskKind) -> int:
|
|
|
- return {
|
|
|
|
|
- ScriptTaskKind.DIRECTION: 32,
|
|
|
|
|
- ScriptTaskKind.PATTERN_RETRIEVAL: 16,
|
|
|
|
|
- ScriptTaskKind.STRUCTURE: 24,
|
|
|
|
|
- ScriptTaskKind.PARAGRAPH: 12,
|
|
|
|
|
- ScriptTaskKind.ELEMENT_SET: 12,
|
|
|
|
|
- ScriptTaskKind.ROOT_DELIVERY: 16,
|
|
|
|
|
- }.get(task_kind, 16)
|
|
|
|
|
|
|
+ try:
|
|
|
|
|
+ return TaskCapabilityCatalog().get(task_kind).persona_limit
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ return 16
|
|
|
|
|
|
|
|
|
|
|
|
|
def _select_persona_entries(
|
|
def _select_persona_entries(
|
|
@@ -2286,47 +2203,6 @@ def _bounded_tokens(value: Any, limit: int, label: str) -> None:
|
|
|
raise WorkbenchError("CONTEXT_BUDGET_EXCEEDED", f"{label} exceeds {limit} tokens")
|
|
raise WorkbenchError("CONTEXT_BUDGET_EXCEEDED", f"{label} exceeds {limit} tokens")
|
|
|
|
|
|
|
|
|
|
|
|
|
-def _bounded_context(value: Any, *, char_limit: int, token_limit: int, label: str) -> None:
|
|
|
|
|
- _bounded_json(value, char_limit, label)
|
|
|
|
|
- _bounded_tokens(value, token_limit, label)
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
-def _fit_planner_state(state: PlannerState) -> PlannerState:
|
|
|
|
|
- candidates = (
|
|
|
|
|
- state,
|
|
|
|
|
- replace(
|
|
|
|
|
- state,
|
|
|
|
|
- active_subgraph=state.active_subgraph[:8],
|
|
|
|
|
- accepted_frontier=state.accepted_frontier[:4],
|
|
|
|
|
- active_operations=state.active_operations[:4],
|
|
|
|
|
- latest_failures=state.latest_failures[:4],
|
|
|
|
|
- changes=state.changes[:4],
|
|
|
|
|
- active_has_more=state.active_has_more or len(state.active_subgraph) > 8,
|
|
|
|
|
- accepted_has_more=state.accepted_has_more or len(state.accepted_frontier) > 4,
|
|
|
|
|
- ),
|
|
|
|
|
- replace(
|
|
|
|
|
- state,
|
|
|
|
|
- active_subgraph=state.active_subgraph[:3],
|
|
|
|
|
- accepted_frontier=state.accepted_frontier[:2],
|
|
|
|
|
- active_operations=state.active_operations[:2],
|
|
|
|
|
- latest_failures=state.latest_failures[:2],
|
|
|
|
|
- changes=(),
|
|
|
|
|
- active_has_more=state.active_has_more or len(state.active_subgraph) > 3,
|
|
|
|
|
- accepted_has_more=state.accepted_has_more or len(state.accepted_frontier) > 2,
|
|
|
|
|
- context_bundle={
|
|
|
|
|
- **dict(state.context_bundle),
|
|
|
|
|
- "cards": [],
|
|
|
|
|
- "compacted": True,
|
|
|
|
|
- },
|
|
|
|
|
- ),
|
|
|
|
|
- )
|
|
|
|
|
- for candidate in candidates:
|
|
|
|
|
- payload = candidate.to_payload()
|
|
|
|
|
- if len(_json(payload)) <= 24_000 and estimate_context_tokens(payload) <= 6_000:
|
|
|
|
|
- return candidate
|
|
|
|
|
- raise WorkbenchError("CONTEXT_BUDGET_EXCEEDED", "minimal PlannerState exceeds budget")
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
def _context_fragment(value: str, *, start: int, token_budget: int) -> str:
|
|
def _context_fragment(value: str, *, start: int, token_budget: int) -> str:
|
|
|
"""Return the largest bounded fragment without splitting context by byte size."""
|
|
"""Return the largest bounded fragment without splitting context by byte size."""
|
|
|
|
|
|