Files
aios/services/aios-autonomous-release-candidate/aios_autonomous_release_candidate/session.py
T

88 lines
3.3 KiB
Python

"""Unified candidate session lifecycle contracts."""
from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
from datetime import datetime
from .errors import StateConflict
class SessionState(str, Enum):
CREATED = "CREATED"
PROVISIONED = "PROVISIONED"
ACTIVE = "ACTIVE"
INTERRUPTED = "INTERRUPTED"
CLOSED = "CLOSED"
REVOKED = "REVOKED"
_ALLOWED_TRANSITIONS: dict[SessionState, frozenset[SessionState]] = {
SessionState.CREATED: frozenset({SessionState.PROVISIONED, SessionState.CLOSED, SessionState.REVOKED}),
SessionState.PROVISIONED: frozenset({SessionState.ACTIVE, SessionState.CLOSED, SessionState.REVOKED}),
SessionState.ACTIVE: frozenset({SessionState.INTERRUPTED, SessionState.CLOSED, SessionState.REVOKED}),
SessionState.INTERRUPTED: frozenset({SessionState.ACTIVE, SessionState.CLOSED, SessionState.REVOKED}),
SessionState.CLOSED: frozenset({SessionState.REVOKED}),
SessionState.REVOKED: frozenset(),
}
@dataclass(frozen=True, slots=True)
class SessionIdentity:
session_id: str
provider: str
provider_session_id: str
worker_id: str
task_id: str
created_at: datetime
def __post_init__(self) -> None:
if not all((self.session_id, self.provider, self.provider_session_id, self.worker_id, self.task_id)):
raise ValueError("all session identity fields are required")
if self.session_id == self.provider_session_id:
raise ValueError("broker and provider session IDs must be distinct")
class SessionOrchestrator:
"""Provider-neutral session lifecycle guard with explicit state transitions."""
def __init__(self) -> None:
self._sessions: dict[str, tuple[SessionIdentity, SessionState]] = {}
def create(self, identity: SessionIdentity) -> None:
if identity.session_id in self._sessions:
raise StateConflict("SESSION_EXISTS", "session already exists")
self._sessions[identity.session_id] = (identity, SessionState.CREATED)
def transition(self, session_id: str, state: SessionState) -> None:
current = self._sessions.get(session_id)
if current is None:
raise StateConflict("SESSION_NOT_FOUND", "session does not exist")
current_state = current[1]
if state not in _ALLOWED_TRANSITIONS[current_state]:
raise StateConflict(
"SESSION_TRANSITION_INVALID",
"session state transition is not allowed",
{"session_id": session_id, "from": current_state.value, "to": state.value},
)
self._sessions[session_id] = (current[0], state)
def identity(self, session_id: str) -> SessionIdentity:
current = self._sessions.get(session_id)
if current is None:
raise StateConflict("SESSION_NOT_FOUND", "session does not exist")
return current[0]
def status(self, session_id: str) -> SessionState:
current = self._sessions.get(session_id)
if current is None:
raise StateConflict("SESSION_NOT_FOUND", "session does not exist")
return current[1]
def close(self, session_id: str) -> None:
self.transition(session_id, SessionState.CLOSED)
def revoke(self, session_id: str) -> None:
self.transition(session_id, SessionState.REVOKED)