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 RespawnReport {
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 fn runtime_state(&self) -> RuntimeState;
148
149 fn input_state(&self, input_id: &InputId) -> Option<&InputState>;
151
152 fn active_input_ids(&self) -> Vec<InputId>;
154}
155
156#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
158#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
159pub trait RuntimeControlPlane: Send + Sync {
160 async fn ingest(
162 &self,
163 runtime_id: &LogicalRuntimeId,
164 input: Input,
165 ) -> Result<AcceptOutcome, RuntimeControlPlaneError>;
166
167 async fn publish_event(
169 &self,
170 event: RuntimeEventEnvelope,
171 ) -> Result<(), RuntimeControlPlaneError>;
172
173 async fn retire(
175 &self,
176 runtime_id: &LogicalRuntimeId,
177 ) -> Result<RetireReport, RuntimeControlPlaneError>;
178
179 async fn respawn(
181 &self,
182 runtime_id: &LogicalRuntimeId,
183 ) -> Result<RespawnReport, RuntimeControlPlaneError>;
184
185 async fn reset(
187 &self,
188 runtime_id: &LogicalRuntimeId,
189 ) -> Result<ResetReport, RuntimeControlPlaneError>;
190
191 async fn recover(
193 &self,
194 runtime_id: &LogicalRuntimeId,
195 ) -> Result<RecoveryReport, RuntimeControlPlaneError>;
196
197 async fn runtime_state(
199 &self,
200 runtime_id: &LogicalRuntimeId,
201 ) -> Result<RuntimeState, RuntimeControlPlaneError>;
202
203 async fn load_boundary_receipt(
205 &self,
206 runtime_id: &LogicalRuntimeId,
207 run_id: &RunId,
208 sequence: u64,
209 ) -> Result<Option<meerkat_core::lifecycle::RunBoundaryReceipt>, RuntimeControlPlaneError>;
210}
211
212#[cfg(test)]
213#[allow(clippy::unwrap_used)]
214mod tests {
215 use super::*;
216
217 fn _assert_driver_object_safe(_: &dyn RuntimeDriver) {}
219 fn _assert_control_plane_object_safe(_: &dyn RuntimeControlPlane) {}
220
221 #[test]
222 fn runtime_control_command_serde() {
223 let cmd = RuntimeControlCommand::Stop;
224 let json = serde_json::to_value(&cmd).unwrap();
225 assert_eq!(json["command"], "stop");
226
227 let cmd = RuntimeControlCommand::Resume;
228 let json = serde_json::to_value(&cmd).unwrap();
229 assert_eq!(json["command"], "resume");
230 }
231
232 #[test]
233 fn runtime_driver_error_display() {
234 let err = RuntimeDriverError::NotReady {
235 state: RuntimeState::Initializing,
236 };
237 assert!(err.to_string().contains("initializing"));
238
239 let err = RuntimeDriverError::ValidationFailed {
240 reason: "bad input".into(),
241 };
242 assert!(err.to_string().contains("bad input"));
243 }
244
245 #[test]
246 fn runtime_control_plane_error_display() {
247 let err = RuntimeControlPlaneError::NotFound(LogicalRuntimeId::new("missing"));
248 assert!(err.to_string().contains("missing"));
249 }
250
251 #[test]
252 fn recovery_report_serde() {
253 let report = RecoveryReport {
254 inputs_recovered: 5,
255 inputs_abandoned: 1,
256 inputs_requeued: 3,
257 details: vec!["requeued 3 staged inputs".into()],
258 };
259 let json = serde_json::to_value(&report).unwrap();
260 let parsed: RecoveryReport = serde_json::from_value(json).unwrap();
261 assert_eq!(parsed.inputs_recovered, 5);
262 }
263}