1use std::fmt;
2
3pub type Result<T> = std::result::Result<T, AgenticError>;
4
5#[derive(Debug)]
6pub enum AgenticError {
7 Api {
8 message: String,
9 status: Option<u16>,
10 retryable: bool,
11 },
12 Tool {
13 tool_name: String,
14 message: String,
15 },
16 Io(std::io::Error),
17 Json(serde_json::Error),
18 Aborted,
19 MaxTurnsExceeded(u32),
20 BudgetExceeded {
21 spent: f64,
22 limit: f64,
23 },
24 ContextOverflow {
25 token_count: u64,
26 limit: u64,
27 },
28 SchemaValidation {
29 path: String,
30 message: String,
31 },
32 SchemaRetryExhausted {
33 retries: u32,
34 },
35 Other(String),
36}
37
38impl fmt::Display for AgenticError {
39 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40 match self {
41 AgenticError::Api {
42 message,
43 status,
44 retryable,
45 } => match status {
46 Some(code) => write!(
47 f,
48 "API error (status {code}): {message} (retryable: {retryable})"
49 ),
50 None => write!(f, "API error: {message} (retryable: {retryable})"),
51 },
52 AgenticError::Tool { tool_name, message } => {
53 write!(f, "Tool error ({tool_name}): {message}")
54 }
55 AgenticError::Io(err) => write!(f, "IO error: {err}"),
56 AgenticError::Json(err) => write!(f, "JSON error: {err}"),
57 AgenticError::Aborted => write!(f, "Operation aborted"),
58 AgenticError::MaxTurnsExceeded(n) => write!(f, "Maximum turns exceeded: {n}"),
59 AgenticError::BudgetExceeded { spent, limit } => {
60 write!(f, "Budget exceeded: spent ${spent:.4}, limit ${limit:.4}")
61 }
62 AgenticError::ContextOverflow { token_count, limit } => {
63 write!(
64 f,
65 "Context overflow: {token_count} tokens exceeds limit of {limit}"
66 )
67 }
68 AgenticError::SchemaValidation { path, message } => {
69 write!(f, "Schema validation error at {path}: {message}")
70 }
71 AgenticError::SchemaRetryExhausted { retries } => {
72 write!(f, "Schema retry exhausted after {retries} attempts")
73 }
74 AgenticError::Other(msg) => write!(f, "{msg}"),
75 }
76 }
77}
78
79impl std::error::Error for AgenticError {
80 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
81 match self {
82 AgenticError::Io(err) => Some(err),
83 AgenticError::Json(err) => Some(err),
84 _ => None,
85 }
86 }
87}
88
89impl From<std::io::Error> for AgenticError {
90 fn from(err: std::io::Error) -> Self {
91 AgenticError::Io(err)
92 }
93}
94
95impl From<serde_json::Error> for AgenticError {
96 fn from(err: serde_json::Error) -> Self {
97 AgenticError::Json(err)
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104
105 #[test]
106 fn display_api_error() {
107 let err = AgenticError::Api {
108 message: "rate limited".into(),
109 status: Some(429),
110 retryable: true,
111 };
112 let display = format!("{err}");
113 assert!(display.contains("429"));
114 assert!(display.contains("rate limited"));
115 }
116
117 #[test]
118 fn from_io_error() {
119 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
120 let err: AgenticError = io_err.into();
121 assert!(matches!(err, AgenticError::Io(_)));
122 assert!(format!("{err}").contains("file not found"));
123 }
124
125 #[test]
126 fn from_json_error() {
127 let json_err = serde_json::from_str::<serde_json::Value>("invalid").unwrap_err();
128 let err: AgenticError = json_err.into();
129 assert!(matches!(err, AgenticError::Json(_)));
130 }
131
132 #[test]
133 fn budget_exceeded_shows_amounts() {
134 let err = AgenticError::BudgetExceeded {
135 spent: 5.1234,
136 limit: 3.0,
137 };
138 let display = format!("{err}");
139 assert!(display.contains("5.1234"));
140 assert!(display.contains("3.0000"));
141 }
142
143 #[test]
144 fn all_variants_display_non_empty() {
145 let variants: Vec<AgenticError> = vec![
146 AgenticError::Api {
147 message: "msg".into(),
148 status: Some(500),
149 retryable: false,
150 },
151 AgenticError::Tool {
152 tool_name: "tool".into(),
153 message: "err".into(),
154 },
155 AgenticError::Io(std::io::Error::new(std::io::ErrorKind::Other, "io")),
156 AgenticError::Json(serde_json::from_str::<()>("bad").unwrap_err()),
157 AgenticError::Aborted,
158 AgenticError::MaxTurnsExceeded(10),
159 AgenticError::BudgetExceeded {
160 spent: 1.0,
161 limit: 0.5,
162 },
163 AgenticError::ContextOverflow {
164 token_count: 200_000,
165 limit: 100_000,
166 },
167 AgenticError::SchemaValidation {
168 path: "/a".into(),
169 message: "bad".into(),
170 },
171 AgenticError::SchemaRetryExhausted { retries: 3 },
172 AgenticError::Other("other".into()),
173 ];
174 for variant in &variants {
175 let display = format!("{variant}");
176 assert!(!display.is_empty(), "Empty display for: {variant:?}");
177 }
178 }
179}