Skip to main content

meerkat_runtime/
traits.rs

1//! §23 Runtime traits — RuntimeDriver and RuntimeControlPlane.
2//!
3//! These define the interface between surfaces and the runtime control-plane.
4
5use meerkat_core::lifecycle::{InputId, RunId};
6use serde::{Deserialize, Serialize};
7
8use crate::accept::AcceptOutcome;
9use crate::identifiers::LogicalRuntimeId;
10use crate::input::Input;
11use crate::input_state::{InputLifecycleState, InputState, StoredInputState};
12use crate::runtime_event::RuntimeEventEnvelope;
13use crate::runtime_state::RuntimeState;
14
15/// Errors from RuntimeDriver operations.
16#[derive(Debug, Clone, thiserror::Error)]
17#[non_exhaustive]
18pub enum RuntimeDriverError {
19    /// The runtime is not in a state that can accept this operation.
20    #[error("Runtime not ready: {state}")]
21    NotReady { state: RuntimeState },
22
23    /// The runtime was never registered / does not exist.
24    ///
25    /// Distinct from [`RuntimeDriverError::Destroyed`] and
26    /// [`RuntimeDriverError::NotReady`] with a `Destroyed` state: absence means
27    /// the runtime id was never admitted, not that it once existed and was torn
28    /// down.
29    #[error("Runtime not found: {runtime_id}")]
30    NotFound { runtime_id: LogicalRuntimeId },
31
32    /// Input validation failed.
33    #[error("Input validation failed: {reason}")]
34    ValidationFailed { reason: String },
35
36    /// The runtime has been destroyed.
37    #[error("Runtime destroyed")]
38    Destroyed,
39
40    /// Durable recovery state could not be replayed through canonical runtime authority.
41    #[error("Recovery corruption: {reason}")]
42    RecoveryCorruption { reason: String },
43
44    /// Internal error.
45    #[error("Internal error: {0}")]
46    Internal(String),
47}
48
49/// Errors from RuntimeControlPlane operations.
50#[derive(Debug, Clone, thiserror::Error)]
51#[non_exhaustive]
52pub enum RuntimeControlPlaneError {
53    /// Runtime not found.
54    #[error("Runtime not found: {0}")]
55    NotFound(LogicalRuntimeId),
56
57    /// Invalid state for this operation.
58    #[error("Invalid state for operation: {state}")]
59    InvalidState { state: RuntimeState },
60
61    /// Store error.
62    #[error("Store error: {0}")]
63    StoreError(String),
64
65    /// Internal error.
66    #[error("Internal error: {0}")]
67    Internal(String),
68}
69
70/// Report from a recovery operation.
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct RecoveryReport {
73    /// How many inputs were recovered.
74    pub inputs_recovered: usize,
75    /// How many inputs were abandoned during recovery.
76    pub inputs_abandoned: usize,
77    /// How many inputs were re-queued.
78    pub inputs_requeued: usize,
79    /// Details of recovery actions.
80    #[serde(default, skip_serializing_if = "Vec::is_empty")]
81    pub details: Vec<String>,
82}
83
84/// Report from a retire operation.
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct RetireReport {
87    /// How many non-terminal inputs were abandoned.
88    pub inputs_abandoned: usize,
89    /// How many inputs are pending drain (will be processed before stopping).
90    #[serde(default)]
91    pub inputs_pending_drain: usize,
92}
93
94/// Report from a reset operation.
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct ResetReport {
97    /// How many non-terminal inputs were abandoned.
98    pub inputs_abandoned: usize,
99}
100
101/// Report from a recycle operation (reset driver and recover state).
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct RecycleReport {
104    /// How many inputs were transferred to the new instance.
105    pub inputs_transferred: usize,
106}
107
108/// Report from a destroy operation.
109#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct DestroyReport {
111    /// How many non-terminal inputs were abandoned.
112    pub inputs_abandoned: usize,
113}
114
115/// The runtime driver — per-session interface for input acceptance and lifecycle.
116///
117/// Each session gets its own RuntimeDriver instance. The driver manages the
118/// InputState ledger, policy resolution, and input queue for that session.
119#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
120#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
121pub trait RuntimeDriver: Send + Sync {
122    /// Accept an input into the runtime.
123    async fn accept_input(&mut self, input: Input) -> Result<AcceptOutcome, RuntimeDriverError>;
124
125    /// Handle a runtime event (from the event bus).
126    async fn on_runtime_event(
127        &mut self,
128        event: RuntimeEventEnvelope,
129    ) -> Result<(), RuntimeDriverError>;
130
131    /// Recover from a crash/restart.
132    async fn recover(&mut self) -> Result<RecoveryReport, RuntimeDriverError>;
133
134    /// Get the current runtime state.
135    fn runtime_state(&self) -> RuntimeState;
136
137    /// Get the state of a specific input.
138    fn input_state(&self, input_id: &InputId) -> Option<&InputState>;
139
140    /// Get the current DSL-owned lifecycle phase of a specific input.
141    fn input_phase(&self, input_id: &InputId) -> Option<InputLifecycleState>;
142
143    /// Get the current DSL-owned last run association for a specific input.
144    fn input_last_run_id(&self, input_id: &InputId) -> Option<RunId>;
145
146    /// Get the current DSL-owned last boundary sequence for a specific input.
147    fn input_last_boundary_sequence(&self, input_id: &InputId) -> Option<u64>;
148
149    /// Get the persisted shell+seed bundle for a specific input.
150    fn stored_input_state(&self, input_id: &InputId) -> Option<StoredInputState>;
151
152    /// Snapshot of every ledger entry paired with its DSL-owned seed.
153    ///
154    /// The live-runtime witness set for terminal-status evaluation: the same
155    /// facts a persistent store commits at every lifecycle boundary, read
156    /// from the DSL authority instead of disk.
157    fn stored_input_states_snapshot(&self) -> Result<Vec<StoredInputState>, RuntimeDriverError>;
158
159    /// Resolve the machine-owned idempotency-key binding to its input id.
160    ///
161    /// Read-only reconciliation mirror of the generated admission map — it
162    /// decides nothing and never registers a binding (the accept-path
163    /// admission resolution stays the only mutator).
164    fn input_id_for_idempotency_key(&self, idempotency_key: &str) -> Option<InputId>;
165
166    /// List all non-terminal input IDs.
167    fn active_input_ids(&self) -> Vec<InputId>;
168}
169
170/// The runtime control plane — manages multiple runtime instances.
171#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
172#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
173pub trait RuntimeControlPlane: Send + Sync {
174    /// Ingest an input into a specific runtime.
175    async fn ingest(
176        &self,
177        runtime_id: &LogicalRuntimeId,
178        input: Input,
179    ) -> Result<AcceptOutcome, RuntimeControlPlaneError>;
180
181    /// Publish a runtime event.
182    async fn publish_event(
183        &self,
184        event: RuntimeEventEnvelope,
185    ) -> Result<(), RuntimeControlPlaneError>;
186
187    /// Retire a runtime (no new input, drain existing).
188    async fn retire(
189        &self,
190        runtime_id: &LogicalRuntimeId,
191    ) -> Result<RetireReport, RuntimeControlPlaneError>;
192
193    /// Recycle a runtime (reset driver and recover state).
194    async fn recycle(
195        &self,
196        runtime_id: &LogicalRuntimeId,
197    ) -> Result<RecycleReport, RuntimeControlPlaneError>;
198
199    /// Reset a runtime (abandon all pending input).
200    async fn reset(
201        &self,
202        runtime_id: &LogicalRuntimeId,
203    ) -> Result<ResetReport, RuntimeControlPlaneError>;
204
205    /// Recover a runtime from crash.
206    async fn recover(
207        &self,
208        runtime_id: &LogicalRuntimeId,
209    ) -> Result<RecoveryReport, RuntimeControlPlaneError>;
210
211    /// Get the state of a runtime.
212    async fn runtime_state(
213        &self,
214        runtime_id: &LogicalRuntimeId,
215    ) -> Result<RuntimeState, RuntimeControlPlaneError>;
216
217    /// Destroy a runtime (terminal state, no recovery possible).
218    async fn destroy(
219        &self,
220        runtime_id: &LogicalRuntimeId,
221    ) -> Result<DestroyReport, RuntimeControlPlaneError>;
222
223    /// Load a boundary receipt for verification.
224    async fn load_boundary_receipt(
225        &self,
226        runtime_id: &LogicalRuntimeId,
227        run_id: &RunId,
228        sequence: u64,
229    ) -> Result<Option<meerkat_core::lifecycle::RunBoundaryReceipt>, RuntimeControlPlaneError>;
230}
231
232#[cfg(test)]
233#[allow(clippy::unwrap_used)]
234mod tests {
235    use super::*;
236
237    // Verify traits are object-safe
238    fn _assert_driver_object_safe(_: &dyn RuntimeDriver) {}
239    fn _assert_control_plane_object_safe(_: &dyn RuntimeControlPlane) {}
240
241    #[test]
242    fn runtime_driver_error_display() {
243        let err = RuntimeDriverError::NotReady {
244            state: RuntimeState::Initializing,
245        };
246        assert!(err.to_string().contains("initializing"));
247
248        let err = RuntimeDriverError::ValidationFailed {
249            reason: "bad input".into(),
250        };
251        assert!(err.to_string().contains("bad input"));
252    }
253
254    #[test]
255    fn runtime_control_plane_error_display() {
256        let err = RuntimeControlPlaneError::NotFound(LogicalRuntimeId::new("missing"));
257        assert!(err.to_string().contains("missing"));
258    }
259
260    #[test]
261    fn recovery_report_serde() {
262        let report = RecoveryReport {
263            inputs_recovered: 5,
264            inputs_abandoned: 1,
265            inputs_requeued: 3,
266            details: vec!["requeued 3 staged inputs".into()],
267        };
268        let json = serde_json::to_value(&report).unwrap();
269        let parsed: RecoveryReport = serde_json::from_value(json).unwrap();
270        assert_eq!(parsed.inputs_recovered, 5);
271    }
272}