1use thiserror::Error;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum SessionBuildResource {
18 MemoryStore,
19 SessionStore,
20 Queue,
21 Mcp,
22 RlTrajectory,
23}
24
25impl std::fmt::Display for SessionBuildResource {
26 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27 f.write_str(match self {
28 Self::MemoryStore => "memory store",
29 Self::SessionStore => "session store",
30 Self::Queue => "session queue",
31 Self::Mcp => "MCP",
32 Self::RlTrajectory => "RL trajectory recorder",
33 })
34 }
35}
36
37pub type Result<T> = std::result::Result<T, CodeError>;
39
40#[derive(Debug, Error)]
45pub enum CodeError {
46 #[error("Config error: {0}")]
48 Config(String),
49
50 #[error("LLM error: {0}")]
52 Llm(String),
53
54 #[error("Tool error: {tool}: {message}")]
56 Tool { tool: String, message: String },
57
58 #[error("Session error: {0}")]
60 Session(String),
61
62 #[error("Invalid session configuration for '{field}': {message}")]
64 SessionConfiguration {
65 field: &'static str,
66 message: String,
67 },
68
69 #[error("Failed to initialize {resource}: {message}")]
71 SessionInitialization {
72 resource: SessionBuildResource,
73 message: String,
74 },
75
76 #[error(
79 "{resource} requires asynchronous session construction; use Agent::session_builder(...).build().await"
80 )]
81 AsyncSessionBuildRequired { resource: SessionBuildResource },
82
83 #[error("Session '{session_id}' is closed")]
89 SessionClosed { session_id: String },
90
91 #[error("Session '{session_id}' already has an active operation")]
97 SessionBusy { session_id: String },
98
99 #[error("Budget exhausted on '{resource}': {reason}")]
103 BudgetExhausted { resource: String, reason: String },
104
105 #[error("Security error: {0}")]
107 Security(String),
108
109 #[error("Context error: {0}")]
111 Context(String),
112
113 #[error("MCP error: {0}")]
115 Mcp(String),
116
117 #[error("Queue error: {0}")]
119 Queue(String),
120
121 #[error("IO error: {0}")]
123 Io(#[from] std::io::Error),
124
125 #[error("Serialization error: {0}")]
127 Serialization(#[from] serde_json::Error),
128
129 #[error("{0:#}")]
135 Internal(#[from] anyhow::Error),
136}
137
138impl CodeError {
139 pub const fn code(&self) -> &'static str {
141 match self {
142 Self::Config(_) => "CONFIG_ERROR",
143 Self::Llm(_) => "LLM_ERROR",
144 Self::Tool { .. } => "TOOL_ERROR",
145 Self::Session(_) => "SESSION_ERROR",
146 Self::SessionConfiguration { .. } => "SESSION_CONFIGURATION_ERROR",
147 Self::SessionInitialization { .. } => "SESSION_INITIALIZATION_ERROR",
148 Self::AsyncSessionBuildRequired { .. } => "ASYNC_SESSION_BUILD_REQUIRED",
149 Self::SessionClosed { .. } => "SESSION_CLOSED",
150 Self::SessionBusy { .. } => "SESSION_BUSY",
151 Self::BudgetExhausted { .. } => "BUDGET_EXHAUSTED",
152 Self::Security(_) => "SECURITY_ERROR",
153 Self::Context(_) => "CONTEXT_ERROR",
154 Self::Mcp(_) => "MCP_ERROR",
155 Self::Queue(_) => "QUEUE_ERROR",
156 Self::Io(_) => "IO_ERROR",
157 Self::Serialization(_) => "SERIALIZATION_ERROR",
158 Self::Internal(_) => "INTERNAL_ERROR",
159 }
160 }
161}
162
163pub(crate) fn read_or_recover<T>(lock: &std::sync::RwLock<T>) -> std::sync::RwLockReadGuard<'_, T> {
173 lock.read().unwrap_or_else(|p| p.into_inner())
174}
175
176pub(crate) fn write_or_recover<T>(
180 lock: &std::sync::RwLock<T>,
181) -> std::sync::RwLockWriteGuard<'_, T> {
182 lock.write().unwrap_or_else(|p| p.into_inner())
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 #[test]
190 fn test_code_error_config() {
191 let err = CodeError::Config("missing API key".to_string());
192 assert!(err.to_string().contains("Config error"));
193 assert!(err.to_string().contains("missing API key"));
194 }
195
196 #[test]
197 fn test_code_error_llm() {
198 let err = CodeError::Llm("rate limited".to_string());
199 assert!(err.to_string().contains("LLM error"));
200 }
201
202 #[test]
203 fn test_code_error_tool() {
204 let err = CodeError::Tool {
205 tool: "bash".to_string(),
206 message: "command not found".to_string(),
207 };
208 let msg = err.to_string();
209 assert!(msg.contains("bash"));
210 assert!(msg.contains("command not found"));
211 }
212
213 #[test]
214 fn test_code_error_session() {
215 let err = CodeError::Session("not found".to_string());
216 assert!(err.to_string().contains("Session error"));
217 }
218
219 #[test]
220 fn test_code_error_session_configuration_keeps_field_identity() {
221 let err = CodeError::SessionConfiguration {
222 field: "session_id",
223 message: "must not be empty".to_string(),
224 };
225 assert!(err.to_string().contains("session_id"));
226 assert!(err.to_string().contains("must not be empty"));
227 }
228
229 #[test]
230 fn test_code_error_session_busy() {
231 let err = CodeError::SessionBusy {
232 session_id: "session-1".to_string(),
233 };
234 assert!(err.to_string().contains("session-1"));
235 assert!(err.to_string().contains("active operation"));
236 }
237
238 #[test]
239 fn test_code_error_security() {
240 let err = CodeError::Security("taint detected".to_string());
241 assert!(err.to_string().contains("Security error"));
242 }
243
244 #[test]
245 fn test_code_error_context() {
246 let err = CodeError::Context("provider failed".to_string());
247 assert!(err.to_string().contains("Context error"));
248 }
249
250 #[test]
251 fn test_code_error_mcp() {
252 let err = CodeError::Mcp("connection refused".to_string());
253 assert!(err.to_string().contains("MCP error"));
254 }
255
256 #[test]
257 fn test_code_error_queue() {
258 let err = CodeError::Queue("lane full".to_string());
259 assert!(err.to_string().contains("Queue error"));
260 }
261
262 #[test]
263 fn test_code_error_from_io() {
264 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
265 let err: CodeError = io_err.into();
266 assert!(matches!(err, CodeError::Io(_)));
267 assert!(err.to_string().contains("file missing"));
268 }
269
270 #[test]
271 fn test_code_error_from_serde_json() {
272 let json_err = serde_json::from_str::<serde_json::Value>("invalid").unwrap_err();
273 let err: CodeError = json_err.into();
274 assert!(matches!(err, CodeError::Serialization(_)));
275 }
276
277 #[test]
278 fn test_code_error_from_anyhow() {
279 let anyhow_err = anyhow::anyhow!("something went wrong");
280 let err: CodeError = anyhow_err.into();
281 assert!(matches!(err, CodeError::Internal(_)));
282 assert!(err.to_string().contains("something went wrong"));
283 }
284
285 #[test]
286 fn stable_error_codes_cover_control_flow_variants() {
287 assert_eq!(
288 CodeError::SessionBusy {
289 session_id: "session-1".to_string(),
290 }
291 .code(),
292 "SESSION_BUSY"
293 );
294 assert_eq!(
295 CodeError::SessionClosed {
296 session_id: "session-1".to_string(),
297 }
298 .code(),
299 "SESSION_CLOSED"
300 );
301 assert_eq!(
302 CodeError::BudgetExhausted {
303 resource: "tokens".to_string(),
304 reason: "limit".to_string(),
305 }
306 .code(),
307 "BUDGET_EXHAUSTED"
308 );
309 }
310
311 #[test]
312 fn test_code_error_question_mark_from_anyhow() {
313 fn inner() -> anyhow::Result<()> {
314 anyhow::bail!("inner error")
315 }
316
317 fn outer() -> Result<()> {
318 inner()?; Ok(())
320 }
321
322 let result = outer();
323 assert!(result.is_err());
324 let err = result.unwrap_err();
325 assert!(matches!(err, CodeError::Internal(_)));
326 }
327
328 #[test]
329 fn test_read_or_recover_normal() {
330 let lock = std::sync::RwLock::new(42);
331 let guard = read_or_recover(&lock);
332 assert_eq!(*guard, 42);
333 }
334
335 #[test]
336 fn test_write_or_recover_normal() {
337 let lock = std::sync::RwLock::new(42);
338 let mut guard = write_or_recover(&lock);
339 *guard = 99;
340 drop(guard);
341 assert_eq!(*read_or_recover(&lock), 99);
342 }
343
344 #[test]
345 fn test_read_or_recover_poisoned() {
346 let lock = std::sync::RwLock::new(42);
347 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
349 let _guard = lock.write().unwrap();
350 panic!("intentional poison");
351 }));
352 let guard = read_or_recover(&lock);
354 assert_eq!(*guard, 42);
355 }
356
357 #[test]
358 fn test_write_or_recover_poisoned() {
359 let lock = std::sync::RwLock::new(42);
360 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
361 let _guard = lock.write().unwrap();
362 panic!("intentional poison");
363 }));
364 let mut guard = write_or_recover(&lock);
365 *guard = 100;
366 assert_eq!(*guard, 100);
367 }
368}