1#[derive(Debug, thiserror::Error)]
8#[non_exhaustive]
9pub enum Error {
10 #[error("LLM error: {message}")]
12 Llm {
13 message: String,
15 #[source]
17 source: Option<Box<dyn std::error::Error + Send + Sync>>,
18 },
19
20 #[error("Validation error: {message}")]
22 Validation {
23 field: Option<String>,
25 message: String,
27 },
28
29 #[error("I/O error: {0}")]
31 Io(#[from] std::io::Error),
32
33 #[error("Serialization error: {0}")]
35 Serialization(#[from] serde_json::Error),
36
37 #[error("Step timed out after {seconds}s")]
39 Timeout {
40 seconds: u64,
42 },
43
44 #[error("Maximum revisions exceeded: {attempts} attempts")]
46 MaxRevisionsExceeded {
47 attempts: u32,
49 },
50
51 #[error("Configuration error: {message}")]
53 Config {
54 message: String,
56 },
57
58 #[error("Workflow not found: {id}")]
60 WorkflowNotFound {
61 id: String,
63 },
64
65 #[error("Step not found: {id}")]
67 StepNotFound {
68 id: String,
70 },
71}
72
73pub type Result<T> = std::result::Result<T, Error>;
77
78impl Error {
79 pub fn is_retryable(&self) -> bool {
84 match self {
85 Error::Llm { .. } => true, Error::Io(_) => true,
87 Error::Timeout { .. } => true,
88 Error::Validation { .. } => false, Error::MaxRevisionsExceeded { .. } => false,
90 Error::Serialization(_) => false,
91 Error::Config { .. } => false,
92 Error::WorkflowNotFound { .. } => false,
93 Error::StepNotFound { .. } => false,
94 }
95 }
96
97 pub fn llm<S: Into<String>>(message: S) -> Self {
99 Error::Llm {
100 message: message.into(),
101 source: None,
102 }
103 }
104
105 pub fn llm_with_source<S, E>(message: S, source: E) -> Self
107 where
108 S: Into<String>,
109 E: std::error::Error + Send + Sync + 'static,
110 {
111 Error::Llm {
112 message: message.into(),
113 source: Some(Box::new(source)),
114 }
115 }
116
117 pub fn validation<S: Into<String>>(message: S) -> Self {
119 Error::Validation {
120 field: None,
121 message: message.into(),
122 }
123 }
124
125 pub fn validation_field<F, M>(field: F, message: M) -> Self
127 where
128 F: Into<String>,
129 M: Into<String>,
130 {
131 Error::Validation {
132 field: Some(field.into()),
133 message: message.into(),
134 }
135 }
136
137 pub fn config<S: Into<String>>(message: S) -> Self {
139 Error::Config {
140 message: message.into(),
141 }
142 }
143}
144
145#[cfg(test)]
146#[allow(clippy::unwrap_used)]
147mod tests {
148 use super::*;
149
150 #[test]
151 fn test_error_display() {
152 let err = Error::llm("Rate limit exceeded");
153 assert_eq!(err.to_string(), "LLM error: Rate limit exceeded");
154 }
155
156 #[test]
157 fn test_retryable_classification() {
158 assert!(Error::llm("test").is_retryable());
159 assert!(Error::Timeout { seconds: 30 }.is_retryable());
160 assert!(!Error::validation("test").is_retryable());
161 assert!(!Error::MaxRevisionsExceeded { attempts: 3 }.is_retryable());
162 }
163
164 #[test]
165 fn test_validation_error_with_field() {
166 let err = Error::validation_field("topic", "must not be empty");
167 let Error::Validation { field, message } = err else {
168 unreachable!("Expected Validation error variant");
169 };
170 assert_eq!(field, Some("topic".to_string()));
171 assert_eq!(message, "must not be empty");
172 }
173
174 #[test]
175 fn test_max_revisions_exceeded() {
176 let err = Error::MaxRevisionsExceeded { attempts: 5 };
177 assert_eq!(err.to_string(), "Maximum revisions exceeded: 5 attempts");
178 }
179
180 #[test]
181 fn test_error_implements_send_sync() {
182 fn assert_send_sync<T: Send + Sync>() {}
183 assert_send_sync::<Error>();
184 }
185
186 #[test]
187 fn test_config_error() {
188 let err = Error::config("Invalid API endpoint");
189 assert_eq!(err.to_string(), "Configuration error: Invalid API endpoint");
190 assert!(!err.is_retryable());
191 }
192
193 #[test]
194 fn test_workflow_not_found() {
195 let err = Error::WorkflowNotFound {
196 id: "wf-123".to_string(),
197 };
198 assert_eq!(err.to_string(), "Workflow not found: wf-123");
199 assert!(!err.is_retryable());
200 }
201
202 #[test]
203 fn test_step_not_found() {
204 let err = Error::StepNotFound {
205 id: "step-456".to_string(),
206 };
207 assert_eq!(err.to_string(), "Step not found: step-456");
208 assert!(!err.is_retryable());
209 }
210
211 #[test]
212 fn test_llm_error_with_source() {
213 let io_error = std::io::Error::other("network failure");
214 let err = Error::llm_with_source("API call failed", Box::new(io_error));
215 assert!(err.to_string().contains("API call failed"));
216 assert!(err.is_retryable());
217 }
218
219 #[test]
220 fn test_io_error_is_retryable() {
221 let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
222 let err: Error = io_error.into();
223 assert!(err.is_retryable());
224 }
225
226 #[test]
227 fn test_serde_error_not_retryable() {
228 let json = "{invalid json}";
229 let serde_err = serde_json::from_str::<serde_json::Value>(json).unwrap_err();
230 let err: Error = serde_err.into();
231 assert!(!err.is_retryable());
232 }
233
234 #[test]
235 fn test_timeout_error_display() {
236 let err = Error::Timeout { seconds: 60 };
237 assert_eq!(err.to_string(), "Step timed out after 60s");
238 }
239
240 #[test]
241 fn test_validation_without_field() {
242 let err = Error::validation("Generic validation failure");
243 let Error::Validation { field, message } = err else {
244 unreachable!("Expected Validation error");
245 };
246 assert_eq!(field, None);
247 assert_eq!(message, "Generic validation failure");
248 }
249}