use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorCode {
ParseError = -32700,
InvalidRequest = -32600,
MethodNotFound = -32601,
InvalidParams = -32602,
InternalError = -32603,
SessionNotFound = -32001,
SessionAlreadyExists = -32002,
NotConnected = -32003,
AuthRequired = -32004,
InvalidMode = -32005,
Cancelled = -32006,
ConnectionFailed = -32007,
StreamingError = -32008,
ToolFailed = -32009,
ConfigError = -32010,
}
impl ErrorCode {
pub fn code(self) -> i32 {
self as i32
}
}
#[derive(Debug, Error)]
pub enum AgentError {
#[error("Session not found: {0}")]
SessionNotFound(String),
#[error("Session already exists: {0}")]
SessionAlreadyExists(String),
#[error("Session is closed: {0}")]
SessionClosed(String),
#[error("Client not connected")]
NotConnected,
#[error("Connection failed: {0}")]
ConnectionFailed(String),
#[error("Connection timeout after {0}ms")]
ConnectionTimeout(u64),
#[error("Already connected")]
AlreadyConnected,
#[error("Authentication required")]
AuthRequired,
#[error("Invalid API key")]
InvalidApiKey,
#[error("Invalid mode: {0}")]
InvalidMode(String),
#[error("Prompt cannot be empty")]
EmptyPrompt,
#[error("Prompt exceeds maximum length: {length} > {max}")]
PromptTooLong { length: usize, max: usize },
#[error("Streaming error: {0}")]
StreamingError(String),
#[error("Failed to send notification: {0}")]
NotificationFailed(String),
#[error("Tool execution failed: {0}")]
ToolExecutionFailed(String),
#[error("Tool not found: {0}")]
ToolNotFound(String),
#[error("Tool permission denied: {0}")]
ToolPermissionDenied(String),
#[error("Configuration error: {0}")]
ConfigError(String),
#[error("Missing required configuration: {0}")]
MissingConfig(String),
#[error("Claude SDK error: {0}")]
ClaudeSdk(#[from] claude_code_agent_sdk::ClaudeError),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
#[error("Internal error: {0}")]
Internal(String),
#[error("Operation cancelled")]
Cancelled,
}
pub type Result<T> = std::result::Result<T, AgentError>;
impl AgentError {
pub fn error_code(&self) -> ErrorCode {
match self {
AgentError::SessionNotFound(_) => ErrorCode::SessionNotFound,
AgentError::SessionAlreadyExists(_) => ErrorCode::SessionAlreadyExists,
AgentError::SessionClosed(_) => ErrorCode::SessionNotFound,
AgentError::NotConnected => ErrorCode::NotConnected,
AgentError::ConnectionFailed(_) => ErrorCode::ConnectionFailed,
AgentError::ConnectionTimeout(_) => ErrorCode::ConnectionFailed,
AgentError::AlreadyConnected => ErrorCode::InternalError,
AgentError::AuthRequired => ErrorCode::AuthRequired,
AgentError::InvalidApiKey => ErrorCode::AuthRequired,
AgentError::InvalidMode(_) => ErrorCode::InvalidMode,
AgentError::EmptyPrompt => ErrorCode::InvalidParams,
AgentError::PromptTooLong { .. } => ErrorCode::InvalidParams,
AgentError::StreamingError(_) => ErrorCode::StreamingError,
AgentError::NotificationFailed(_) => ErrorCode::StreamingError,
AgentError::ToolExecutionFailed(_) => ErrorCode::ToolFailed,
AgentError::ToolNotFound(_) => ErrorCode::ToolFailed,
AgentError::ToolPermissionDenied(_) => ErrorCode::ToolFailed,
AgentError::ConfigError(_) => ErrorCode::ConfigError,
AgentError::MissingConfig(_) => ErrorCode::ConfigError,
AgentError::ClaudeSdk(_) => ErrorCode::InternalError,
AgentError::Io(_) => ErrorCode::InternalError,
AgentError::Json(_) => ErrorCode::ParseError,
AgentError::Internal(_) => ErrorCode::InternalError,
AgentError::Cancelled => ErrorCode::Cancelled,
}
}
pub fn is_retryable(&self) -> bool {
matches!(
self,
AgentError::ConnectionFailed(_)
| AgentError::ConnectionTimeout(_)
| AgentError::StreamingError(_)
| AgentError::NotificationFailed(_)
)
}
pub fn is_client_error(&self) -> bool {
matches!(
self,
AgentError::SessionNotFound(_)
| AgentError::InvalidMode(_)
| AgentError::EmptyPrompt
| AgentError::PromptTooLong { .. }
| AgentError::ToolNotFound(_)
| AgentError::ToolPermissionDenied(_)
)
}
pub fn internal(msg: impl Into<String>) -> Self {
AgentError::Internal(msg.into())
}
pub fn session_not_found(session_id: impl Into<String>) -> Self {
AgentError::SessionNotFound(session_id.into())
}
pub fn session_already_exists(session_id: impl Into<String>) -> Self {
AgentError::SessionAlreadyExists(session_id.into())
}
pub fn invalid_mode(mode: impl Into<String>) -> Self {
AgentError::InvalidMode(mode.into())
}
pub fn tool_failed(msg: impl Into<String>) -> Self {
AgentError::ToolExecutionFailed(msg.into())
}
pub fn connection_failed(msg: impl Into<String>) -> Self {
AgentError::ConnectionFailed(msg.into())
}
pub fn streaming_error(msg: impl Into<String>) -> Self {
AgentError::StreamingError(msg.into())
}
pub fn notification_failed(msg: impl Into<String>) -> Self {
AgentError::NotificationFailed(msg.into())
}
pub fn config_error(msg: impl Into<String>) -> Self {
AgentError::ConfigError(msg.into())
}
pub fn missing_config(key: impl Into<String>) -> Self {
AgentError::MissingConfig(key.into())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_display() {
let err = AgentError::session_not_found("test-123");
assert_eq!(err.to_string(), "Session not found: test-123");
let err = AgentError::invalid_mode("unknown");
assert_eq!(err.to_string(), "Invalid mode: unknown");
}
#[test]
fn test_error_codes() {
let err = AgentError::session_not_found("test");
assert_eq!(err.error_code(), ErrorCode::SessionNotFound);
assert_eq!(err.error_code().code(), -32001);
let err = AgentError::NotConnected;
assert_eq!(err.error_code(), ErrorCode::NotConnected);
let err = AgentError::Cancelled;
assert_eq!(err.error_code(), ErrorCode::Cancelled);
}
#[test]
fn test_is_retryable() {
assert!(AgentError::connection_failed("timeout").is_retryable());
assert!(AgentError::streaming_error("lost").is_retryable());
assert!(!AgentError::session_not_found("x").is_retryable());
assert!(!AgentError::Cancelled.is_retryable());
}
#[test]
fn test_is_client_error() {
assert!(AgentError::session_not_found("x").is_client_error());
assert!(AgentError::invalid_mode("bad").is_client_error());
assert!(AgentError::EmptyPrompt.is_client_error());
assert!(!AgentError::NotConnected.is_client_error());
assert!(!AgentError::internal("oops").is_client_error());
}
#[test]
fn test_prompt_too_long() {
let err = AgentError::PromptTooLong {
length: 100_000,
max: 50_000,
};
assert_eq!(
err.to_string(),
"Prompt exceeds maximum length: 100000 > 50000"
);
assert!(err.is_client_error());
}
#[test]
fn test_constructor_helpers() {
assert!(matches!(
AgentError::session_already_exists("sess-1"),
AgentError::SessionAlreadyExists(_)
));
assert!(matches!(
AgentError::connection_failed("refused"),
AgentError::ConnectionFailed(_)
));
assert!(matches!(
AgentError::streaming_error("disconnected"),
AgentError::StreamingError(_)
));
assert!(matches!(
AgentError::notification_failed("timeout"),
AgentError::NotificationFailed(_)
));
assert!(matches!(
AgentError::config_error("invalid format"),
AgentError::ConfigError(_)
));
assert!(matches!(
AgentError::missing_config("ANTHROPIC_API_KEY"),
AgentError::MissingConfig(_)
));
}
}