use crate::types::SessionId;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum RlmError {
#[error("Session not found: {session_id}")]
SessionNotFound { session_id: SessionId },
#[error("Session expired: {session_id}")]
SessionExpired { session_id: SessionId },
#[error("Session {session_id} is in invalid state {state} for operation {operation}")]
InvalidSessionState {
session_id: SessionId,
state: String,
operation: String,
},
#[error("Session {session_id} has reached maximum extensions ({max})")]
MaxExtensionsReached { session_id: SessionId, max: u32 },
#[error("Token budget exceeded: used {used} of {budget} tokens")]
TokenBudgetExceeded { used: u64, budget: u64 },
#[error("Time budget exceeded: used {used_ms}ms of {budget_ms}ms")]
TimeBudgetExceeded { used_ms: u64, budget_ms: u64 },
#[error("Recursion depth limit exceeded: {depth} >= {max_depth}")]
RecursionDepthExceeded { depth: u32, max_depth: u32 },
#[error("Code execution failed: {message}")]
ExecutionFailed {
message: String,
exit_code: Option<i32>,
stdout: Option<String>,
stderr: Option<String>,
},
#[error("Failed to parse command from LLM output: {message}")]
CommandParseFailed { message: String },
#[error("Execution timed out after {timeout_ms}ms")]
ExecutionTimeout { timeout_ms: u64 },
#[error("VM crashed: {message}")]
VmCrashed { message: String },
#[error("KG validation failed: unknown terms {unknown_terms:?}")]
KgValidationFailed { unknown_terms: Vec<String> },
#[error("KG validation requires user approval for terms: {unknown_terms:?}")]
KgEscalationRequired {
unknown_terms: Vec<String>,
suggested_action: String,
context: String,
},
#[error("Snapshot not found: {snapshot_id}")]
SnapshotNotFound { snapshot_id: String },
#[error("Maximum snapshots ({max}) reached for session")]
MaxSnapshotsReached { max: u32 },
#[error("Failed to create snapshot: {message}")]
SnapshotCreationFailed { message: String },
#[error("Failed to restore snapshot: {message}")]
SnapshotRestoreFailed { message: String },
#[error("No execution backend available. Tried: {tried:?}")]
NoBackendAvailable { tried: Vec<String> },
#[error("Failed to initialize {backend} backend: {message}")]
BackendInitFailed { backend: String, message: String },
#[error(
"VM pool exhausted: all {pool_size} VMs busy, overflow at capacity ({overflow_count}/{max_overflow})"
)]
PoolExhausted {
pool_size: u32,
overflow_count: u32,
max_overflow: u32,
},
#[error("VM allocation timed out after {timeout_ms}ms")]
VmAllocationTimeout { timeout_ms: u64 },
#[error("DNS query blocked: {domain} not in allowlist")]
DnsBlocked { domain: String },
#[error("Network request blocked: {url}")]
NetworkBlocked { url: String },
#[error("LLM call failed: {message}")]
LlmCallFailed { message: String },
#[error(
"No LLM client configured. Enable the `llm` feature (--features llm) and set OPENROUTER_API_KEY or run Ollama on localhost:11434."
)]
LlmNotConfigured,
#[error("LLM bridge authentication failed: invalid session token")]
LlmBridgeAuthFailed,
#[error("Invalid session token: {token}")]
InvalidSessionToken { token: String },
#[error("Batch size {size} exceeds maximum {max}")]
BatchSizeTooLarge { size: usize, max: usize },
#[error("Output exceeds inline limit ({size} > {limit} bytes), streamed to {file_path}")]
OutputTooLarge {
size: u64,
limit: u64,
file_path: String,
},
#[error("Auto-remediation failed after {attempts} attempts: {message}")]
AutoRemediationFailed { attempts: u32, message: String },
#[error("Failed to send alert to webhook: {message}")]
AlertWebhookFailed { message: String },
#[error("Configuration error: {message}")]
ConfigError { message: String },
#[error("Backend '{backend}' does not support operation '{op}'")]
NotSupported { backend: String, op: String },
#[error("Internal error: {message}")]
Internal { message: String },
#[error("Operation cancelled")]
Cancelled,
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Serialization error: {0}")]
Serialization(#[from] serde_json::Error),
}
impl RlmError {
pub fn is_retryable(&self) -> bool {
matches!(
self,
RlmError::ExecutionTimeout { .. }
| RlmError::VmAllocationTimeout { .. }
| RlmError::LlmCallFailed { .. }
| RlmError::AlertWebhookFailed { .. }
)
}
pub fn is_budget_exhausted(&self) -> bool {
matches!(
self,
RlmError::TokenBudgetExceeded { .. }
| RlmError::TimeBudgetExceeded { .. }
| RlmError::RecursionDepthExceeded { .. }
)
}
pub fn requires_user_action(&self) -> bool {
matches!(
self,
RlmError::KgEscalationRequired { .. }
| RlmError::MaxExtensionsReached { .. }
| RlmError::NoBackendAvailable { .. }
)
}
pub fn to_mcp_error(&self) -> McpError {
McpError {
code: self.mcp_error_code(),
message: self.to_string(),
data: self.mcp_error_data(),
}
}
fn mcp_error_code(&self) -> i32 {
match self {
RlmError::CommandParseFailed { .. } => -32700, RlmError::ConfigError { .. } => -32602,
RlmError::SessionNotFound { .. } => -32001,
RlmError::SessionExpired { .. } => -32002,
RlmError::TokenBudgetExceeded { .. } => -32010,
RlmError::TimeBudgetExceeded { .. } => -32011,
RlmError::RecursionDepthExceeded { .. } => -32012,
RlmError::ExecutionFailed { .. } => -32020,
RlmError::ExecutionTimeout { .. } => -32021,
RlmError::VmCrashed { .. } => -32022,
RlmError::KgValidationFailed { .. } => -32030,
RlmError::KgEscalationRequired { .. } => -32031,
RlmError::SnapshotNotFound { .. } => -32040,
RlmError::NoBackendAvailable { .. } => -32050,
RlmError::DnsBlocked { .. } => -32060,
RlmError::NotSupported { .. } => -32070,
RlmError::Cancelled => -32099,
_ => -32000, }
}
fn mcp_error_data(&self) -> Option<serde_json::Value> {
match self {
RlmError::KgEscalationRequired {
unknown_terms,
suggested_action,
context,
} => Some(serde_json::json!({
"unknown_terms": unknown_terms,
"suggested_action": suggested_action,
"context": context,
})),
RlmError::ExecutionFailed {
exit_code,
stdout,
stderr,
..
} => Some(serde_json::json!({
"exit_code": exit_code,
"stdout": stdout,
"stderr": stderr,
})),
RlmError::OutputTooLarge {
size,
limit,
file_path,
} => Some(serde_json::json!({
"size": size,
"limit": limit,
"file_path": file_path,
})),
_ => None,
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct McpError {
pub code: i32,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<serde_json::Value>,
}
pub type RlmResult<T> = Result<T, RlmError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_retryable() {
let retryable = RlmError::ExecutionTimeout { timeout_ms: 1000 };
assert!(retryable.is_retryable());
let not_retryable = RlmError::Cancelled;
assert!(!not_retryable.is_retryable());
}
#[test]
fn test_error_budget_exhausted() {
let budget = RlmError::TokenBudgetExceeded {
used: 100,
budget: 50,
};
assert!(budget.is_budget_exhausted());
let not_budget = RlmError::Cancelled;
assert!(!not_budget.is_budget_exhausted());
}
#[test]
fn test_not_supported_not_retryable_and_has_code() {
let err = RlmError::NotSupported {
backend: "local".to_string(),
op: "create_snapshot".to_string(),
};
assert!(!err.is_retryable());
assert!(!err.is_budget_exhausted());
assert_eq!(err.to_mcp_error().code, -32070);
let display = err.to_string();
assert!(display.contains("local"));
assert!(display.contains("create_snapshot"));
}
#[test]
fn test_mcp_error_conversion() {
let error = RlmError::KgEscalationRequired {
unknown_terms: vec!["foo".to_string(), "bar".to_string()],
suggested_action: "approve".to_string(),
context: "testing".to_string(),
};
let mcp = error.to_mcp_error();
assert_eq!(mcp.code, -32031);
assert!(mcp.data.is_some());
}
}