918 lines
27 KiB
Python
918 lines
27 KiB
Python
"""Public V3 contracts for the embedded EvoScientist Web model runtime.
|
|
|
|
The host can provide identity, storage and durable event services through
|
|
these DTOs and protocols. Provider connection details never cross this
|
|
boundary.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import time
|
|
import uuid
|
|
from collections.abc import AsyncIterator, Mapping, Sequence
|
|
from dataclasses import asdict, dataclass, field, fields
|
|
from typing import Any, Literal, Protocol, TypeVar
|
|
|
|
from .crypto import (
|
|
HmacKeyRing,
|
|
KeyMaterial,
|
|
canonical_json_v1,
|
|
hmac_id,
|
|
sign_contract,
|
|
verify_contract,
|
|
)
|
|
|
|
CONTRACT_VERSION = 3
|
|
ADMIN_CONTROL_VERSION = 2
|
|
GATEWAY_ISSUER = "ai4sci-gateway"
|
|
EVO_ISSUER = "evoscientist-runtime"
|
|
GATEWAY_AUDIENCE = EVO_ISSUER
|
|
EVO_AUDIENCE = GATEWAY_ISSUER
|
|
MAX_CLOCK_SKEW_MS = 5_000
|
|
MAX_CONTRACT_TTL_MS = 120_000
|
|
MAX_ADMIN_TTL_MS = 60_000
|
|
|
|
_GATEWAY_GRANT_INFO = "ai4sci/gateway-to-evo-grant/v3"
|
|
_EVO_QUOTE_INFO = "ai4sci/evo-to-gateway-quote/v3"
|
|
_INPUT_DIGEST_INFO = "ai4sci/agent-input-digest/v3"
|
|
_PREPARED_SNAPSHOT_INFO = "ai4sci/prepared-snapshot-digest/v3"
|
|
_PREPARED_INPUT_INFO = "ai4sci/prepared-input-digest/v3"
|
|
_TOOL_REGISTRY_INFO = "ai4sci/tool-registry-snapshot/v3"
|
|
_ADMIN_CONFIG_INFO = "ai4sci/admin-config/v2"
|
|
|
|
|
|
class EvoRuntimeError(RuntimeError):
|
|
"""A stable error whose code may be projected to the host."""
|
|
|
|
def __init__(
|
|
self,
|
|
code: str,
|
|
message: str | None = None,
|
|
*,
|
|
details: Sequence[Mapping[str, Any]] = (),
|
|
) -> None:
|
|
super().__init__(message or code)
|
|
self.code = code
|
|
self.details = tuple(dict(item) for item in details)
|
|
|
|
def __repr__(self) -> str:
|
|
# LangGraph persists task failures using repr(exc). Keep that snapshot
|
|
# machine-readable without serializing provider messages or details.
|
|
return f"{type(self).__name__}(code={self.code!r})"
|
|
|
|
|
|
def now_ms() -> int:
|
|
return time.time_ns() // 1_000_000
|
|
|
|
|
|
def _unsigned(value: Any) -> dict[str, Any]:
|
|
payload = asdict(value)
|
|
payload.pop("signature", None)
|
|
if payload.get("predecessor_owner_epoch") == 0:
|
|
payload.pop("predecessor_owner_epoch")
|
|
for name in ("continuation_pending_hash", "continuation_decision_hash"):
|
|
if payload.get(name) == "":
|
|
payload.pop(name)
|
|
for name in ("execution_id", "predecessor_execution_id", "predecessor_checkpoint_id"):
|
|
if payload.get(name) == "":
|
|
payload.pop(name)
|
|
return payload
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class RoutePreparationGrant:
|
|
issuer: str
|
|
audience: str
|
|
grant_id: str
|
|
request_id: str
|
|
turn_id: str
|
|
thread_id: str
|
|
subject_id: str
|
|
requested_model_ref: str
|
|
plan: str
|
|
roles: tuple[str, ...]
|
|
requires_vision: bool
|
|
reasoning_effort: str
|
|
title_policy: Literal["disabled", "best_effort"]
|
|
gateway_input_digest: str
|
|
checkpoint_thread_id: str
|
|
checkpoint_snapshot_id: str
|
|
turn_fencing_token: int
|
|
issued_at: int
|
|
expires_at: int
|
|
key_id: str
|
|
signature: str
|
|
contract_version: int = CONTRACT_VERSION
|
|
predecessor_execution_id: str = ""
|
|
predecessor_checkpoint_id: str = ""
|
|
predecessor_owner_epoch: int = 0
|
|
continuation_pending_hash: str = ""
|
|
continuation_decision_hash: str = ""
|
|
|
|
def unsigned_payload(self) -> dict[str, Any]:
|
|
return _unsigned(self)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class RouteIdentity:
|
|
config_revision: int
|
|
config_identity_key_id: str
|
|
purpose: str
|
|
route_selector_id: str
|
|
route_fingerprint: str
|
|
provider_id: str
|
|
endpoint_name: str
|
|
model_id: str
|
|
protocol: str
|
|
api_mode: str
|
|
tool_call_transport: str
|
|
route_semantics_hash: str
|
|
billing_sku: str
|
|
pricing_revision: str
|
|
quote_id: str
|
|
|
|
@property
|
|
def route_key(self) -> str:
|
|
return ":".join(
|
|
(
|
|
self.provider_id,
|
|
self.endpoint_name,
|
|
self.model_id,
|
|
self.api_mode,
|
|
self.tool_call_transport,
|
|
)
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class PricingQuote:
|
|
billing_sku: str
|
|
pricing_revision: str
|
|
currency: str
|
|
unit_scale: int
|
|
input_microunits_per_million: int
|
|
cached_input_microunits_per_million: int
|
|
output_microunits_per_million: int
|
|
quote_id: str
|
|
multiplier: str = "1"
|
|
|
|
@property
|
|
def cached_microunits_per_million(self) -> int:
|
|
return self.cached_input_microunits_per_million
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class RouteCallBound:
|
|
route_identity: RouteIdentity
|
|
max_output_tokens: int
|
|
payload_input_hard_cap: int
|
|
billable_input_cap: int
|
|
protocol_margin_tokens: int
|
|
attempt_reserve_microunits: int
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class PreparedRunQuote:
|
|
issuer: str
|
|
audience: str
|
|
preparation_id: str
|
|
request_id: str
|
|
turn_id: str
|
|
thread_id: str
|
|
subject_id: str
|
|
requested_model_ref: str
|
|
plan: str
|
|
roles: tuple[str, ...]
|
|
requires_vision: bool
|
|
reasoning_effort: str
|
|
title_policy: Literal["disabled", "best_effort"]
|
|
gateway_input_digest: str
|
|
prepared_snapshot_digest: str
|
|
prepared_input_digest: str
|
|
config_revision: int
|
|
catalog_revision: int
|
|
enabled_purposes: tuple[str, ...]
|
|
purpose_routes: Mapping[str, Mapping[str, Any]]
|
|
purpose_route_call_bounds: Mapping[str, tuple[RouteCallBound, ...]]
|
|
purpose_attempt_limits: Mapping[str, int]
|
|
total_max_attempts: int
|
|
pricing_quotes: Mapping[str, PricingQuote]
|
|
quote_ids: tuple[str, ...]
|
|
provider_run_reserve_microunits: int
|
|
checkpoint_snapshot_id: str
|
|
tool_registry_snapshot_id: str
|
|
turn_fencing_token: int
|
|
route_semantics_hashes: tuple[str, ...]
|
|
issued_at: int
|
|
expires_at: int
|
|
key_id: str
|
|
signature: str
|
|
contract_version: int = CONTRACT_VERSION
|
|
execution_id: str = ""
|
|
|
|
def unsigned_payload(self) -> dict[str, Any]:
|
|
return _unsigned(self)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class AdmissionGrant:
|
|
issuer: str
|
|
audience: str
|
|
grant_id: str
|
|
preparation_id: str
|
|
request_id: str
|
|
turn_id: str
|
|
thread_id: str
|
|
subject_id: str
|
|
requested_model_ref: str
|
|
plan: str
|
|
roles: tuple[str, ...]
|
|
requires_vision: bool
|
|
reasoning_effort: str
|
|
title_policy: Literal["disabled", "best_effort"]
|
|
gateway_input_digest: str
|
|
prepared_snapshot_digest: str
|
|
prepared_input_digest: str
|
|
config_revision: int
|
|
catalog_revision: int
|
|
purpose_attempt_limits: Mapping[str, int]
|
|
total_max_attempts: int
|
|
checkpoint_snapshot_id: str
|
|
tool_registry_snapshot_id: str
|
|
turn_fencing_token: int
|
|
admission_snapshot_id: str
|
|
admission_id: str
|
|
hold_id: str
|
|
billing_fencing_token: int
|
|
provider_run_reserve_microunits: int
|
|
billing_policy_version: str
|
|
issued_at: int
|
|
expires_at: int
|
|
key_id: str
|
|
signature: str
|
|
contract_version: int = CONTRACT_VERSION
|
|
|
|
def unsigned_payload(self) -> dict[str, Any]:
|
|
return _unsigned(self)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class VerifiedModelSubject:
|
|
issuer: str
|
|
audience: str
|
|
grant_id: str
|
|
subject_id: str
|
|
plan: str
|
|
roles: tuple[str, ...]
|
|
issued_at: int
|
|
expires_at: int
|
|
key_id: str
|
|
signature: str
|
|
contract_version: int = CONTRACT_VERSION
|
|
|
|
def unsigned_payload(self) -> dict[str, Any]:
|
|
return _unsigned(self)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class AdminConfigGrant:
|
|
issuer: str
|
|
audience: str
|
|
grant_id: str
|
|
subject_id: str
|
|
action: str
|
|
operation_id: str
|
|
request_digest: str
|
|
issued_at: int
|
|
expires_at: int
|
|
key_id: str
|
|
signature: str
|
|
contract_version: int = CONTRACT_VERSION
|
|
|
|
def unsigned_payload(self) -> dict[str, Any]:
|
|
return _unsigned(self)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class GetModelConfigRequest:
|
|
operation_id: str
|
|
admin_grant: AdminConfigGrant
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class GetModelConfigResult:
|
|
operation_id: str
|
|
config_revision: int
|
|
redacted_config: Mapping[str, Any]
|
|
catalog_projection: Mapping[str, Any]
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class ConcreteRouteProposal:
|
|
route: Mapping[str, str]
|
|
route_semantics_hash: str
|
|
endpoint_fingerprint: str
|
|
required_probe_kinds: tuple[str, ...]
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class ValidateCandidateConfigRequest:
|
|
operation_id: str
|
|
expected_revision: int
|
|
payload: Mapping[str, Any]
|
|
admin_grant: AdminConfigGrant
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class ValidateCandidateConfigResult:
|
|
operation_id: str
|
|
target_revision: int
|
|
config_identity_key_id: str
|
|
proposal_hash: str
|
|
concrete_routes: tuple[ConcreteRouteProposal, ...]
|
|
expires_at: int
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class ProbeCandidateRouteRequest:
|
|
operation_id: str
|
|
proposal_hash: str
|
|
route_semantics_hash: str
|
|
probe_kind: Literal["connectivity", "tool_protocol"]
|
|
admin_grant: AdminConfigGrant
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class ProbeCandidateRouteResult:
|
|
operation_id: str
|
|
evidence_id: str
|
|
route_semantics_hash: str
|
|
connectivity: str
|
|
tool_capability: str
|
|
adapter_revision: str
|
|
fixture_digest: str
|
|
expires_at: int
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class CommitModelConfigRequest:
|
|
operation_id: str
|
|
expected_revision: int
|
|
proposal_hash: str
|
|
payload: Mapping[str, Any]
|
|
evidence_ids: tuple[str, ...]
|
|
admin_grant: AdminConfigGrant
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class CommitModelConfigResult:
|
|
operation_id: str
|
|
config_revision: int
|
|
committed_at: int
|
|
redacted_diff: Mapping[str, Any]
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class CreateProposalRequest:
|
|
operation_id: str
|
|
expected_active_revision: int
|
|
admin_grant: AdminConfigGrant
|
|
admin_control_version: int = ADMIN_CONTROL_VERSION
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class UpdateProposalRequest:
|
|
operation_id: str
|
|
proposal_id: str
|
|
expected_state_version: int
|
|
expected_draft_etag: str
|
|
draft_payload: Mapping[str, Any]
|
|
admin_grant: AdminConfigGrant
|
|
admin_control_version: int = ADMIN_CONTROL_VERSION
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class ValidateProposalRequest:
|
|
operation_id: str
|
|
proposal_id: str
|
|
expected_state_version: int
|
|
expected_draft_etag: str
|
|
admin_grant: AdminConfigGrant
|
|
admin_control_version: int = ADMIN_CONTROL_VERSION
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class ProbeProposalRequest:
|
|
operation_id: str
|
|
proposal_id: str
|
|
validated_digest: str
|
|
route_semantics_hash: str
|
|
probe_kind: str
|
|
admin_grant: AdminConfigGrant
|
|
admin_control_version: int = ADMIN_CONTROL_VERSION
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class CommitProposalRequest:
|
|
operation_id: str
|
|
proposal_id: str
|
|
expected_active_revision: int
|
|
expected_state_version: int
|
|
expected_draft_etag: str
|
|
validated_digest: str
|
|
evidence_ids: tuple[str, ...]
|
|
admin_grant: AdminConfigGrant
|
|
admin_control_version: int = ADMIN_CONTROL_VERSION
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class CancelProposalRequest:
|
|
operation_id: str
|
|
proposal_id: str
|
|
expected_state_version: int
|
|
admin_grant: AdminConfigGrant
|
|
admin_control_version: int = ADMIN_CONTROL_VERSION
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class RollbackConfigRequest:
|
|
operation_id: str
|
|
expected_active_revision: int
|
|
target_revision: int
|
|
admin_grant: AdminConfigGrant
|
|
admin_control_version: int = ADMIN_CONTROL_VERSION
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class DiscoverProviderModelsRequest:
|
|
operation_id: str
|
|
proposal_id: str
|
|
provider_id: str
|
|
admin_grant: AdminConfigGrant
|
|
admin_control_version: int = ADMIN_CONTROL_VERSION
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class DiscoverProviderModelsResult:
|
|
operation_id: str
|
|
proposal_id: str
|
|
provider_id: str
|
|
source: str
|
|
discovered_at: int
|
|
models: tuple[Mapping[str, str], ...]
|
|
admin_control_version: int = ADMIN_CONTROL_VERSION
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class AdminProposalResult:
|
|
operation_id: str
|
|
active_revision: int
|
|
proposal_id: str
|
|
state: str
|
|
state_version: int
|
|
draft_etag: str
|
|
base_revision: int
|
|
target_revision: int
|
|
expires_at: int
|
|
validated_digest: str | None = None
|
|
routes: tuple[Mapping[str, Any], ...] = ()
|
|
evidence_ids: tuple[str, ...] = ()
|
|
committed_at: int | None = None
|
|
draft_payload: Mapping[str, Any] | None = None
|
|
migration_report: Mapping[str, Any] | None = None
|
|
admin_control_version: int = ADMIN_CONTROL_VERSION
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class RollbackConfigResult:
|
|
operation_id: str
|
|
previous_active_revision: int
|
|
active_revision: int
|
|
target_revision: int
|
|
rolled_back_at: int
|
|
admin_control_version: int = ADMIN_CONTROL_VERSION
|
|
|
|
|
|
class AdmissionGrantVerifier(Protocol):
|
|
def require_preparation(self, grant: RoutePreparationGrant) -> None: ...
|
|
|
|
def require_admission(self, grant: AdmissionGrant) -> None: ...
|
|
|
|
def verify_subject(self, subject: VerifiedModelSubject) -> bool: ...
|
|
|
|
|
|
class AdminConfigGrantVerifier(Protocol):
|
|
def require_admin(self, grant: AdminConfigGrant) -> None: ...
|
|
|
|
|
|
_ContractT = TypeVar(
|
|
"_ContractT",
|
|
RoutePreparationGrant,
|
|
PreparedRunQuote,
|
|
AdmissionGrant,
|
|
VerifiedModelSubject,
|
|
AdminConfigGrant,
|
|
)
|
|
|
|
|
|
class HmacGrantAuthority:
|
|
"""Purpose-separated V3 contract signer with current/previous key support."""
|
|
|
|
def __init__(
|
|
self,
|
|
secret: str | bytes,
|
|
key_id: str = "runtime-current",
|
|
*,
|
|
previous_secret: str | bytes | None = None,
|
|
previous_key_id: str | None = None,
|
|
) -> None:
|
|
previous = None
|
|
if (previous_secret is None) != (previous_key_id is None):
|
|
raise ValueError("previous secret and key id must be configured together")
|
|
if previous_secret is not None and previous_key_id is not None:
|
|
previous = KeyMaterial.create(previous_key_id, previous_secret)
|
|
self.key_ring = HmacKeyRing(KeyMaterial.create(key_id, secret), previous)
|
|
|
|
def sign_preparation(self, **kwargs: Any) -> RoutePreparationGrant:
|
|
return self._sign(
|
|
RoutePreparationGrant, _GATEWAY_GRANT_INFO, gateway=True, **kwargs
|
|
)
|
|
|
|
def sign_admission(self, **kwargs: Any) -> AdmissionGrant:
|
|
return self._sign(AdmissionGrant, _GATEWAY_GRANT_INFO, gateway=True, **kwargs)
|
|
|
|
def sign_subject(self, **kwargs: Any) -> VerifiedModelSubject:
|
|
return self._sign(
|
|
VerifiedModelSubject, _GATEWAY_GRANT_INFO, gateway=True, **kwargs
|
|
)
|
|
|
|
def sign_admin(self, **kwargs: Any) -> AdminConfigGrant:
|
|
return self._sign(AdminConfigGrant, _ADMIN_CONFIG_INFO, gateway=True, **kwargs)
|
|
|
|
def sign_quote(self, **kwargs: Any) -> PreparedRunQuote:
|
|
return self._sign(PreparedRunQuote, _EVO_QUOTE_INFO, gateway=False, **kwargs)
|
|
|
|
def agent_input_digest(self, payload: Any) -> str:
|
|
_, key = self.key_ring.derive_current(_INPUT_DIGEST_INFO)
|
|
return hmac_id(key, payload)
|
|
|
|
def prepared_input_digest(self, payload: Any) -> str:
|
|
_, key = self.key_ring.derive_current(_PREPARED_INPUT_INFO)
|
|
return hmac_id(key, payload)
|
|
|
|
def prepared_snapshot_digest(self, payload: Any) -> str:
|
|
_, key = self.key_ring.derive_current(_PREPARED_SNAPSHOT_INFO)
|
|
return hmac_id(key, payload)
|
|
|
|
def tool_registry_snapshot_id(self, payload: Any) -> str:
|
|
_, key = self.key_ring.derive_current(_TOOL_REGISTRY_INFO)
|
|
return hmac_id(key, payload)
|
|
|
|
def require_preparation(self, grant: RoutePreparationGrant) -> None:
|
|
if not isinstance(grant, RoutePreparationGrant):
|
|
raise EvoRuntimeError("CONTRACT_SIGNATURE_INVALID")
|
|
self._require(grant, _GATEWAY_GRANT_INFO, gateway=True)
|
|
|
|
def require_admission(self, grant: AdmissionGrant) -> None:
|
|
if not isinstance(grant, AdmissionGrant):
|
|
raise EvoRuntimeError("CONTRACT_SIGNATURE_INVALID")
|
|
self._require(grant, _GATEWAY_GRANT_INFO, gateway=True)
|
|
|
|
def require_admin(self, grant: AdminConfigGrant) -> None:
|
|
if not isinstance(grant, AdminConfigGrant):
|
|
raise EvoRuntimeError("CONTRACT_SIGNATURE_INVALID")
|
|
self._require(
|
|
grant, _ADMIN_CONFIG_INFO, gateway=True, max_ttl_ms=MAX_ADMIN_TTL_MS
|
|
)
|
|
|
|
def require_quote(self, quote: PreparedRunQuote) -> None:
|
|
if not isinstance(quote, PreparedRunQuote):
|
|
raise EvoRuntimeError("CONTRACT_SIGNATURE_INVALID")
|
|
self._require(quote, _EVO_QUOTE_INFO, gateway=False)
|
|
|
|
def verify_admission(self, grant: AdmissionGrant) -> bool:
|
|
try:
|
|
self.require_admission(grant)
|
|
except EvoRuntimeError:
|
|
return False
|
|
return True
|
|
|
|
def verify_subject(self, subject: VerifiedModelSubject) -> bool:
|
|
try:
|
|
self._require(subject, _GATEWAY_GRANT_INFO, gateway=True)
|
|
except EvoRuntimeError:
|
|
return False
|
|
return True
|
|
|
|
def verify_admin(self, grant: AdminConfigGrant) -> bool:
|
|
try:
|
|
self.require_admin(grant)
|
|
except EvoRuntimeError:
|
|
return False
|
|
return True
|
|
|
|
def _sign(
|
|
self,
|
|
contract_class: type[_ContractT],
|
|
info: str,
|
|
*,
|
|
gateway: bool,
|
|
**kwargs: Any,
|
|
) -> _ContractT:
|
|
issued_at = int(kwargs.pop("issued_at", now_ms()))
|
|
ttl_ms = int(kwargs.pop("ttl_ms", 60_000))
|
|
key_id, key = self.key_ring.derive_current(info)
|
|
defaults = {
|
|
"contract_version": CONTRACT_VERSION,
|
|
"issuer": GATEWAY_ISSUER if gateway else EVO_ISSUER,
|
|
"audience": GATEWAY_AUDIENCE if gateway else EVO_AUDIENCE,
|
|
"issued_at": issued_at,
|
|
"expires_at": int(kwargs.pop("expires_at", issued_at + ttl_ms)),
|
|
"key_id": key_id,
|
|
}
|
|
field_names = {item.name for item in fields(contract_class)}
|
|
if "grant_id" in field_names:
|
|
defaults["grant_id"] = str(kwargs.pop("grant_id", uuid.uuid4()))
|
|
payload = {**defaults, **kwargs}
|
|
if "roles" in payload:
|
|
payload["roles"] = tuple(sorted({str(role) for role in payload["roles"]}))
|
|
unsigned = _unsigned(contract_class(signature="", **payload))
|
|
signature = sign_contract(contract_class.__name__, unsigned, key)
|
|
return contract_class(signature=signature, **payload)
|
|
|
|
def _require(
|
|
self,
|
|
contract: Any,
|
|
info: str,
|
|
*,
|
|
gateway: bool,
|
|
max_ttl_ms: int = MAX_CONTRACT_TTL_MS,
|
|
) -> None:
|
|
if int(contract.contract_version) != CONTRACT_VERSION:
|
|
raise EvoRuntimeError("CONTRACT_VERSION_UNSUPPORTED")
|
|
expected_issuer = GATEWAY_ISSUER if gateway else EVO_ISSUER
|
|
expected_audience = GATEWAY_AUDIENCE if gateway else EVO_AUDIENCE
|
|
if contract.issuer != expected_issuer or contract.audience != expected_audience:
|
|
raise EvoRuntimeError("CONTRACT_SIGNATURE_INVALID")
|
|
try:
|
|
key = self.key_ring.derive(contract.key_id, info)
|
|
except KeyError as exc:
|
|
raise EvoRuntimeError("CONTRACT_KEY_UNKNOWN") from exc
|
|
if not verify_contract(
|
|
type(contract).__name__,
|
|
contract.unsigned_payload(),
|
|
contract.signature,
|
|
key,
|
|
):
|
|
raise EvoRuntimeError("CONTRACT_SIGNATURE_INVALID")
|
|
current = now_ms()
|
|
issued_at = int(contract.issued_at)
|
|
expires_at = int(contract.expires_at)
|
|
if expires_at <= issued_at or expires_at - issued_at > max_ttl_ms:
|
|
raise EvoRuntimeError("CONTRACT_EXPIRED")
|
|
if (
|
|
issued_at > current + MAX_CLOCK_SKEW_MS
|
|
or expires_at < current - MAX_CLOCK_SKEW_MS
|
|
):
|
|
raise EvoRuntimeError("CONTRACT_EXPIRED")
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class MediaDescriptor:
|
|
media_id: str
|
|
media_type: str
|
|
content_hash: str
|
|
version: str
|
|
byte_length: int
|
|
token_bound: int
|
|
locator: str
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class AgentInputV3:
|
|
message: Any
|
|
checkpoint_thread_id: str
|
|
metadata: Mapping[str, Any] = field(default_factory=dict)
|
|
media: Sequence[MediaDescriptor | Mapping[str, Any] | str] = ()
|
|
content_blocks: Sequence[Mapping[str, Any]] = ()
|
|
|
|
def projection(self) -> Mapping[str, Any]:
|
|
allowed_metadata = {
|
|
key: self.metadata[key]
|
|
for key in sorted(self.metadata)
|
|
if key
|
|
in {
|
|
"force_context_repair",
|
|
"public_thread_id",
|
|
"source",
|
|
"user_id",
|
|
"model_options",
|
|
}
|
|
}
|
|
media = [
|
|
asdict(item) if isinstance(item, MediaDescriptor) else item
|
|
for item in self.media
|
|
]
|
|
return {
|
|
"message": self.message,
|
|
"checkpoint_thread_id": self.checkpoint_thread_id,
|
|
"metadata": allowed_metadata,
|
|
"media": media,
|
|
"content_blocks": list(self.content_blocks),
|
|
}
|
|
|
|
def canonical_bytes(self) -> bytes:
|
|
return canonical_json_v1(self.projection())
|
|
|
|
|
|
AgentInput = AgentInputV3
|
|
|
|
|
|
class RuntimeEventSink(Protocol):
|
|
async def commit(self, event: EvoRuntimeEvent) -> str: ...
|
|
|
|
async def confirm(self, event_id: str, payload_digest: str) -> str: ...
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class WebHostContext:
|
|
workspace_dir: str
|
|
memory_dir: str
|
|
workspace_backend: Any
|
|
checkpointer: Any
|
|
tool_selector_threshold: int | None = None
|
|
memory_max_inline_profile_chars: int | None = None
|
|
on_mcp_progress: Any = None
|
|
runtime_event_sink: RuntimeEventSink | None = None
|
|
tool_registry: Sequence[Any] = ()
|
|
tool_registry_revision: str = "static"
|
|
tool_registry_provider: Any = None
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class AgentExecutionProfile:
|
|
name: str
|
|
configurable_model_override: bool = True
|
|
subagents: bool = True
|
|
async_subagents: bool = True
|
|
memory_workers: bool = True
|
|
scheduler: bool = True
|
|
background_execution: bool = True
|
|
|
|
@classmethod
|
|
def web_v3(cls) -> AgentExecutionProfile:
|
|
return cls(
|
|
name="web_v3",
|
|
configurable_model_override=False,
|
|
subagents=False,
|
|
async_subagents=False,
|
|
memory_workers=False,
|
|
scheduler=False,
|
|
background_execution=False,
|
|
)
|
|
|
|
@classmethod
|
|
def web_v1(cls) -> AgentExecutionProfile:
|
|
return cls.web_v3()
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class ModelFactoryResult:
|
|
"""Explicit transfer of exclusively owned closeable resources to one run.
|
|
|
|
Plain custom factory return values are borrowed. Do not list shared clients.
|
|
The runtime closes only these exact objects, never their reachable children.
|
|
Built-in factories register newly allocated transports during construction.
|
|
Cleanup tries every resource even after failures, at most three times per
|
|
resource. Failed resources and their exception history remain owned; no
|
|
successful terminal event is emitted until all resources close.
|
|
"""
|
|
|
|
model: Any
|
|
owned_clients: tuple[Any, ...] = ()
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class AgentModelSet:
|
|
main_agent: Any
|
|
tool_selector: Any
|
|
deepagents_summarizer: Any
|
|
title: Any | None = None
|
|
main_fallbacks: tuple[Any, ...] = ()
|
|
route_health: Any = None
|
|
capacity: Any = None
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class ModelCatalogEntry:
|
|
alias: str
|
|
provider: str
|
|
supports_vision: bool
|
|
supports_reasoning: bool
|
|
allowed_reasoning_efforts: tuple[str, ...]
|
|
context_window: int
|
|
max_output_tokens: int
|
|
reasoning_mode: str
|
|
billing_sku: str
|
|
quote: PricingQuote
|
|
default_reasoning_effort: str = ""
|
|
health: Literal["closed", "open", "half_open"] = "closed"
|
|
display_name: str = ""
|
|
provider_display_name: str = ""
|
|
description: str = ""
|
|
version_policy: str = "rolling"
|
|
resolved_model_revision: str | None = None
|
|
reproducible: bool = False
|
|
capabilities: tuple[str, ...] = ()
|
|
user_options: Mapping[str, Mapping[str, Any]] = field(default_factory=dict)
|
|
parameter_constraints: tuple[Mapping[str, Any], ...] = ()
|
|
options_schema_hash: str = ""
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class ModelCatalog:
|
|
catalog_revision: int
|
|
default_alias: str
|
|
entries: tuple[ModelCatalogEntry, ...]
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class ModelAttemptEvent:
|
|
request_id: str
|
|
turn_id: str
|
|
run_id: str
|
|
preparation_id: str
|
|
admission_snapshot_id: str
|
|
admission_id: str
|
|
hold_id: str
|
|
prepared_snapshot_digest: str
|
|
prepared_input_digest: str
|
|
billing_fencing_token: int
|
|
turn_fencing_token: int
|
|
logical_call_id: str
|
|
attempt_id: str
|
|
purpose: Literal["main_agent", "tool_selector", "deepagents_summarizer", "title"]
|
|
attempt_index: int
|
|
identity: RouteIdentity
|
|
quote_id: str
|
|
billing_intent: Literal["user_charge", "platform_cost"]
|
|
outcome: Literal["rejected", "started", "succeeded", "failed", "usage_unconfirmed"]
|
|
provider_request_started: bool
|
|
provider_input_bound_tokens: int
|
|
provider_reserved_microunits: int
|
|
usage: Mapping[str, int | str | None] | None = None
|
|
usage_available: bool = False
|
|
error_code: str | None = None
|
|
health_state: Literal["closed", "open", "half_open"] = "closed"
|
|
fallback_from_attempt_id: str | None = None
|
|
timestamp: int = field(default_factory=now_ms)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class EvoRuntimeEvent:
|
|
event_id: str
|
|
runtime_instance_id: str
|
|
run_id: str
|
|
request_id: str
|
|
turn_id: str
|
|
sequence: int
|
|
kind: Literal["agent", "model_attempt", "title", "run"]
|
|
payload: Mapping[str, Any]
|
|
emitted_at: int = field(default_factory=now_ms)
|
|
schema_version: int = CONTRACT_VERSION
|
|
|
|
|
|
class EvoWebRun(Protocol):
|
|
@property
|
|
def run_id(self) -> str: ...
|
|
|
|
async def stream(
|
|
self, after_sequence: int | None = None
|
|
) -> AsyncIterator[EvoRuntimeEvent]: ...
|
|
|
|
async def cancel(self, reason: str, *, owner_epoch: int | None = None,
|
|
boot_id: str | None = None) -> str: ...
|
|
|
|
async def wait_stopped(self, timeout: float | None = 2.0) -> str:
|
|
"""Return a terminal outcome only after owned resource cleanup.
|
|
|
|
A bounded timeout returns ``unknown`` without cancelling execution or
|
|
cleanup. Cancelling the waiter also leaves cleanup owned by the run.
|
|
Host checkpointers and workspace backends are borrowed, never closed.
|
|
``awaiting_input`` ends this execution, not the checkpoint workflow;
|
|
a separate run may resume it. Its run_terminal payload includes the
|
|
final checkpoint_thread_id, checkpoint_id, checkpoint_ns and JSON
|
|
pending_interrupts (id/value records). Consumers must not interpret
|
|
every non-failed outcome as completed. Final checkpoint read failure
|
|
yields failed with FINAL_CHECKPOINT_READ_FAILED, never completed.
|
|
"""
|
|
...
|
|
|
|
|
|
def event_payload(value: Any) -> dict[str, Any]:
|
|
if isinstance(value, ModelAttemptEvent):
|
|
return asdict(value)
|
|
if isinstance(value, Mapping):
|
|
return dict(value)
|
|
return {"value": str(value)}
|