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