1use 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#[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("Internal error: {0}")]
33 Internal(String),
34}
35
36#[derive(Debug, Clone, thiserror::Error)]
38#[non_exhaustive]
39pub enum RuntimeControlPlaneError {
40 #[error("Runtime not found: {0}")]
42 NotFound(LogicalRuntimeId),
43
44 #[error("Invalid state for operation: {state}")]
46 InvalidState { state: RuntimeState },
47
48 #[error("Store error: {0}")]
50 StoreError(String),
51
52 #[error("Internal error: {0}")]
54 Internal(String),
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
59#[serde(tag = "command", rename_all = "snake_case")]
60pub enum RuntimeControlCommand {
61 Stop,
63 Resume,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct RecoveryReport {
70 pub inputs_recovered: usize,
72 pub inputs_abandoned: usize,
74 pub inputs_requeued: usize,
76 #[serde(default, skip_serializing_if = "Vec::is_empty")]
78 pub details: Vec<String>,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct RetireReport {
84 pub inputs_abandoned: usize,
86 #[serde(default)]
88 pub inputs_pending_drain: usize,
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct ResetReport {
94 pub inputs_abandoned: usize,
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct RecycleReport {
101 pub inputs_transferred: usize,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct DestroyReport {
108 pub inputs_abandoned: usize,
110}
111
112#[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 async fn accept_input(&mut self, input: Input) -> Result<AcceptOutcome, RuntimeDriverError>;
121
122 async fn on_runtime_event(
124 &mut self,
125 event: RuntimeEventEnvelope,
126 ) -> Result<(), RuntimeDriverError>;
127
128 async fn on_run_event(&mut self, event: RunEvent) -> Result<(), RuntimeDriverError>;
130
131 async fn on_runtime_control(
133 &mut self,
134 command: RuntimeControlCommand,
135 ) -> Result<(), RuntimeDriverError>;
136
137 async fn recover(&mut self) -> Result<RecoveryReport, RuntimeDriverError>;
139
140 async fn retire(&mut self) -> Result<RetireReport, RuntimeDriverError>;
142
143 async fn reset(&mut self) -> Result<ResetReport, RuntimeDriverError>;
145
146 async fn destroy(&mut self) -> Result<DestroyReport, RuntimeDriverError>;
148
149 fn runtime_state(&self) -> RuntimeState;
151
152 fn input_state(&self, input_id: &InputId) -> Option<&InputState>;
154
155 fn active_input_ids(&self) -> Vec<InputId>;
157}
158
159#[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 async fn ingest(
165 &self,
166 runtime_id: &LogicalRuntimeId,
167 input: Input,
168 ) -> Result<AcceptOutcome, RuntimeControlPlaneError>;
169
170 async fn publish_event(
172 &self,
173 event: RuntimeEventEnvelope,
174 ) -> Result<(), RuntimeControlPlaneError>;
175
176 async fn retire(
178 &self,
179 runtime_id: &LogicalRuntimeId,
180 ) -> Result<RetireReport, RuntimeControlPlaneError>;
181
182 async fn recycle(
184 &self,
185 runtime_id: &LogicalRuntimeId,
186 ) -> Result<RecycleReport, RuntimeControlPlaneError>;
187
188 async fn reset(
190 &self,
191 runtime_id: &LogicalRuntimeId,
192 ) -> Result<ResetReport, RuntimeControlPlaneError>;
193
194 async fn recover(
196 &self,
197 runtime_id: &LogicalRuntimeId,
198 ) -> Result<RecoveryReport, RuntimeControlPlaneError>;
199
200 async fn runtime_state(
202 &self,
203 runtime_id: &LogicalRuntimeId,
204 ) -> Result<RuntimeState, RuntimeControlPlaneError>;
205
206 async fn destroy(
208 &self,
209 runtime_id: &LogicalRuntimeId,
210 ) -> Result<DestroyReport, RuntimeControlPlaneError>;
211
212 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 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}