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    /// Atomic unregister persistence may already be durable. The live retry
45    /// anchor is retained, but no compensating durable rollback may run.
46    #[error("Unregister finalization outcome is unknown: {reason}")]
47    UnregisterFinalizationOutcomeUnknown { reason: String },
48
49    /// The machine-owned unregister saga still owns this runtime epoch and is
50    /// continuing asynchronously. The caller may retry to join the same saga;
51    /// this is not permission to replace or abandon the owned executor.
52    #[error("Unregister teardown is still in progress for runtime {runtime_id}")]
53    UnregisterInProgress { runtime_id: LogicalRuntimeId },
54
55    /// The machine-owned ordinary-stop cleanup coordinator still owns this
56    /// runtime epoch. The caller may retry to join the same coordinator; this
57    /// is not permission to unregister, replace, or abandon the exact executor.
58    #[error("Runtime stop cleanup is still in progress for runtime {runtime_id}")]
59    RuntimeStopInProgress { runtime_id: LogicalRuntimeId },
60
61    /// Internal error.
62    #[error("Internal error: {0}")]
63    Internal(String),
64}
65
66/// Errors from RuntimeControlPlane operations.
67#[derive(Debug, Clone, thiserror::Error)]
68#[non_exhaustive]
69pub enum RuntimeControlPlaneError {
70    /// Runtime not found.
71    #[error("Runtime not found: {0}")]
72    NotFound(LogicalRuntimeId),
73
74    /// Invalid state for this operation.
75    #[error("Invalid state for operation: {state}")]
76    InvalidState { state: RuntimeState },
77
78    /// Store error.
79    #[error("Store error: {0}")]
80    StoreError(String),
81
82    /// Internal error.
83    #[error("Internal error: {0}")]
84    Internal(String),
85}
86
87/// Report from a recovery operation.
88#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct RecoveryReport {
90    /// How many inputs were recovered.
91    pub inputs_recovered: usize,
92    /// How many inputs were abandoned during recovery.
93    pub inputs_abandoned: usize,
94    /// How many inputs were re-queued.
95    pub inputs_requeued: usize,
96    /// Details of recovery actions.
97    #[serde(default, skip_serializing_if = "Vec::is_empty")]
98    pub details: Vec<String>,
99}
100
101/// Report from a retire operation.
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct RetireReport {
104    /// How many non-terminal inputs were abandoned.
105    pub inputs_abandoned: usize,
106    /// How many inputs are pending drain (will be processed before stopping).
107    #[serde(default)]
108    pub inputs_pending_drain: usize,
109}
110
111/// Report from a reset operation.
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct ResetReport {
114    /// How many non-terminal inputs were abandoned.
115    pub inputs_abandoned: usize,
116}
117
118/// Report from a recycle operation (reset driver and recover state).
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct RecycleReport {
121    /// How many inputs were transferred to the new instance.
122    pub inputs_transferred: usize,
123}
124
125/// Report from a destroy operation.
126#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct DestroyReport {
128    /// How many non-terminal inputs were abandoned.
129    pub inputs_abandoned: usize,
130}
131
132/// The runtime driver — per-session interface for input acceptance and lifecycle.
133///
134/// Each session gets its own RuntimeDriver instance. The driver manages the
135/// InputState ledger, policy resolution, and input queue for that session.
136#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
137#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
138pub trait RuntimeDriver: Send + Sync {
139    /// Accept an input into the runtime.
140    async fn accept_input(&mut self, input: Input) -> Result<AcceptOutcome, RuntimeDriverError>;
141
142    /// Handle a runtime event (from the event bus).
143    async fn on_runtime_event(
144        &mut self,
145        event: RuntimeEventEnvelope,
146    ) -> Result<(), RuntimeDriverError>;
147
148    /// Recover from a crash/restart.
149    async fn recover(&mut self) -> Result<RecoveryReport, RuntimeDriverError>;
150
151    /// Get the current runtime state.
152    fn runtime_state(&self) -> RuntimeState;
153
154    /// Get the state of a specific input.
155    fn input_state(&self, input_id: &InputId) -> Option<&InputState>;
156
157    /// Get the current DSL-owned lifecycle phase of a specific input.
158    fn input_phase(&self, input_id: &InputId) -> Option<InputLifecycleState>;
159
160    /// Get the current DSL-owned last run association for a specific input.
161    fn input_last_run_id(&self, input_id: &InputId) -> Option<RunId>;
162
163    /// Get the current DSL-owned last boundary sequence for a specific input.
164    fn input_last_boundary_sequence(&self, input_id: &InputId) -> Option<u64>;
165
166    /// Get the persisted shell+seed bundle for a specific input.
167    fn stored_input_state(&self, input_id: &InputId) -> Option<StoredInputState>;
168
169    /// Snapshot of every ledger entry paired with its DSL-owned seed.
170    ///
171    /// The live-runtime witness set for terminal-status evaluation: the same
172    /// facts a persistent store commits at every lifecycle boundary, read
173    /// from the DSL authority instead of disk.
174    fn stored_input_states_snapshot(&self) -> Result<Vec<StoredInputState>, RuntimeDriverError>;
175
176    /// Resolve the machine-owned idempotency-key binding to its input id.
177    ///
178    /// Read-only reconciliation mirror of the generated admission map — it
179    /// decides nothing and never registers a binding (the accept-path
180    /// admission resolution stays the only mutator).
181    fn input_id_for_idempotency_key(&self, idempotency_key: &str) -> Option<InputId>;
182
183    /// List all non-terminal input IDs.
184    fn active_input_ids(&self) -> Vec<InputId>;
185}
186
187/// The runtime control plane — manages multiple runtime instances.
188#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
189#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
190pub trait RuntimeControlPlane: Send + Sync {
191    /// Ingest an input into a specific runtime.
192    async fn ingest(
193        &self,
194        runtime_id: &LogicalRuntimeId,
195        input: Input,
196    ) -> Result<AcceptOutcome, RuntimeControlPlaneError>;
197
198    /// Publish a runtime event.
199    async fn publish_event(
200        &self,
201        event: RuntimeEventEnvelope,
202    ) -> Result<(), RuntimeControlPlaneError>;
203
204    /// Retire a runtime (no new input, drain existing).
205    async fn retire(
206        &self,
207        runtime_id: &LogicalRuntimeId,
208    ) -> Result<RetireReport, RuntimeControlPlaneError>;
209
210    /// Recycle a runtime (reset driver and recover state).
211    async fn recycle(
212        &self,
213        runtime_id: &LogicalRuntimeId,
214    ) -> Result<RecycleReport, RuntimeControlPlaneError>;
215
216    /// Reset a runtime (abandon all pending input).
217    async fn reset(
218        &self,
219        runtime_id: &LogicalRuntimeId,
220    ) -> Result<ResetReport, RuntimeControlPlaneError>;
221
222    /// Recover a runtime from crash.
223    async fn recover(
224        &self,
225        runtime_id: &LogicalRuntimeId,
226    ) -> Result<RecoveryReport, RuntimeControlPlaneError>;
227
228    /// Get the state of a runtime.
229    async fn runtime_state(
230        &self,
231        runtime_id: &LogicalRuntimeId,
232    ) -> Result<RuntimeState, RuntimeControlPlaneError>;
233
234    /// Destroy a runtime (terminal state, no recovery possible).
235    async fn destroy(
236        &self,
237        runtime_id: &LogicalRuntimeId,
238    ) -> Result<DestroyReport, RuntimeControlPlaneError>;
239
240    /// Load a boundary receipt for verification.
241    async fn load_boundary_receipt(
242        &self,
243        runtime_id: &LogicalRuntimeId,
244        run_id: &RunId,
245        sequence: u64,
246    ) -> Result<Option<meerkat_core::lifecycle::RunBoundaryReceipt>, RuntimeControlPlaneError>;
247}
248
249#[cfg(test)]
250#[allow(clippy::unwrap_used)]
251mod tests {
252    use super::*;
253
254    // Verify traits are object-safe
255    fn _assert_driver_object_safe(_: &dyn RuntimeDriver) {}
256    fn _assert_control_plane_object_safe(_: &dyn RuntimeControlPlane) {}
257
258    #[test]
259    fn runtime_driver_error_display() {
260        let err = RuntimeDriverError::NotReady {
261            state: RuntimeState::Initializing,
262        };
263        assert!(err.to_string().contains("initializing"));
264
265        let err = RuntimeDriverError::ValidationFailed {
266            reason: "bad input".into(),
267        };
268        assert!(err.to_string().contains("bad input"));
269    }
270
271    #[test]
272    fn runtime_control_plane_error_display() {
273        let err = RuntimeControlPlaneError::NotFound(LogicalRuntimeId::new("missing"));
274        assert!(err.to_string().contains("missing"));
275    }
276
277    #[test]
278    fn recovery_report_serde() {
279        let report = RecoveryReport {
280            inputs_recovered: 5,
281            inputs_abandoned: 1,
282            inputs_requeued: 3,
283            details: vec!["requeued 3 staged inputs".into()],
284        };
285        let json = serde_json::to_value(&report).unwrap();
286        let parsed: RecoveryReport = serde_json::from_value(json).unwrap();
287        assert_eq!(parsed.inputs_recovered, 5);
288    }
289}