use crate::ids::{FlowId, MeerkatId, ProfileName};
use crate::runtime::MobState;
use crate::validate::Diagnostic;
use crate::{MobId, RunId, StepId};
#[derive(Debug, thiserror::Error)]
pub enum MobError {
#[error("profile not found: {0}")]
ProfileNotFound(ProfileName),
#[error("meerkat not found: {0}")]
MeerkatNotFound(MeerkatId),
#[error("meerkat already exists: {0}")]
MeerkatAlreadyExists(MeerkatId),
#[error("meerkat is not externally addressable: {0}")]
NotExternallyAddressable(MeerkatId),
#[error("invalid state transition: {from} -> {to}")]
InvalidTransition { from: MobState, to: MobState },
#[error("wiring error: {0}")]
WiringError(String),
#[error("member {member_id} failed to restore session {session_id}: {reason}")]
MemberRestoreFailed {
member_id: MeerkatId,
session_id: meerkat_core::types::SessionId,
reason: String,
},
#[error("kickoff wait timed out")]
KickoffWaitTimedOut { pending_member_ids: Vec<MeerkatId> },
#[error("definition error: {}", format_diagnostics(.0))]
DefinitionError(Vec<Diagnostic>),
#[error("flow not found: {0}")]
FlowNotFound(FlowId),
#[error("flow failed for run {run_id}: {reason}")]
FlowFailed { run_id: RunId, reason: String },
#[error("run not found: {0}")]
RunNotFound(RunId),
#[error("run canceled: {0}")]
RunCanceled(RunId),
#[error("flow turn timed out")]
FlowTurnTimedOut,
#[error("spec revision conflict for mob {mob_id}: expected {expected:?}, actual {actual}")]
SpecRevisionConflict {
mob_id: MobId,
expected: Option<u64>,
actual: u64,
},
#[error("schema validation failed for step {step_id}: {message}")]
SchemaValidation { step_id: StepId, message: String },
#[error("insufficient targets for step {step_id}: required {required}, available {available}")]
InsufficientTargets {
step_id: StepId,
required: u8,
available: usize,
},
#[error("topology violation: {from_role} -> {to_role}")]
TopologyViolation {
from_role: ProfileName,
to_role: ProfileName,
},
#[error("supervisor escalation: {0}")]
SupervisorEscalation(String),
#[error("unsupported for runtime mode {mode}: {reason}")]
UnsupportedForMode {
mode: crate::MobRuntimeMode,
reason: String,
},
#[error("reset barrier active")]
ResetBarrier,
#[error("storage error: {0}")]
StorageError(#[source] Box<dyn std::error::Error + Send + Sync>),
#[error("session error: {0}")]
SessionError(#[from] meerkat_core::service::SessionError),
#[error("comms error: {0}")]
CommsError(#[from] meerkat_core::comms::SendError),
#[error("callback pending for session {session_id} on tool '{tool_name}'")]
CallbackPending {
session_id: meerkat_core::types::SessionId,
tool_name: String,
args: serde_json::Value,
},
#[error("internal error: {0}")]
Internal(String),
#[error("not yet implemented: {0}")]
NotYetImplemented(String),
}
fn format_diagnostics(diagnostics: &[Diagnostic]) -> String {
diagnostics
.iter()
.map(|d| format!("{}: {}", d.code, d.message))
.collect::<Vec<_>>()
.join("; ")
}
impl From<Box<dyn std::error::Error + Send + Sync>> for MobError {
fn from(error: Box<dyn std::error::Error + Send + Sync>) -> Self {
Self::StorageError(error)
}
}
impl From<crate::store::MobStoreError> for MobError {
fn from(error: crate::store::MobStoreError) -> Self {
match error {
crate::store::MobStoreError::SpecRevisionConflict {
mob_id,
expected,
actual,
} => Self::SpecRevisionConflict {
mob_id,
expected,
actual,
},
other => Self::StorageError(Box::new(other)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::validate::{Diagnostic, DiagnosticCode, DiagnosticSeverity};
#[test]
fn test_profile_not_found_display() {
let err = MobError::ProfileNotFound(ProfileName::from("missing"));
assert!(format!("{err}").contains("missing"));
}
#[test]
fn test_invalid_transition_display() {
let err = MobError::InvalidTransition {
from: MobState::Completed,
to: MobState::Running,
};
let msg = format!("{err}");
assert!(msg.contains("Completed"));
assert!(msg.contains("Running"));
}
#[test]
fn test_definition_error_display() {
let err = MobError::DefinitionError(vec![
Diagnostic {
code: DiagnosticCode::MissingSkillRef,
message: "skill 'foo' not found".to_string(),
location: Some("profiles.worker.skills[0]".to_string()),
severity: DiagnosticSeverity::Error,
},
Diagnostic {
code: DiagnosticCode::MissingMcpRef,
message: "mcp 'bar' not defined".to_string(),
location: Some("profiles.worker.tools.mcp[0]".to_string()),
severity: DiagnosticSeverity::Error,
},
]);
let msg = format!("{err}");
assert!(msg.contains("missing_skill_ref"));
assert!(msg.contains("missing_mcp_ref"));
}
#[test]
fn test_session_error_from() {
let session_err = meerkat_core::service::SessionError::NotFound {
id: meerkat_core::types::SessionId::new(),
};
let mob_err: MobError = session_err.into();
assert!(matches!(mob_err, MobError::SessionError(_)));
}
#[test]
fn test_comms_error_from() {
let send_err = meerkat_core::comms::SendError::PeerNotFound("agent-1".to_string());
let mob_err: MobError = send_err.into();
assert!(matches!(mob_err, MobError::CommsError(_)));
}
#[test]
fn test_storage_error() {
let err = MobError::StorageError(Box::new(std::io::Error::new(
std::io::ErrorKind::Other,
"disk full",
)));
assert!(format!("{err}").contains("disk full"));
}
#[test]
fn test_all_variants_exist() {
let _variants: Vec<MobError> = vec![
MobError::ProfileNotFound(ProfileName::from("p")),
MobError::MeerkatNotFound(MeerkatId::from("m")),
MobError::MeerkatAlreadyExists(MeerkatId::from("m")),
MobError::NotExternallyAddressable(MeerkatId::from("m")),
MobError::InvalidTransition {
from: MobState::Creating,
to: MobState::Running,
},
MobError::WiringError("w".to_string()),
MobError::MemberRestoreFailed {
member_id: MeerkatId::from("m"),
session_id: meerkat_core::types::SessionId::new(),
reason: "restore failed".to_string(),
},
MobError::KickoffWaitTimedOut {
pending_member_ids: vec![MeerkatId::from("m")],
},
MobError::DefinitionError(vec![]),
MobError::FlowNotFound(FlowId::from("f")),
MobError::FlowFailed {
run_id: RunId::new(),
reason: "r".to_string(),
},
MobError::RunNotFound(RunId::new()),
MobError::RunCanceled(RunId::new()),
MobError::FlowTurnTimedOut,
MobError::SpecRevisionConflict {
mob_id: MobId::from("mob"),
expected: Some(2),
actual: 3,
},
MobError::SchemaValidation {
step_id: StepId::from("step"),
message: "invalid".to_string(),
},
MobError::InsufficientTargets {
step_id: StepId::from("step"),
required: 2,
available: 1,
},
MobError::TopologyViolation {
from_role: ProfileName::from("lead"),
to_role: ProfileName::from("worker"),
},
MobError::SupervisorEscalation("boom".to_string()),
MobError::UnsupportedForMode {
mode: crate::MobRuntimeMode::TurnDriven,
reason: "autonomous host runtime required".to_string(),
},
MobError::ResetBarrier,
MobError::StorageError(Box::new(std::io::Error::new(
std::io::ErrorKind::Other,
"e",
))),
MobError::SessionError(meerkat_core::service::SessionError::PersistenceDisabled),
MobError::CommsError(meerkat_core::comms::SendError::PeerOffline),
MobError::Internal("i".to_string()),
MobError::NotYetImplemented("storage cas".to_string()),
];
}
}