1use thiserror::Error;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum SessionBuildResource {
18 Capability,
19 MemoryStore,
20 MemoryMaintenance,
21 SessionStore,
22 Queue,
23 Mcp,
24 RlTrajectory,
25 WorkspaceRetrieval,
26}
27
28impl std::fmt::Display for SessionBuildResource {
29 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30 f.write_str(match self {
31 Self::Capability => "capability runtime",
32 Self::MemoryStore => "memory store",
33 Self::MemoryMaintenance => "memory maintenance",
34 Self::SessionStore => "session store",
35 Self::Queue => "session queue",
36 Self::Mcp => "MCP",
37 Self::RlTrajectory => "RL trajectory recorder",
38 Self::WorkspaceRetrieval => "workspace retrieval",
39 })
40 }
41}
42
43pub type Result<T> = std::result::Result<T, CodeError>;
45
46#[derive(Debug, Error)]
51pub enum CodeError {
52 #[error("Config error: {0}")]
54 Config(String),
55
56 #[error("LLM error: {0}")]
58 Llm(String),
59
60 #[error("Tool error: {tool}: {message}")]
62 Tool { tool: String, message: String },
63
64 #[error("Session error: {0}")]
66 Session(String),
67
68 #[error("Invalid session configuration for '{field}': {message}")]
70 SessionConfiguration {
71 field: &'static str,
72 message: String,
73 },
74
75 #[error("Failed to initialize {resource}: {message}")]
77 SessionInitialization {
78 resource: SessionBuildResource,
79 message: String,
80 },
81
82 #[error(
85 "{resource} requires asynchronous session construction; use Agent::session_builder(...).build().await"
86 )]
87 AsyncSessionBuildRequired { resource: SessionBuildResource },
88
89 #[error("Session '{session_id}' is closed")]
95 SessionClosed { session_id: String },
96
97 #[error("Session '{session_id}' already has an active operation")]
103 SessionBusy { session_id: String },
104
105 #[error("Task admission for session '{session_id}' was cancelled")]
107 TaskAdmissionCancelled { session_id: String },
108
109 #[error("Task scheduler is closed")]
111 TaskSchedulerClosed,
112
113 #[error("Task admission queue for session '{session_id}' is full (limit {limit})")]
116 TaskAdmissionLimit { session_id: String, limit: usize },
117
118 #[error("Run '{run_id}' is already bound to different immutable input")]
121 RunIdentityConflict { run_id: String },
122
123 #[error("Run control error: {0}")]
126 RunControl(#[from] crate::run_control::RunControlError),
127
128 #[error("Budget exhausted on '{resource}': {reason}")]
132 BudgetExhausted { resource: String, reason: String },
133
134 #[error("Security error: {0}")]
136 Security(String),
137
138 #[error("Context error: {0}")]
140 Context(String),
141
142 #[error("MCP error: {0}")]
144 Mcp(String),
145
146 #[error("Queue error: {0}")]
148 Queue(String),
149
150 #[error("Capability runtime error: {0}")]
152 Capability(#[from] crate::capability::CapabilityRuntimeError),
153
154 #[error("Flow error: {0}")]
156 Flow(#[from] a3s_flow::FlowError),
157
158 #[error("IO error: {0}")]
160 Io(#[from] std::io::Error),
161
162 #[error("Serialization error: {0}")]
164 Serialization(#[from] serde_json::Error),
165
166 #[error("{0:#}")]
172 Internal(#[from] anyhow::Error),
173}
174
175impl CodeError {
176 pub const fn code(&self) -> &'static str {
178 match self {
179 Self::Config(_) => "CONFIG_ERROR",
180 Self::Llm(_) => "LLM_ERROR",
181 Self::Tool { .. } => "TOOL_ERROR",
182 Self::Session(_) => "SESSION_ERROR",
183 Self::SessionConfiguration { .. } => "SESSION_CONFIGURATION_ERROR",
184 Self::SessionInitialization { .. } => "SESSION_INITIALIZATION_ERROR",
185 Self::AsyncSessionBuildRequired { .. } => "ASYNC_SESSION_BUILD_REQUIRED",
186 Self::SessionClosed { .. } => "SESSION_CLOSED",
187 Self::SessionBusy { .. } => "SESSION_BUSY",
188 Self::TaskAdmissionCancelled { .. } => "TASK_ADMISSION_CANCELLED",
189 Self::TaskSchedulerClosed => "TASK_SCHEDULER_CLOSED",
190 Self::TaskAdmissionLimit { .. } => "TASK_ADMISSION_LIMIT",
191 Self::RunIdentityConflict { .. } => "RUN_IDENTITY_CONFLICT",
192 Self::RunControl(error) => error.code(),
193 Self::BudgetExhausted { .. } => "BUDGET_EXHAUSTED",
194 Self::Security(_) => "SECURITY_ERROR",
195 Self::Context(_) => "CONTEXT_ERROR",
196 Self::Mcp(_) => "MCP_ERROR",
197 Self::Queue(_) => "QUEUE_ERROR",
198 Self::Capability(_) => "CAPABILITY_RUNTIME_ERROR",
199 Self::Flow(_) => "FLOW_ERROR",
200 Self::Io(_) => "IO_ERROR",
201 Self::Serialization(_) => "SERIALIZATION_ERROR",
202 Self::Internal(_) => "INTERNAL_ERROR",
203 }
204 }
205}
206
207pub(crate) fn read_or_recover<T>(lock: &std::sync::RwLock<T>) -> std::sync::RwLockReadGuard<'_, T> {
217 lock.read().unwrap_or_else(|p| p.into_inner())
218}
219
220pub(crate) fn write_or_recover<T>(
224 lock: &std::sync::RwLock<T>,
225) -> std::sync::RwLockWriteGuard<'_, T> {
226 lock.write().unwrap_or_else(|p| p.into_inner())
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232
233 #[test]
234 fn test_code_error_config() {
235 let err = CodeError::Config("missing API key".to_string());
236 assert!(err.to_string().contains("Config error"));
237 assert!(err.to_string().contains("missing API key"));
238 }
239
240 #[test]
241 fn test_code_error_llm() {
242 let err = CodeError::Llm("rate limited".to_string());
243 assert!(err.to_string().contains("LLM error"));
244 }
245
246 #[test]
247 fn test_code_error_tool() {
248 let err = CodeError::Tool {
249 tool: "bash".to_string(),
250 message: "command not found".to_string(),
251 };
252 let msg = err.to_string();
253 assert!(msg.contains("bash"));
254 assert!(msg.contains("command not found"));
255 }
256
257 #[test]
258 fn test_code_error_session() {
259 let err = CodeError::Session("not found".to_string());
260 assert!(err.to_string().contains("Session error"));
261 }
262
263 #[test]
264 fn test_code_error_session_configuration_keeps_field_identity() {
265 let err = CodeError::SessionConfiguration {
266 field: "session_id",
267 message: "must not be empty".to_string(),
268 };
269 assert!(err.to_string().contains("session_id"));
270 assert!(err.to_string().contains("must not be empty"));
271 }
272
273 #[test]
274 fn test_code_error_session_busy() {
275 let err = CodeError::SessionBusy {
276 session_id: "session-1".to_string(),
277 };
278 assert!(err.to_string().contains("session-1"));
279 assert!(err.to_string().contains("active operation"));
280 }
281
282 #[test]
283 fn test_code_error_security() {
284 let err = CodeError::Security("taint detected".to_string());
285 assert!(err.to_string().contains("Security error"));
286 }
287
288 #[test]
289 fn test_code_error_context() {
290 let err = CodeError::Context("provider failed".to_string());
291 assert!(err.to_string().contains("Context error"));
292 }
293
294 #[test]
295 fn test_code_error_mcp() {
296 let err = CodeError::Mcp("connection refused".to_string());
297 assert!(err.to_string().contains("MCP error"));
298 }
299
300 #[test]
301 fn test_code_error_queue() {
302 let err = CodeError::Queue("lane full".to_string());
303 assert!(err.to_string().contains("Queue error"));
304 }
305
306 #[test]
307 fn test_code_error_from_io() {
308 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
309 let err: CodeError = io_err.into();
310 assert!(matches!(err, CodeError::Io(_)));
311 assert!(err.to_string().contains("file missing"));
312 }
313
314 #[test]
315 fn test_code_error_from_serde_json() {
316 let json_err = serde_json::from_str::<serde_json::Value>("invalid").unwrap_err();
317 let err: CodeError = json_err.into();
318 assert!(matches!(err, CodeError::Serialization(_)));
319 }
320
321 #[test]
322 fn test_code_error_from_anyhow() {
323 let anyhow_err = anyhow::anyhow!("something went wrong");
324 let err: CodeError = anyhow_err.into();
325 assert!(matches!(err, CodeError::Internal(_)));
326 assert!(err.to_string().contains("something went wrong"));
327 }
328
329 #[test]
330 fn stable_error_codes_cover_control_flow_variants() {
331 assert_eq!(
332 CodeError::SessionBusy {
333 session_id: "session-1".to_string(),
334 }
335 .code(),
336 "SESSION_BUSY"
337 );
338 assert_eq!(
339 CodeError::SessionClosed {
340 session_id: "session-1".to_string(),
341 }
342 .code(),
343 "SESSION_CLOSED"
344 );
345 assert_eq!(
346 CodeError::RunIdentityConflict {
347 run_id: "run-1".to_string(),
348 }
349 .code(),
350 "RUN_IDENTITY_CONFLICT"
351 );
352 assert_eq!(
353 CodeError::BudgetExhausted {
354 resource: "tokens".to_string(),
355 reason: "limit".to_string(),
356 }
357 .code(),
358 "BUDGET_EXHAUSTED"
359 );
360 assert_eq!(
361 CodeError::TaskAdmissionCancelled {
362 session_id: "session-1".to_string(),
363 }
364 .code(),
365 "TASK_ADMISSION_CANCELLED"
366 );
367 assert_eq!(
368 CodeError::TaskSchedulerClosed.code(),
369 "TASK_SCHEDULER_CLOSED"
370 );
371 }
372
373 #[test]
374 fn test_code_error_question_mark_from_anyhow() {
375 fn inner() -> anyhow::Result<()> {
376 anyhow::bail!("inner error")
377 }
378
379 fn outer() -> Result<()> {
380 inner()?; Ok(())
382 }
383
384 let result = outer();
385 assert!(result.is_err());
386 let err = result.unwrap_err();
387 assert!(matches!(err, CodeError::Internal(_)));
388 }
389
390 #[test]
391 fn test_read_or_recover_normal() {
392 let lock = std::sync::RwLock::new(42);
393 let guard = read_or_recover(&lock);
394 assert_eq!(*guard, 42);
395 }
396
397 #[test]
398 fn test_write_or_recover_normal() {
399 let lock = std::sync::RwLock::new(42);
400 let mut guard = write_or_recover(&lock);
401 *guard = 99;
402 drop(guard);
403 assert_eq!(*read_or_recover(&lock), 99);
404 }
405
406 #[test]
407 fn test_read_or_recover_poisoned() {
408 let lock = std::sync::RwLock::new(42);
409 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
411 let _guard = lock.write().unwrap();
412 panic!("intentional poison");
413 }));
414 let guard = read_or_recover(&lock);
416 assert_eq!(*guard, 42);
417 }
418
419 #[test]
420 fn test_write_or_recover_poisoned() {
421 let lock = std::sync::RwLock::new(42);
422 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
423 let _guard = lock.write().unwrap();
424 panic!("intentional poison");
425 }));
426 let mut guard = write_or_recover(&lock);
427 *guard = 100;
428 assert_eq!(*guard, 100);
429 }
430}