use thiserror::Error;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum RuntimeError {
#[error("invalid request: {message}")]
InvalidRequest {
message: String,
param: Option<String>,
},
#[error("session not found: {session_id}")]
NotFound {
session_id: String,
},
#[error("conflict: {message}")]
Conflict {
message: String,
},
#[error("provider error ({provider}): {message}")]
ProviderError {
provider: String,
message: String,
},
#[error("tool timeout: {tool_use_id} after {timeout_secs}s")]
ToolTimeout {
tool_use_id: String,
timeout_secs: u64,
},
#[error("checkpoint failed: {message}")]
CheckpointFailed {
message: String,
},
#[error("sandbox error: {message}")]
SandboxError {
message: String,
},
#[error("internal error: {message}")]
Internal {
message: String,
},
}
impl RuntimeError {
pub fn invalid_request(message: impl Into<String>) -> Self {
Self::InvalidRequest { message: message.into(), param: None }
}
pub fn invalid_param(message: impl Into<String>, param: impl Into<String>) -> Self {
Self::InvalidRequest { message: message.into(), param: Some(param.into()) }
}
pub fn not_found(session_id: impl Into<String>) -> Self {
Self::NotFound { session_id: session_id.into() }
}
pub fn conflict(message: impl Into<String>) -> Self {
Self::Conflict { message: message.into() }
}
pub fn provider_error(provider: impl Into<String>, message: impl Into<String>) -> Self {
Self::ProviderError { provider: provider.into(), message: message.into() }
}
pub fn tool_timeout(tool_use_id: impl Into<String>, timeout_secs: u64) -> Self {
Self::ToolTimeout { tool_use_id: tool_use_id.into(), timeout_secs }
}
pub fn checkpoint_failed(message: impl Into<String>) -> Self {
Self::CheckpointFailed { message: message.into() }
}
pub fn sandbox_error(message: impl Into<String>) -> Self {
Self::SandboxError { message: message.into() }
}
pub fn internal(message: impl Into<String>) -> Self {
Self::Internal { message: message.into() }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_invalid_request_display() {
let err = RuntimeError::invalid_request("missing field 'model'");
assert_eq!(err.to_string(), "invalid request: missing field 'model'");
}
#[test]
fn test_invalid_param_display() {
let err = RuntimeError::invalid_param("must be positive", "timeout_ms");
assert_eq!(err.to_string(), "invalid request: must be positive");
}
#[test]
fn test_not_found_display() {
let err = RuntimeError::not_found("ses_abc123");
assert_eq!(err.to_string(), "session not found: ses_abc123");
}
#[test]
fn test_conflict_display() {
let err = RuntimeError::conflict("cannot transition from Archived to Running");
assert_eq!(err.to_string(), "conflict: cannot transition from Archived to Running");
}
#[test]
fn test_provider_error_display() {
let err = RuntimeError::provider_error("openai", "rate limit exceeded");
assert_eq!(err.to_string(), "provider error (openai): rate limit exceeded");
}
#[test]
fn test_tool_timeout_display() {
let err = RuntimeError::tool_timeout("tool_use_xyz", 300);
assert_eq!(err.to_string(), "tool timeout: tool_use_xyz after 300s");
}
#[test]
fn test_checkpoint_failed_display() {
let err = RuntimeError::checkpoint_failed("database connection lost");
assert_eq!(err.to_string(), "checkpoint failed: database connection lost");
}
#[test]
fn test_sandbox_error_display() {
let err = RuntimeError::sandbox_error("container crashed");
assert_eq!(err.to_string(), "sandbox error: container crashed");
}
#[test]
fn test_internal_error_display() {
let err = RuntimeError::internal("unexpected state");
assert_eq!(err.to_string(), "internal error: unexpected state");
}
#[test]
fn test_error_is_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<RuntimeError>();
}
#[test]
fn test_error_variants_have_structured_fields() {
let err = RuntimeError::ToolTimeout { tool_use_id: "tu_123".to_string(), timeout_secs: 60 };
if let RuntimeError::ToolTimeout { tool_use_id, timeout_secs } = &err {
assert_eq!(tool_use_id, "tu_123");
assert_eq!(*timeout_secs, 60);
} else {
panic!("expected ToolTimeout variant");
}
}
}