Files
openai--openai-agents-python/src/agents/sandbox/session/sandbox_session_state.py
T
2026-07-13 12:39:17 +08:00

115 lines
4.1 KiB
Python

from __future__ import annotations
import uuid
from collections.abc import Iterable
from typing import Any, ClassVar, Literal, get_args, get_origin
from pydantic import BaseModel, ConfigDict, Field, SerializeAsAny, field_validator, model_serializer
from ..manifest import Manifest
from ..snapshot import SnapshotBase
SessionStateClass = type["SandboxSessionState"]
class SandboxSessionState(BaseModel):
model_config = ConfigDict(arbitrary_types_allowed=True)
type: str
session_id: uuid.UUID = Field(default_factory=uuid.uuid4)
snapshot: SerializeAsAny[SnapshotBase]
manifest: Manifest
exposed_ports: tuple[int, ...] = Field(default_factory=tuple)
snapshot_fingerprint: str | None = None
snapshot_fingerprint_version: str | None = None
workspace_root_ready: bool = False
_subclass_registry: ClassVar[dict[str, SessionStateClass]] = {}
@classmethod
def __pydantic_init_subclass__(cls, **kwargs: Any) -> None:
"""Auto-register every subclass by its ``type`` field default."""
super().__pydantic_init_subclass__(**kwargs)
type_field = cls.model_fields.get("type")
if type_field is None:
return
annotation = type_field.annotation
if get_origin(annotation) is not Literal:
return
args = get_args(annotation)
if not args:
return
type_default = type_field.default
if not isinstance(type_default, str) or type_default == "":
return
SandboxSessionState._subclass_registry[type_default] = cls
@classmethod
def parse(cls, payload: object) -> SandboxSessionState:
"""Deserialize *payload* into the correct registered subclass.
Accepts a ``SandboxSessionState`` instance (returned as-is if already a
subclass, or upgraded via ``model_dump`` -> registry lookup if it is a
bare base instance) or a plain ``dict``.
"""
if isinstance(payload, SandboxSessionState):
if type(payload) is not SandboxSessionState:
return payload
payload = payload.model_dump()
if isinstance(payload, dict):
state_type = payload.get("type")
if not isinstance(state_type, str):
raise ValueError("sandbox session state payload must include a string `type`")
subclass = SandboxSessionState._subclass_registry.get(state_type)
if subclass is None:
raise ValueError(f"unknown sandbox session state type `{state_type}`")
return subclass.model_validate(payload)
raise TypeError("session state payload must be a SandboxSessionState or dict")
@model_serializer(mode="wrap")
def _serialize_always_include_defaults(self, handler: Any) -> dict[str, Any]:
data: dict[str, Any] = handler(self)
if self.type:
data["type"] = self.type
if self.session_id:
data["session_id"] = self.session_id
return data
@field_validator("snapshot", mode="before")
@classmethod
def _coerce_snapshot(cls, value: object) -> SnapshotBase:
return SnapshotBase.parse(value)
@field_validator("exposed_ports", mode="before")
@classmethod
def _coerce_exposed_ports(cls, value: object) -> tuple[int, ...]:
if value is None:
return ()
if isinstance(value, int):
ports: Iterable[object] = (value,)
elif isinstance(value, Iterable) and not isinstance(value, str | bytes | bytearray):
ports = value
else:
raise TypeError("exposed_ports must be an iterable of TCP port integers")
normalized: list[int] = []
seen: set[int] = set()
for port in ports:
if not isinstance(port, int):
raise TypeError("exposed_ports must contain integers")
if port < 1 or port > 65535:
raise ValueError("exposed_ports entries must be between 1 and 65535")
if port in seen:
continue
seen.add(port)
normalized.append(port)
return tuple(normalized)