210 lines
6.2 KiB
Python
210 lines
6.2 KiB
Python
"""Thin callable adapter for :mod:`autonomous_resume_controller`.
|
|
|
|
This module contains no transport, daemon, database or scheduler loop. It only
|
|
binds current AIOS runtime functions to the stable ``AutonomousExecutionPort``
|
|
contract and exposes one scheduler tick.
|
|
|
|
A field integrator should:
|
|
- resolve the live current APIs;
|
|
- supply callables that use existing governed authorities;
|
|
- register ``AutonomousSchedulerTick.run`` with the existing scheduler/timer.
|
|
|
|
No additional control plane is created here.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from typing import Callable
|
|
|
|
from .autonomous_resume_controller import (
|
|
AutonomousExecutionPort,
|
|
AutonomousResumeController,
|
|
ControllerOutcome,
|
|
MissionCheckpoint,
|
|
MissionObservation,
|
|
MissionPhase,
|
|
ResumeLease,
|
|
StepResult,
|
|
)
|
|
|
|
|
|
PhaseHandler = Callable[
|
|
[MissionObservation, MissionCheckpoint, ResumeLease],
|
|
StepResult,
|
|
]
|
|
ReconcileHandler = PhaseHandler
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class PhaseCallbacks:
|
|
admission: PhaseHandler
|
|
source_execution: PhaseHandler
|
|
gate0: PhaseHandler
|
|
release_ready: PhaseHandler
|
|
promotion: PhaseHandler
|
|
final_acceptance: PhaseHandler
|
|
|
|
def for_phase(self, phase: MissionPhase) -> PhaseHandler:
|
|
mapping = {
|
|
MissionPhase.S2_ADMISSION: self.admission,
|
|
MissionPhase.S2_EXECUTION: self.source_execution,
|
|
MissionPhase.GATE0: self.gate0,
|
|
MissionPhase.RELEASE_READY: self.release_ready,
|
|
MissionPhase.PROMOTION: self.promotion,
|
|
MissionPhase.FINAL_ACCEPTANCE: self.final_acceptance,
|
|
}
|
|
try:
|
|
return mapping[phase]
|
|
except KeyError as exc:
|
|
raise ValueError(f"phase_has_no_executor:{phase.value}") from exc
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class AuthorityCallbacks:
|
|
read_observation: Callable[[str], MissionObservation]
|
|
load_checkpoint: Callable[[str], MissionCheckpoint | None]
|
|
acquire_resume_lease: Callable[[str, int, str, int], ResumeLease | None]
|
|
persist_checkpoint: Callable[[MissionCheckpoint, ResumeLease], None]
|
|
persist_owner_action_required: Callable[
|
|
[dict, MissionObservation, MissionCheckpoint, ResumeLease],
|
|
str,
|
|
]
|
|
release_resume_lease: Callable[[ResumeLease, str], None]
|
|
reconcile: ReconcileHandler
|
|
|
|
|
|
class CallableAutonomousExecutionPort(AutonomousExecutionPort):
|
|
"""Runtime adapter composed from already-governed AIOS callables."""
|
|
|
|
def __init__(
|
|
self,
|
|
authority: AuthorityCallbacks,
|
|
phases: PhaseCallbacks,
|
|
) -> None:
|
|
self.authority = authority
|
|
self.phases = phases
|
|
|
|
def read_observation(self, mission_id: str) -> MissionObservation:
|
|
value = self.authority.read_observation(mission_id)
|
|
if not isinstance(value, MissionObservation):
|
|
raise TypeError("read_observation_must_return_MissionObservation")
|
|
return value
|
|
|
|
def load_checkpoint(self, mission_id: str) -> MissionCheckpoint | None:
|
|
value = self.authority.load_checkpoint(mission_id)
|
|
if value is not None and not isinstance(value, MissionCheckpoint):
|
|
raise TypeError("load_checkpoint_must_return_checkpoint_or_none")
|
|
return value
|
|
|
|
def acquire_resume_lease(
|
|
self,
|
|
*,
|
|
mission_id: str,
|
|
generation: int,
|
|
idempotency_key: str,
|
|
lease_seconds: int,
|
|
) -> ResumeLease | None:
|
|
value = self.authority.acquire_resume_lease(
|
|
mission_id,
|
|
generation,
|
|
idempotency_key,
|
|
lease_seconds,
|
|
)
|
|
if value is not None and not isinstance(value, ResumeLease):
|
|
raise TypeError("acquire_resume_lease_must_return_lease_or_none")
|
|
return value
|
|
|
|
def step(
|
|
self,
|
|
*,
|
|
phase: MissionPhase,
|
|
observation: MissionObservation,
|
|
checkpoint: MissionCheckpoint,
|
|
lease: ResumeLease,
|
|
) -> StepResult:
|
|
handler = self.phases.for_phase(phase)
|
|
value = handler(observation, checkpoint, lease)
|
|
if not isinstance(value, StepResult):
|
|
raise TypeError("phase_handler_must_return_StepResult")
|
|
return value
|
|
|
|
def reconcile(
|
|
self,
|
|
*,
|
|
observation: MissionObservation,
|
|
checkpoint: MissionCheckpoint,
|
|
lease: ResumeLease,
|
|
) -> StepResult:
|
|
value = self.authority.reconcile(
|
|
observation,
|
|
checkpoint,
|
|
lease,
|
|
)
|
|
if not isinstance(value, StepResult):
|
|
raise TypeError("reconcile_must_return_StepResult")
|
|
return value
|
|
|
|
def persist_checkpoint(
|
|
self,
|
|
checkpoint: MissionCheckpoint,
|
|
*,
|
|
lease: ResumeLease,
|
|
) -> None:
|
|
self.authority.persist_checkpoint(checkpoint, lease)
|
|
|
|
def persist_owner_action_required(
|
|
self,
|
|
payload,
|
|
*,
|
|
observation: MissionObservation,
|
|
checkpoint: MissionCheckpoint,
|
|
lease: ResumeLease,
|
|
) -> str:
|
|
value = self.authority.persist_owner_action_required(
|
|
dict(payload),
|
|
observation,
|
|
checkpoint,
|
|
lease,
|
|
)
|
|
if not isinstance(value, str) or not value:
|
|
raise ValueError("owner_action_record_ref_required")
|
|
return value
|
|
|
|
def release_resume_lease(
|
|
self,
|
|
lease: ResumeLease,
|
|
*,
|
|
outcome_code: str,
|
|
) -> None:
|
|
self.authority.release_resume_lease(lease, outcome_code)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class SchedulerTickResult:
|
|
outcome: ControllerOutcome
|
|
next_wake_after_seconds: int | None
|
|
|
|
|
|
class AutonomousSchedulerTick:
|
|
"""One invocation for an existing scheduler/timer.
|
|
|
|
This object never sleeps or loops. The host scheduler owns cadence.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
controller: AutonomousResumeController,
|
|
mission_id: str,
|
|
) -> None:
|
|
if not mission_id:
|
|
raise ValueError("mission_id_required")
|
|
self.controller = controller
|
|
self.mission_id = mission_id
|
|
|
|
def run(self) -> SchedulerTickResult:
|
|
outcome = self.controller.run_once(self.mission_id)
|
|
return SchedulerTickResult(
|
|
outcome=outcome,
|
|
next_wake_after_seconds=outcome.next_wake_after_seconds,
|
|
)
|