1use crate::ids::{FlowId, MeerkatId, ProfileName};
4use crate::runtime::MobState;
5use crate::validate::Diagnostic;
6use crate::{MobId, RunId, StepId};
7
8#[derive(Debug, thiserror::Error)]
10pub enum MobError {
11 #[error("profile not found: {0}")]
13 ProfileNotFound(ProfileName),
14
15 #[error("meerkat not found: {0}")]
17 MeerkatNotFound(MeerkatId),
18
19 #[error("meerkat already exists: {0}")]
21 MeerkatAlreadyExists(MeerkatId),
22
23 #[error("meerkat is not externally addressable: {0}")]
25 NotExternallyAddressable(MeerkatId),
26
27 #[error("invalid state transition: {from} -> {to}")]
29 InvalidTransition { from: MobState, to: MobState },
30
31 #[error("wiring error: {0}")]
33 WiringError(String),
34
35 #[error("member {member_id} failed to restore session {session_id}: {reason}")]
37 MemberRestoreFailed {
38 member_id: MeerkatId,
39 session_id: meerkat_core::types::SessionId,
40 reason: String,
41 },
42
43 #[error("kickoff wait timed out")]
45 KickoffWaitTimedOut { pending_member_ids: Vec<MeerkatId> },
46
47 #[error("definition error: {}", format_diagnostics(.0))]
49 DefinitionError(Vec<Diagnostic>),
50
51 #[error("flow not found: {0}")]
53 FlowNotFound(FlowId),
54
55 #[error("flow failed for run {run_id}: {reason}")]
57 FlowFailed { run_id: RunId, reason: String },
58
59 #[error("run not found: {0}")]
61 RunNotFound(RunId),
62
63 #[error("run canceled: {0}")]
65 RunCanceled(RunId),
66
67 #[error("flow turn timed out")]
69 FlowTurnTimedOut,
70
71 #[error("spec revision conflict for mob {mob_id}: expected {expected:?}, actual {actual}")]
73 SpecRevisionConflict {
74 mob_id: MobId,
75 expected: Option<u64>,
76 actual: u64,
77 },
78
79 #[error("schema validation failed for step {step_id}: {message}")]
81 SchemaValidation { step_id: StepId, message: String },
82
83 #[error("insufficient targets for step {step_id}: required {required}, available {available}")]
85 InsufficientTargets {
86 step_id: StepId,
87 required: u8,
88 available: usize,
89 },
90
91 #[error("topology violation: {from_role} -> {to_role}")]
93 TopologyViolation {
94 from_role: ProfileName,
95 to_role: ProfileName,
96 },
97
98 #[error("supervisor escalation: {0}")]
100 SupervisorEscalation(String),
101
102 #[error("unsupported for runtime mode {mode}: {reason}")]
104 UnsupportedForMode {
105 mode: crate::MobRuntimeMode,
106 reason: String,
107 },
108
109 #[error("reset barrier active")]
111 ResetBarrier,
112
113 #[error("storage error: {0}")]
115 StorageError(#[source] Box<dyn std::error::Error + Send + Sync>),
116
117 #[error("session error: {0}")]
119 SessionError(#[from] meerkat_core::service::SessionError),
120
121 #[error("comms error: {0}")]
123 CommsError(#[from] meerkat_core::comms::SendError),
124
125 #[error("callback pending for session {session_id} on tool '{tool_name}'")]
127 CallbackPending {
128 session_id: meerkat_core::types::SessionId,
129 tool_name: String,
130 args: serde_json::Value,
131 },
132
133 #[error("internal error: {0}")]
135 Internal(String),
136
137 #[error("not yet implemented: {0}")]
143 NotYetImplemented(String),
144}
145
146fn format_diagnostics(diagnostics: &[Diagnostic]) -> String {
147 diagnostics
148 .iter()
149 .map(|d| format!("{}: {}", d.code, d.message))
150 .collect::<Vec<_>>()
151 .join("; ")
152}
153
154impl From<Box<dyn std::error::Error + Send + Sync>> for MobError {
155 fn from(error: Box<dyn std::error::Error + Send + Sync>) -> Self {
156 Self::StorageError(error)
157 }
158}
159
160impl From<crate::store::MobStoreError> for MobError {
161 fn from(error: crate::store::MobStoreError) -> Self {
162 match error {
163 crate::store::MobStoreError::SpecRevisionConflict {
164 mob_id,
165 expected,
166 actual,
167 } => Self::SpecRevisionConflict {
168 mob_id,
169 expected,
170 actual,
171 },
172 other => Self::StorageError(Box::new(other)),
173 }
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180 use crate::validate::{Diagnostic, DiagnosticCode, DiagnosticSeverity};
181
182 #[test]
183 fn test_profile_not_found_display() {
184 let err = MobError::ProfileNotFound(ProfileName::from("missing"));
185 assert!(format!("{err}").contains("missing"));
186 }
187
188 #[test]
189 fn test_invalid_transition_display() {
190 let err = MobError::InvalidTransition {
191 from: MobState::Completed,
192 to: MobState::Running,
193 };
194 let msg = format!("{err}");
195 assert!(msg.contains("Completed"));
196 assert!(msg.contains("Running"));
197 }
198
199 #[test]
200 fn test_definition_error_display() {
201 let err = MobError::DefinitionError(vec![
202 Diagnostic {
203 code: DiagnosticCode::MissingSkillRef,
204 message: "skill 'foo' not found".to_string(),
205 location: Some("profiles.worker.skills[0]".to_string()),
206 severity: DiagnosticSeverity::Error,
207 },
208 Diagnostic {
209 code: DiagnosticCode::MissingMcpRef,
210 message: "mcp 'bar' not defined".to_string(),
211 location: Some("profiles.worker.tools.mcp[0]".to_string()),
212 severity: DiagnosticSeverity::Error,
213 },
214 ]);
215 let msg = format!("{err}");
216 assert!(msg.contains("missing_skill_ref"));
217 assert!(msg.contains("missing_mcp_ref"));
218 }
219
220 #[test]
221 fn test_session_error_from() {
222 let session_err = meerkat_core::service::SessionError::NotFound {
223 id: meerkat_core::types::SessionId::new(),
224 };
225 let mob_err: MobError = session_err.into();
226 assert!(matches!(mob_err, MobError::SessionError(_)));
227 }
228
229 #[test]
230 fn test_comms_error_from() {
231 let send_err = meerkat_core::comms::SendError::PeerNotFound("agent-1".to_string());
232 let mob_err: MobError = send_err.into();
233 assert!(matches!(mob_err, MobError::CommsError(_)));
234 }
235
236 #[test]
237 fn test_storage_error() {
238 let err = MobError::StorageError(Box::new(std::io::Error::new(
239 std::io::ErrorKind::Other,
240 "disk full",
241 )));
242 assert!(format!("{err}").contains("disk full"));
243 }
244
245 #[test]
246 fn test_all_variants_exist() {
247 let _variants: Vec<MobError> = vec![
249 MobError::ProfileNotFound(ProfileName::from("p")),
250 MobError::MeerkatNotFound(MeerkatId::from("m")),
251 MobError::MeerkatAlreadyExists(MeerkatId::from("m")),
252 MobError::NotExternallyAddressable(MeerkatId::from("m")),
253 MobError::InvalidTransition {
254 from: MobState::Creating,
255 to: MobState::Running,
256 },
257 MobError::WiringError("w".to_string()),
258 MobError::MemberRestoreFailed {
259 member_id: MeerkatId::from("m"),
260 session_id: meerkat_core::types::SessionId::new(),
261 reason: "restore failed".to_string(),
262 },
263 MobError::KickoffWaitTimedOut {
264 pending_member_ids: vec![MeerkatId::from("m")],
265 },
266 MobError::DefinitionError(vec![]),
267 MobError::FlowNotFound(FlowId::from("f")),
268 MobError::FlowFailed {
269 run_id: RunId::new(),
270 reason: "r".to_string(),
271 },
272 MobError::RunNotFound(RunId::new()),
273 MobError::RunCanceled(RunId::new()),
274 MobError::FlowTurnTimedOut,
275 MobError::SpecRevisionConflict {
276 mob_id: MobId::from("mob"),
277 expected: Some(2),
278 actual: 3,
279 },
280 MobError::SchemaValidation {
281 step_id: StepId::from("step"),
282 message: "invalid".to_string(),
283 },
284 MobError::InsufficientTargets {
285 step_id: StepId::from("step"),
286 required: 2,
287 available: 1,
288 },
289 MobError::TopologyViolation {
290 from_role: ProfileName::from("lead"),
291 to_role: ProfileName::from("worker"),
292 },
293 MobError::SupervisorEscalation("boom".to_string()),
294 MobError::UnsupportedForMode {
295 mode: crate::MobRuntimeMode::TurnDriven,
296 reason: "autonomous host runtime required".to_string(),
297 },
298 MobError::ResetBarrier,
299 MobError::StorageError(Box::new(std::io::Error::new(
300 std::io::ErrorKind::Other,
301 "e",
302 ))),
303 MobError::SessionError(meerkat_core::service::SessionError::PersistenceDisabled),
304 MobError::CommsError(meerkat_core::comms::SendError::PeerOffline),
305 MobError::Internal("i".to_string()),
306 MobError::NotYetImplemented("storage cas".to_string()),
307 ];
308 }
309}