Files
EvoScientist-Multi/EvoScientist/llm/contracts.py
T
m4 c683f6e739
Docker / build (push) Has been cancelled
Build / build (push) Has been cancelled
Lint / ruff (push) Has been cancelled
Test / pytest (ubuntu-latest, 3.11) (push) Has been cancelled
Test / pytest (ubuntu-latest, 3.12) (push) Has been cancelled
Test / pytest (windows-latest, 3.11) (push) Has been cancelled
Test / pytest (windows-latest, 3.12) (push) Has been cancelled
feat: prepare EvoScientist 0.3.0
Add bounded document ingestion, controlled web search, recoverable session support, subagent timeouts, and the native sandbox runtime contract. Unify package versioning and add release-focused regression coverage.
2026-09-03 06:55:56 +08:00

876 lines
25 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)
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
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
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 = {
key_name: value
for key_name, value in payload.items()
if key_name != "signature"
}
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 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) -> str: ...
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)}