use thiserror::Error;
#[derive(Error, Debug)]
pub enum CCSwarmError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("JSON error: {0}")]
SerdeJson(#[from] serde_json::Error),
#[error("Configuration error: {message}")]
Configuration {
message: String,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
#[error("Agent error [{agent_id}]: {message}")]
Agent {
agent_id: String,
message: String,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
#[error("Session error [{session_id}]: {message}")]
Session {
session_id: String,
message: String,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
#[error("Task error [{task_id}]: {message}")]
Task {
task_id: String,
message: String,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
#[error("Network error: {message}")]
Network {
message: String,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
#[error("Orchestrator error: {message}")]
Orchestrator {
message: String,
task_id: Option<String>,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
#[error("Git error: {message}")]
Git {
message: String,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
#[error("Template error: {message}")]
Template {
message: String,
template_name: Option<String>,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
#[error("Extension error [{extension_id}]: {message}")]
Extension {
extension_id: String,
message: String,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
#[error("Resource error: {message}")]
Resource {
message: String,
resource_type: Option<String>,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
#[error("Authentication error: {message}")]
Auth {
message: String,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
#[error("{message}")]
UserError {
message: String,
suggestion: Option<String>,
},
#[error("{message}")]
Other {
message: String,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
}
impl From<String> for CCSwarmError {
fn from(error: String) -> Self {
Self::Other {
message: error,
source: None,
}
}
}
impl From<&str> for CCSwarmError {
fn from(error: &str) -> Self {
Self::Other {
message: error.to_string(),
source: None,
}
}
}
pub type Result<T> = std::result::Result<T, CCSwarmError>;
pub trait ErrorContext<T> {
fn with_context<F>(self, f: F) -> Result<T>
where
F: FnOnce() -> String;
fn with_context_and_source<F, E>(self, f: F, source: E) -> Result<T>
where
F: FnOnce() -> String,
E: std::error::Error + Send + Sync + 'static;
}
impl<T, E> ErrorContext<T> for std::result::Result<T, E>
where
E: std::error::Error + Send + Sync + 'static,
{
fn with_context<F>(self, f: F) -> Result<T>
where
F: FnOnce() -> String,
{
self.map_err(|e| CCSwarmError::Other {
message: f(),
source: Some(Box::new(e)),
})
}
fn with_context_and_source<F, S>(self, f: F, source: S) -> Result<T>
where
F: FnOnce() -> String,
S: std::error::Error + Send + Sync + 'static,
{
self.map_err(|_| CCSwarmError::Other {
message: f(),
source: Some(Box::new(source)),
})
}
}
impl CCSwarmError {
pub fn config<S: Into<String>>(message: S) -> Self {
Self::Configuration {
message: message.into(),
source: None,
}
}
pub fn agent<S: Into<String>, I: Into<String>>(agent_id: I, message: S) -> Self {
Self::Agent {
agent_id: agent_id.into(),
message: message.into(),
source: None,
}
}
pub fn session<S: Into<String>, I: Into<String>>(session_id: I, message: S) -> Self {
Self::Session {
session_id: session_id.into(),
message: message.into(),
source: None,
}
}
pub fn orchestrator<S: Into<String>>(message: S, task_id: Option<String>) -> Self {
Self::Orchestrator {
message: message.into(),
task_id,
source: None,
}
}
pub fn task<S: Into<String>, I: Into<String>>(task_id: I, message: S) -> Self {
Self::Task {
task_id: task_id.into(),
message: message.into(),
source: None,
}
}
pub fn network<S: Into<String>>(message: S) -> Self {
Self::Network {
message: message.into(),
source: None,
}
}
pub fn git<S: Into<String>>(message: S) -> Self {
Self::Git {
message: message.into(),
source: None,
}
}
pub fn template<S: Into<String>>(message: S) -> Self {
Self::Template {
message: message.into(),
template_name: None,
source: None,
}
}
pub fn template_with_name<S: Into<String>, N: Into<String>>(
message: S,
template_name: N,
) -> Self {
Self::Template {
message: message.into(),
template_name: Some(template_name.into()),
source: None,
}
}
pub fn extension<S: Into<String>, I: Into<String>>(extension_id: I, message: S) -> Self {
Self::Extension {
extension_id: extension_id.into(),
message: message.into(),
source: None,
}
}
pub fn resource<S: Into<String>>(message: S) -> Self {
Self::Resource {
message: message.into(),
resource_type: None,
source: None,
}
}
pub fn resource_with_type<S: Into<String>, T: Into<String>>(
message: S,
resource_type: T,
) -> Self {
Self::Resource {
message: message.into(),
resource_type: Some(resource_type.into()),
source: None,
}
}
pub fn auth<S: Into<String>>(message: S) -> Self {
Self::Auth {
message: message.into(),
source: None,
}
}
pub fn user_error<S: Into<String>>(message: S) -> Self {
Self::UserError {
message: message.into(),
suggestion: None,
}
}
pub fn user_error_with_suggestion<S: Into<String>, T: Into<String>>(
message: S,
suggestion: T,
) -> Self {
Self::UserError {
message: message.into(),
suggestion: Some(suggestion.into()),
}
}
pub fn with_source<E>(mut self, source: E) -> Self
where
E: std::error::Error + Send + Sync + 'static,
{
match &mut self {
Self::Configuration { source: s, .. }
| Self::Agent { source: s, .. }
| Self::Session { source: s, .. }
| Self::Task { source: s, .. }
| Self::Network { source: s, .. }
| Self::Git { source: s, .. }
| Self::Template { source: s, .. }
| Self::Extension { source: s, .. }
| Self::Resource { source: s, .. }
| Self::Auth { source: s, .. }
| Self::Other { source: s, .. } => {
*s = Some(Box::new(source));
}
_ => {}
}
self
}
pub fn is_recoverable(&self) -> bool {
matches!(
self,
Self::Network { .. } | Self::Io(_) | Self::Task { .. } | Self::Resource { .. }
)
}
pub fn should_retry(&self) -> bool {
match self {
Self::Network { .. } | Self::Resource { .. } => true,
Self::Io(io_err) => {
matches!(
io_err.kind(),
std::io::ErrorKind::ConnectionReset
| std::io::ErrorKind::ConnectionAborted
| std::io::ErrorKind::BrokenPipe
| std::io::ErrorKind::TimedOut
| std::io::ErrorKind::Interrupted
| std::io::ErrorKind::WouldBlock
)
}
_ => false,
}
}
pub fn suggested_retry_delay(&self) -> std::time::Duration {
match self {
Self::Network { .. } => std::time::Duration::from_secs(1),
Self::Resource { .. } => std::time::Duration::from_secs(2),
Self::Io(_) => std::time::Duration::from_millis(500),
_ => std::time::Duration::from_secs(1),
}
}
pub fn max_retries(&self) -> u32 {
if !self.should_retry() {
return 0;
}
match self {
Self::Network { .. } => 3,
Self::Resource { .. } => 5,
Self::Io(_) => 2,
_ => 0,
}
}
pub fn severity(&self) -> ErrorSeverity {
match self {
Self::Auth { .. } | Self::Configuration { .. } => ErrorSeverity::Critical,
Self::Agent { .. }
| Self::Session { .. }
| Self::Extension { .. }
| Self::Orchestrator { .. } => ErrorSeverity::High,
Self::Task { .. } | Self::Git { .. } | Self::Template { .. } => ErrorSeverity::Medium,
Self::Network { .. } | Self::Resource { .. } => ErrorSeverity::Low,
Self::Io(_) | Self::SerdeJson(_) => ErrorSeverity::Medium,
Self::UserError { .. } => ErrorSeverity::Info,
Self::Other { .. } => ErrorSeverity::Medium,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ErrorSeverity {
Info,
Low,
Medium,
High,
Critical,
}
impl std::fmt::Display for ErrorSeverity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Info => write!(f, "INFO"),
Self::Low => write!(f, "LOW"),
Self::Medium => write!(f, "MEDIUM"),
Self::High => write!(f, "HIGH"),
Self::Critical => write!(f, "CRITICAL"),
}
}
}
pub trait FatalError: Sized {
fn fatal(self) -> ClassifiedError<Self>;
fn non_fatal(self) -> ClassifiedError<Self>;
}
#[derive(Debug)]
pub struct ClassifiedError<E> {
pub error: E,
pub is_fatal: bool,
}
impl<E> ClassifiedError<E> {
pub fn fatal(error: E) -> Self {
Self {
error,
is_fatal: true,
}
}
pub fn non_fatal(error: E) -> Self {
Self {
error,
is_fatal: false,
}
}
pub fn is_fatal(&self) -> bool {
self.is_fatal
}
pub fn into_inner(self) -> E {
self.error
}
pub fn map<F, O>(self, f: F) -> ClassifiedError<O>
where
F: FnOnce(E) -> O,
{
ClassifiedError {
error: f(self.error),
is_fatal: self.is_fatal,
}
}
}
impl<E: std::fmt::Display> std::fmt::Display for ClassifiedError<E> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.is_fatal {
write!(f, "[FATAL] {}", self.error)
} else {
write!(f, "{}", self.error)
}
}
}
impl<E: std::error::Error + 'static> std::error::Error for ClassifiedError<E> {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.error)
}
}
impl<E> FatalError for E {
fn fatal(self) -> ClassifiedError<Self> {
ClassifiedError::fatal(self)
}
fn non_fatal(self) -> ClassifiedError<Self> {
ClassifiedError::non_fatal(self)
}
}
pub trait ResultFatalExt<T, E> {
fn fatal_on_err(self) -> std::result::Result<T, ClassifiedError<E>>;
fn non_fatal_on_err(self) -> std::result::Result<T, ClassifiedError<E>>;
}
impl<T, E> ResultFatalExt<T, E> for std::result::Result<T, E> {
fn fatal_on_err(self) -> std::result::Result<T, ClassifiedError<E>> {
self.map_err(ClassifiedError::fatal)
}
fn non_fatal_on_err(self) -> std::result::Result<T, ClassifiedError<E>> {
self.map_err(ClassifiedError::non_fatal)
}
}
impl CCSwarmError {
pub fn to_user_error(&self) -> crate::utils::user_error::UserError {
use crate::utils::user_error::UserError;
match self {
Self::Configuration { message, .. } => {
UserError::new("Configuration Error", message.as_str())
.with_suggestion("Check your ccswarm.json file. Run 'ccswarm doctor' to diagnose configuration issues.")
}
Self::Agent { agent_id, message, .. } => {
UserError::new(format!("Agent Error [{}]", agent_id), message.as_str())
.with_suggestion(format!(
"Check agent '{}' status with 'ccswarm status --agent {}'. Try 'ccswarm doctor --fix' to resolve common issues.",
agent_id, agent_id
))
}
Self::Session { session_id, message, .. } => {
UserError::new(format!("Session Error [{}]", session_id), message.as_str())
.with_suggestion("Run 'ccswarm session list' to check active sessions. Try restarting with 'ccswarm stop && ccswarm start'.")
}
Self::Task { task_id, message, .. } => {
UserError::new(format!("Task Error [{}]", task_id), message.as_str())
.with_suggestion(format!(
"Check task status with 'ccswarm task list'. Retry with 'ccswarm task retry {}'.",
task_id
))
}
Self::Network { message, .. } => {
UserError::new("Network Error", message.as_str())
.with_suggestion("Check your network connection and API keys. Run 'ccswarm doctor --check-api' to test connectivity.")
}
Self::Git { message, .. } => {
UserError::new("Git Error", message.as_str())
.with_suggestion("Ensure you're in a Git repository. Run 'git status' to check, or 'ccswarm doctor' for diagnostics.")
}
Self::Auth { message, .. } => {
UserError::new("Authentication Error", message.as_str())
.with_suggestion("Verify your API key is set: ANTHROPIC_API_KEY for Claude. Run 'ccswarm doctor --check-api'.")
}
Self::Orchestrator { message, .. } => {
UserError::new("Orchestrator Error", message.as_str())
.with_suggestion("Try restarting the orchestrator: 'ccswarm stop && ccswarm start'.")
}
Self::Resource { message, resource_type, .. } => {
let detail = resource_type.as_deref().unwrap_or("unknown");
UserError::new(format!("Resource Error ({})", detail), message.as_str())
.with_suggestion("Check system resources with 'ccswarm health --resources'. Close unnecessary processes and retry.")
}
Self::Io(io_err) => {
UserError::new("I/O Error", &io_err.to_string())
.with_suggestion("Check file permissions and disk space. Ensure the target path exists.")
}
Self::SerdeJson(json_err) => {
UserError::new("JSON Parse Error", &json_err.to_string())
.with_suggestion("Check your configuration file for syntax errors. Use a JSON validator to find issues.")
}
Self::UserError { message, suggestion } => {
let err = UserError::new("Error", message.as_str());
if let Some(s) = suggestion {
err.with_suggestion(s.clone())
} else {
err
}
}
Self::Template { message, template_name, .. } => {
let title = match template_name {
Some(name) => format!("Template Error [{}]", name),
None => "Template Error".to_string(),
};
UserError::new(title, message.as_str())
.with_suggestion("Check available templates with 'ccswarm template list'.")
}
Self::Extension { extension_id, message, .. } => {
UserError::new(format!("Extension Error [{}]", extension_id), message.as_str())
.with_suggestion("Run 'ccswarm extend list' to check extension status.")
}
Self::Other { message, .. } => {
UserError::new("Error", message.as_str())
.with_suggestion("Run 'ccswarm doctor' for diagnostics. Use '--verbose' for more details.")
}
}
}
}
impl CCSwarmError {
pub fn is_fatal(&self) -> bool {
matches!(
self,
Self::Auth { .. } | Self::Configuration { .. } | Self::Orchestrator { .. }
)
}
pub fn classify(self) -> ClassifiedError<Self> {
if self.is_fatal() {
ClassifiedError::fatal(self)
} else {
ClassifiedError::non_fatal(self)
}
}
}