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("Run '{run_id}' is already bound to different immutable input")]
102 RunIdentityConflict { run_id: String },
103
104 #[error("Budget exhausted on '{resource}': {reason}")]
108 BudgetExhausted { resource: String, reason: String },
109
110 #[error("Security error: {0}")]
112 Security(String),
113
114 #[error("Context error: {0}")]
116 Context(String),
117
118 #[error("MCP error: {0}")]
120 Mcp(String),
121
122 #[error("Queue error: {0}")]
124 Queue(String),
125
126 #[error("IO error: {0}")]
128 Io(#[from] std::io::Error),
129
130 #[error("Serialization error: {0}")]
132 Serialization(#[from] serde_json::Error),
133
134 #[error("{0:#}")]
140 Internal(#[from] anyhow::Error),
141}
142
143impl CodeError {
144 pub const fn code(&self) -> &'static str {
146 match self {
147 Self::Config(_) => "CONFIG_ERROR",
148 Self::Llm(_) => "LLM_ERROR",
149 Self::Tool { .. } => "TOOL_ERROR",
150 Self::Session(_) => "SESSION_ERROR",
151 Self::SessionConfiguration { .. } => "SESSION_CONFIGURATION_ERROR",
152 Self::SessionInitialization { .. } => "SESSION_INITIALIZATION_ERROR",
153 Self::AsyncSessionBuildRequired { .. } => "ASYNC_SESSION_BUILD_REQUIRED",
154 Self::SessionClosed { .. } => "SESSION_CLOSED",
155 Self::SessionBusy { .. } => "SESSION_BUSY",
156 Self::RunIdentityConflict { .. } => "RUN_IDENTITY_CONFLICT",
157 Self::BudgetExhausted { .. } => "BUDGET_EXHAUSTED",
158 Self::Security(_) => "SECURITY_ERROR",
159 Self::Context(_) => "CONTEXT_ERROR",
160 Self::Mcp(_) => "MCP_ERROR",
161 Self::Queue(_) => "QUEUE_ERROR",
162 Self::Io(_) => "IO_ERROR",
163 Self::Serialization(_) => "SERIALIZATION_ERROR",
164 Self::Internal(_) => "INTERNAL_ERROR",
165 }
166 }
167}
168
169pub(crate) fn read_or_recover<T>(lock: &std::sync::RwLock<T>) -> std::sync::RwLockReadGuard<'_, T> {
179 lock.read().unwrap_or_else(|p| p.into_inner())
180}
181
182pub(crate) fn write_or_recover<T>(
186 lock: &std::sync::RwLock<T>,
187) -> std::sync::RwLockWriteGuard<'_, T> {
188 lock.write().unwrap_or_else(|p| p.into_inner())
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 #[test]
196 fn test_code_error_config() {
197 let err = CodeError::Config("missing API key".to_string());
198 assert!(err.to_string().contains("Config error"));
199 assert!(err.to_string().contains("missing API key"));
200 }
201
202 #[test]
203 fn test_code_error_llm() {
204 let err = CodeError::Llm("rate limited".to_string());
205 assert!(err.to_string().contains("LLM error"));
206 }
207
208 #[test]
209 fn test_code_error_tool() {
210 let err = CodeError::Tool {
211 tool: "bash".to_string(),
212 message: "command not found".to_string(),
213 };
214 let msg = err.to_string();
215 assert!(msg.contains("bash"));
216 assert!(msg.contains("command not found"));
217 }
218
219 #[test]
220 fn test_code_error_session() {
221 let err = CodeError::Session("not found".to_string());
222 assert!(err.to_string().contains("Session error"));
223 }
224
225 #[test]
226 fn test_code_error_session_configuration_keeps_field_identity() {
227 let err = CodeError::SessionConfiguration {
228 field: "session_id",
229 message: "must not be empty".to_string(),
230 };
231 assert!(err.to_string().contains("session_id"));
232 assert!(err.to_string().contains("must not be empty"));
233 }
234
235 #[test]
236 fn test_code_error_session_busy() {
237 let err = CodeError::SessionBusy {
238 session_id: "session-1".to_string(),
239 };
240 assert!(err.to_string().contains("session-1"));
241 assert!(err.to_string().contains("active operation"));
242 }
243
244 #[test]
245 fn test_code_error_security() {
246 let err = CodeError::Security("taint detected".to_string());
247 assert!(err.to_string().contains("Security error"));
248 }
249
250 #[test]
251 fn test_code_error_context() {
252 let err = CodeError::Context("provider failed".to_string());
253 assert!(err.to_string().contains("Context error"));
254 }
255
256 #[test]
257 fn test_code_error_mcp() {
258 let err = CodeError::Mcp("connection refused".to_string());
259 assert!(err.to_string().contains("MCP error"));
260 }
261
262 #[test]
263 fn test_code_error_queue() {
264 let err = CodeError::Queue("lane full".to_string());
265 assert!(err.to_string().contains("Queue error"));
266 }
267
268 #[test]
269 fn test_code_error_from_io() {
270 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
271 let err: CodeError = io_err.into();
272 assert!(matches!(err, CodeError::Io(_)));
273 assert!(err.to_string().contains("file missing"));
274 }
275
276 #[test]
277 fn test_code_error_from_serde_json() {
278 let json_err = serde_json::from_str::<serde_json::Value>("invalid").unwrap_err();
279 let err: CodeError = json_err.into();
280 assert!(matches!(err, CodeError::Serialization(_)));
281 }
282
283 #[test]
284 fn test_code_error_from_anyhow() {
285 let anyhow_err = anyhow::anyhow!("something went wrong");
286 let err: CodeError = anyhow_err.into();
287 assert!(matches!(err, CodeError::Internal(_)));
288 assert!(err.to_string().contains("something went wrong"));
289 }
290
291 #[test]
292 fn stable_error_codes_cover_control_flow_variants() {
293 assert_eq!(
294 CodeError::SessionBusy {
295 session_id: "session-1".to_string(),
296 }
297 .code(),
298 "SESSION_BUSY"
299 );
300 assert_eq!(
301 CodeError::SessionClosed {
302 session_id: "session-1".to_string(),
303 }
304 .code(),
305 "SESSION_CLOSED"
306 );
307 assert_eq!(
308 CodeError::RunIdentityConflict {
309 run_id: "run-1".to_string(),
310 }
311 .code(),
312 "RUN_IDENTITY_CONFLICT"
313 );
314 assert_eq!(
315 CodeError::BudgetExhausted {
316 resource: "tokens".to_string(),
317 reason: "limit".to_string(),
318 }
319 .code(),
320 "BUDGET_EXHAUSTED"
321 );
322 }
323
324 #[test]
325 fn test_code_error_question_mark_from_anyhow() {
326 fn inner() -> anyhow::Result<()> {
327 anyhow::bail!("inner error")
328 }
329
330 fn outer() -> Result<()> {
331 inner()?; Ok(())
333 }
334
335 let result = outer();
336 assert!(result.is_err());
337 let err = result.unwrap_err();
338 assert!(matches!(err, CodeError::Internal(_)));
339 }
340
341 #[test]
342 fn test_read_or_recover_normal() {
343 let lock = std::sync::RwLock::new(42);
344 let guard = read_or_recover(&lock);
345 assert_eq!(*guard, 42);
346 }
347
348 #[test]
349 fn test_write_or_recover_normal() {
350 let lock = std::sync::RwLock::new(42);
351 let mut guard = write_or_recover(&lock);
352 *guard = 99;
353 drop(guard);
354 assert_eq!(*read_or_recover(&lock), 99);
355 }
356
357 #[test]
358 fn test_read_or_recover_poisoned() {
359 let lock = std::sync::RwLock::new(42);
360 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
362 let _guard = lock.write().unwrap();
363 panic!("intentional poison");
364 }));
365 let guard = read_or_recover(&lock);
367 assert_eq!(*guard, 42);
368 }
369
370 #[test]
371 fn test_write_or_recover_poisoned() {
372 let lock = std::sync::RwLock::new(42);
373 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
374 let _guard = lock.write().unwrap();
375 panic!("intentional poison");
376 }));
377 let mut guard = write_or_recover(&lock);
378 *guard = 100;
379 assert_eq!(*guard, 100);
380 }
381}