Skip to main content

soul_core/
error.rs

1use thiserror::Error;
2
3#[derive(Error, Debug)]
4pub enum SoulError {
5    #[error("Provider error: {0}")]
6    Provider(String),
7
8    #[error("Provider rate limited: {provider}, retry after {retry_after_ms}ms")]
9    RateLimited {
10        provider: String,
11        retry_after_ms: u64,
12    },
13
14    #[error("Auth error: {0}")]
15    Auth(String),
16
17    #[error("Tool execution error: tool={tool_name}, {message}")]
18    ToolExecution { tool_name: String, message: String },
19
20    #[error("Context overflow: {used_tokens} tokens used, {max_tokens} max")]
21    ContextOverflow {
22        used_tokens: usize,
23        max_tokens: usize,
24    },
25
26    #[error("Compaction failed: {0}")]
27    CompactionFailed(String),
28
29    #[error("Session error: {0}")]
30    Session(String),
31
32    #[error("Serialization error: {0}")]
33    Serialization(#[from] serde_json::Error),
34
35    #[error("IO error: {0}")]
36    Io(#[from] std::io::Error),
37
38    #[error("HTTP error: {0}")]
39    Http(#[from] reqwest::Error),
40
41    #[error("Agent interrupted")]
42    Interrupted,
43
44    #[error("Failover exhausted: tried {attempts} providers")]
45    FailoverExhausted { attempts: usize },
46
47    #[error("Permission denied: tool={tool_name}, {reason}")]
48    PermissionDenied { tool_name: String, reason: String },
49
50    #[error("Budget exceeded: {message}")]
51    BudgetExceeded { message: String },
52
53    #[error("MCP error: server={server}, {message}")]
54    Mcp { server: String, message: String },
55
56    #[error("JSON-RPC error: code={code}, {message}")]
57    JsonRpc { code: i32, message: String },
58
59    #[error("Skill parse error: {message}")]
60    SkillParse { message: String },
61
62    #[error("Executor not found: {name}")]
63    ExecutorNotFound { name: String },
64
65    #[error("{0}")]
66    Other(#[from] anyhow::Error),
67}
68
69pub type SoulResult<T> = Result<T, SoulError>;
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    #[test]
76    fn error_display_formats() {
77        let err = SoulError::Provider("connection refused".into());
78        assert_eq!(err.to_string(), "Provider error: connection refused");
79
80        let err = SoulError::RateLimited {
81            provider: "anthropic".into(),
82            retry_after_ms: 5000,
83        };
84        assert!(err.to_string().contains("5000ms"));
85
86        let err = SoulError::ContextOverflow {
87            used_tokens: 200_000,
88            max_tokens: 180_000,
89        };
90        assert!(err.to_string().contains("200000"));
91
92        let err = SoulError::ToolExecution {
93            tool_name: "bash".into(),
94            message: "command not found".into(),
95        };
96        assert!(err.to_string().contains("bash"));
97    }
98
99    #[test]
100    fn error_is_send_sync() {
101        fn assert_send_sync<T: Send + Sync>() {}
102        assert_send_sync::<SoulError>();
103    }
104
105    #[test]
106    fn io_error_converts() {
107        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
108        let soul_err: SoulError = io_err.into();
109        assert!(matches!(soul_err, SoulError::Io(_)));
110    }
111
112    #[test]
113    fn json_error_converts() {
114        let json_err = serde_json::from_str::<String>("invalid").unwrap_err();
115        let soul_err: SoulError = json_err.into();
116        assert!(matches!(soul_err, SoulError::Serialization(_)));
117    }
118}