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