1use 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#[derive(Debug, Clone, thiserror::Error)]
17#[non_exhaustive]
18pub enum RuntimeDriverError {
19 #[error("Runtime not ready: {state}")]
21 NotReady { state: RuntimeState },
22
23 #[error("Input validation failed: {reason}")]
25 ValidationFailed { reason: String },
26
27 #[error("Runtime destroyed")]
29 Destroyed,
30
31 #[error("Recovery corruption: {reason}")]
33 RecoveryCorruption { reason: String },
34
35 #[error("Internal error: {0}")]
37 Internal(String),
38}
39
40#[derive(Debug, Clone, thiserror::Error)]
42#[non_exhaustive]
43pub enum RuntimeControlPlaneError {
44 #[error("Runtime not found: {0}")]
46 NotFound(LogicalRuntimeId),
47
48 #[error("Invalid state for operation: {state}")]
50 InvalidState { state: RuntimeState },
51
52 #[error("Store error: {0}")]
54 StoreError(String),
55
56 #[error("Internal error: {0}")]
58 Internal(String),
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct RecoveryReport {
64 pub inputs_recovered: usize,
66 pub inputs_abandoned: usize,
68 pub inputs_requeued: usize,
70 #[serde(default, skip_serializing_if = "Vec::is_empty")]
72 pub details: Vec<String>,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct RetireReport {
78 pub inputs_abandoned: usize,
80 #[serde(default)]
82 pub inputs_pending_drain: usize,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct ResetReport {
88 pub inputs_abandoned: usize,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct RecycleReport {
95 pub inputs_transferred: usize,
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct DestroyReport {
102 pub inputs_abandoned: usize,
104}
105
106#[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 async fn accept_input(&mut self, input: Input) -> Result<AcceptOutcome, RuntimeDriverError>;
115
116 async fn on_runtime_event(
118 &mut self,
119 event: RuntimeEventEnvelope,
120 ) -> Result<(), RuntimeDriverError>;
121
122 async fn recover(&mut self) -> Result<RecoveryReport, RuntimeDriverError>;
124
125 fn runtime_state(&self) -> RuntimeState;
127
128 fn input_state(&self, input_id: &InputId) -> Option<&InputState>;
130
131 fn input_phase(&self, input_id: &InputId) -> Option<InputLifecycleState>;
133
134 fn input_last_run_id(&self, input_id: &InputId) -> Option<RunId>;
136
137 fn input_last_boundary_sequence(&self, input_id: &InputId) -> Option<u64>;
139
140 fn stored_input_state(&self, input_id: &InputId) -> Option<StoredInputState>;
142
143 fn active_input_ids(&self) -> Vec<InputId>;
145}
146
147#[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 async fn ingest(
153 &self,
154 runtime_id: &LogicalRuntimeId,
155 input: Input,
156 ) -> Result<AcceptOutcome, RuntimeControlPlaneError>;
157
158 async fn publish_event(
160 &self,
161 event: RuntimeEventEnvelope,
162 ) -> Result<(), RuntimeControlPlaneError>;
163
164 async fn retire(
166 &self,
167 runtime_id: &LogicalRuntimeId,
168 ) -> Result<RetireReport, RuntimeControlPlaneError>;
169
170 async fn recycle(
172 &self,
173 runtime_id: &LogicalRuntimeId,
174 ) -> Result<RecycleReport, RuntimeControlPlaneError>;
175
176 async fn reset(
178 &self,
179 runtime_id: &LogicalRuntimeId,
180 ) -> Result<ResetReport, RuntimeControlPlaneError>;
181
182 async fn recover(
184 &self,
185 runtime_id: &LogicalRuntimeId,
186 ) -> Result<RecoveryReport, RuntimeControlPlaneError>;
187
188 async fn runtime_state(
190 &self,
191 runtime_id: &LogicalRuntimeId,
192 ) -> Result<RuntimeState, RuntimeControlPlaneError>;
193
194 async fn destroy(
196 &self,
197 runtime_id: &LogicalRuntimeId,
198 ) -> Result<DestroyReport, RuntimeControlPlaneError>;
199
200 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 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}