1#[derive(Debug)]
11pub struct DowncastError {
12 pub expected: &'static str,
14 pub actual: String,
16}
17
18impl std::fmt::Display for DowncastError {
19 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20 write!(
21 f,
22 "Downcast failed: expected {}, got {}",
23 self.expected, self.actual
24 )
25 }
26}
27
28impl std::error::Error for DowncastError {}
29
30#[non_exhaustive]
34#[derive(Debug, thiserror::Error)]
35pub enum AgentError {
36 #[error("context window overflow for model: {model}")]
38 ContextWindowOverflow { model: String },
39
40 #[error("model request throttled (rate limited)")]
42 ModelThrottled,
43
44 #[error("network error")]
46 NetworkError {
47 #[source]
48 source: Box<dyn std::error::Error + Send + Sync>,
49 },
50
51 #[error("structured output failed after {attempts} attempts: {last_error}")]
53 StructuredOutputFailed { attempts: usize, last_error: String },
54
55 #[error("agent is already running")]
57 AlreadyRunning,
58
59 #[error("cannot continue with empty message history")]
61 NoMessages,
62
63 #[error("cannot continue when last message is an assistant message")]
65 InvalidContinue,
66
67 #[error("stream error")]
69 StreamError {
70 #[source]
71 source: Box<dyn std::error::Error + Send + Sync>,
72 },
73
74 #[error("operation aborted via cancellation token")]
76 Aborted,
77
78 #[error("plugin error ({name})")]
80 Plugin {
81 name: String,
82 source: Box<dyn std::error::Error + Send + Sync>,
83 },
84
85 #[error("provider cache miss")]
91 CacheMiss,
92
93 #[error("content filtered by provider safety policy")]
99 ContentFiltered,
100
101 #[error("sync API called inside an active Tokio runtime — use the async variant instead")]
108 SyncInAsyncContext,
109
110 #[error("failed to create Tokio runtime for sync API")]
112 RuntimeInit {
113 #[source]
114 source: std::io::Error,
115 },
116}
117
118impl AgentError {
119 #[must_use]
122 pub const fn is_retryable(&self) -> bool {
123 matches!(self, Self::ModelThrottled | Self::NetworkError { .. })
124 }
125
126 pub fn network(err: impl std::error::Error + Send + Sync + 'static) -> Self {
128 Self::NetworkError {
129 source: Box::new(err),
130 }
131 }
132
133 pub fn stream(err: impl std::error::Error + Send + Sync + 'static) -> Self {
135 Self::StreamError {
136 source: Box::new(err),
137 }
138 }
139
140 pub fn context_overflow(model: impl Into<String>) -> Self {
142 Self::ContextWindowOverflow {
143 model: model.into(),
144 }
145 }
146
147 pub fn structured_output_failed(attempts: usize, last_error: impl Into<String>) -> Self {
149 Self::StructuredOutputFailed {
150 attempts,
151 last_error: last_error.into(),
152 }
153 }
154
155 pub fn plugin(
157 name: impl Into<String>,
158 source: impl std::error::Error + Send + Sync + 'static,
159 ) -> Self {
160 Self::Plugin {
161 name: name.into(),
162 source: Box::new(source),
163 }
164 }
165
166 pub const fn runtime_init(source: std::io::Error) -> Self {
168 Self::RuntimeInit { source }
169 }
170}
171
172impl From<std::io::Error> for AgentError {
173 fn from(err: std::io::Error) -> Self {
174 Self::NetworkError {
175 source: Box::new(err),
176 }
177 }
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183
184 #[test]
185 fn agent_error_plugin_display() {
186 let err = AgentError::plugin("my-plugin", std::io::Error::other("boom"));
187 let msg = format!("{err}");
188 assert_eq!(msg, "plugin error (my-plugin)");
189 }
190
191 #[test]
192 fn plugin_error_not_retryable() {
193 let err = AgentError::plugin("test", std::io::Error::other("fail"));
194 assert!(!err.is_retryable());
195 }
196
197 #[test]
198 fn content_filtered_not_retryable() {
199 let err = AgentError::ContentFiltered;
200 assert!(!err.is_retryable());
201 assert_eq!(
202 format!("{err}"),
203 "content filtered by provider safety policy"
204 );
205 }
206
207 #[test]
208 fn sync_in_async_context_not_retryable() {
209 let err = AgentError::SyncInAsyncContext;
210 assert!(!err.is_retryable());
211 assert!(format!("{err}").contains("sync API"));
212 }
213}