Skip to main content

behest_core/
error.rs

1//! Error types shared across the public API.
2
3use std::time::Duration;
4
5use thiserror::Error;
6
7use crate::id::ProviderId;
8
9/// Crate-wide result type.
10pub type Result<T, E = Error> = std::result::Result<T, E>;
11
12/// Top-level crate error.
13#[derive(Debug, Error)]
14#[non_exhaustive]
15pub enum Error {
16    /// Error returned by a model or embedding provider.
17    #[error(transparent)]
18    Provider(#[from] ProviderError),
19
20    /// Error from tool execution.
21    #[error(transparent)]
22    Tool(#[from] ToolError),
23
24    /// Error from context construction.
25    #[error(transparent)]
26    Context(#[from] ContextError),
27
28    /// Error from a storage backend.
29    #[error(transparent)]
30    Storage(#[from] StorageError),
31
32    /// Configuration loading or validation error.
33    #[error("configuration error: {0}")]
34    Config(String),
35}
36
37/// Errors produced by provider implementations.
38#[derive(Debug, Error)]
39#[non_exhaustive]
40pub enum ProviderError {
41    /// Provider credentials were rejected.
42    #[error("authentication failed for provider `{provider}`")]
43    Authentication {
44        /// Provider that rejected authentication.
45        provider: ProviderId,
46    },
47
48    /// Request payload was invalid for the target provider.
49    #[error("provider `{provider}` rejected request: {message}")]
50    BadRequest {
51        /// Provider that rejected the request.
52        provider: ProviderId,
53        /// Provider-facing validation message.
54        message: String,
55    },
56
57    /// Provider asked the caller to slow down.
58    #[error("provider `{provider}` rate limited request")]
59    RateLimited {
60        /// Provider that rate limited the request.
61        provider: ProviderId,
62        /// Suggested delay before retrying.
63        retry_after: Option<Duration>,
64    },
65
66    /// Provider call exceeded the configured timeout.
67    #[error("provider `{provider}` timed out")]
68    Timeout {
69        /// Provider that timed out.
70        provider: ProviderId,
71    },
72
73    /// Provider is temporarily overloaded.
74    #[error("provider `{provider}` is overloaded")]
75    Overloaded {
76        /// Provider that reported overload.
77        provider: ProviderId,
78    },
79
80    /// Requested provider feature is unavailable.
81    #[error("provider `{provider}` does not support feature `{feature}`")]
82    Unsupported {
83        /// Provider that cannot serve the feature.
84        provider: ProviderId,
85        /// Feature name, such as `chat_stream` or `embedding`.
86        feature: String,
87    },
88
89    /// Transport layer failed before a provider response was decoded.
90    #[error("transport error for provider `{provider}`: {source}")]
91    Transport {
92        /// Provider reached by the transport.
93        provider: ProviderId,
94        /// Lower-level HTTP client error.
95        #[source]
96        source: TransportError,
97    },
98
99    /// Provider response could not be decoded into the neutral schema.
100    #[error("failed to decode response from provider `{provider}`: {message}")]
101    Decode {
102        /// Provider that returned an invalid response.
103        provider: ProviderId,
104        /// Decode failure summary.
105        message: String,
106    },
107
108    /// Provider returned a structured error response.
109    #[error("provider `{provider}` returned error: {message}")]
110    Provider {
111        /// Provider that returned the error.
112        provider: ProviderId,
113        /// Optional HTTP status or provider-specific status code.
114        status: Option<u16>,
115        /// Human-readable provider error message.
116        message: String,
117    },
118}
119
120impl ProviderError {
121    /// Returns `true` when retrying the same request may succeed.
122    #[must_use]
123    pub const fn is_retryable(&self) -> bool {
124        matches!(
125            self,
126            Self::RateLimited { .. }
127                | Self::Timeout { .. }
128                | Self::Overloaded { .. }
129                | Self::Transport { .. }
130        )
131    }
132
133    /// Returns `true` when the error indicates the request exceeded the
134    /// model's context window and should be retried after compaction.
135    #[must_use]
136    pub fn is_context_overflow(&self) -> bool {
137        let message = match self {
138            Self::BadRequest { message, .. } | Self::Provider { message, .. } => message.as_str(),
139            _ => return false,
140        };
141
142        message.contains("context_length_exceeded")
143            || message.contains("maximum context length")
144            || message.contains("too many tokens")
145            || message.contains("reduce the length")
146            || message.contains("token limit")
147            || message.contains("prompt is too long")
148            || message.contains("input length exceeds")
149            || message.contains("context window")
150    }
151}
152
153/// A type-erased transport error.
154///
155/// This replaces the direct `reqwest::Error` dependency so that `behest-core`
156/// remains free of HTTP client dependencies.
157#[derive(Debug)]
158pub struct TransportError(Box<dyn std::error::Error + Send + Sync>);
159
160impl TransportError {
161    /// Wraps a transport-level error.
162    #[must_use]
163    pub fn new(error: impl std::error::Error + Send + Sync + 'static) -> Self {
164        Self(Box::new(error))
165    }
166}
167
168impl std::fmt::Display for TransportError {
169    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170        self.0.fmt(f)
171    }
172}
173
174impl std::error::Error for TransportError {
175    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
176        Some(self.0.as_ref())
177    }
178}
179
180/// Errors produced during tool execution.
181#[derive(Debug, Error)]
182#[non_exhaustive]
183pub enum ToolError {
184    /// Requested tool is not registered.
185    #[error("tool `{name}` is not registered")]
186    NotFound {
187        /// Name of the missing tool.
188        name: String,
189    },
190
191    /// Tool execution failed with an application error.
192    #[error("tool `{name}` failed: {message}")]
193    Execution {
194        /// Name of the tool that failed.
195        name: String,
196        /// Human-readable error message.
197        message: String,
198    },
199
200    /// Tool arguments could not be parsed or validated.
201    #[error("tool `{name}` received invalid arguments: {message}")]
202    InvalidArguments {
203        /// Name of the tool that rejected arguments.
204        name: String,
205        /// Validation or parsing failure description.
206        message: String,
207    },
208
209    /// Tool is defined but not yet implemented.
210    #[error("tool `{name}` is not implemented")]
211    NotImplemented {
212        /// Name of the unimplemented tool.
213        name: String,
214    },
215}
216
217/// Errors produced during context construction.
218#[derive(Debug, Error)]
219#[non_exhaustive]
220pub enum ContextError {
221    /// Context adapter failed to produce its fragment.
222    #[error("context adapter `{adapter}` failed: {message}")]
223    AdapterFailed {
224        /// Name of the adapter that failed.
225        adapter: String,
226        /// Human-readable error message.
227        message: String,
228    },
229
230    /// Context input was invalid or incomplete.
231    #[error("invalid context input: {message}")]
232    InvalidInput {
233        /// Description of the validation failure.
234        message: String,
235    },
236
237    /// Required context adapter is not registered.
238    #[error("context adapter `{adapter}` is not registered")]
239    AdapterNotFound {
240        /// Name of the missing adapter.
241        adapter: String,
242    },
243}
244
245/// Errors produced by storage backends.
246#[derive(Debug, Error)]
247#[non_exhaustive]
248pub enum StorageError {
249    /// Requested entity was not found.
250    #[error("storage entity not found: `{id}`")]
251    NotFound {
252        /// Identifier of the missing entity.
253        id: String,
254    },
255
256    /// Failed to connect to the storage backend.
257    #[error("storage connection failed ({backend}): {message}")]
258    ConnectionFailed {
259        /// Backend identifier (e.g., "postgres", "redis").
260        backend: String,
261        /// Human-readable failure description.
262        message: String,
263        /// Lower-level error, if available.
264        #[source]
265        source: Option<Box<dyn std::error::Error + Send + Sync>>,
266    },
267
268    /// Serialization or deserialization failed.
269    #[error("storage serialization failed: {message}")]
270    SerializationFailed {
271        /// Description of the serialization failure.
272        message: String,
273        /// Lower-level error, if available.
274        #[source]
275        source: Option<Box<dyn std::error::Error + Send + Sync>>,
276    },
277
278    /// Backend-specific operational error.
279    #[error("storage backend error ({backend}): {message}")]
280    BackendError {
281        /// Backend identifier.
282        backend: String,
283        /// Human-readable error description.
284        message: String,
285        /// Lower-level error, if available.
286        #[source]
287        source: Option<Box<dyn std::error::Error + Send + Sync>>,
288    },
289
290    /// Schema migration failed.
291    #[error("storage migration failed ({backend}): {message}")]
292    MigrationFailed {
293        /// Backend identifier.
294        backend: String,
295        /// Human-readable failure description.
296        message: String,
297        /// Lower-level error, if available.
298        #[source]
299        source: Option<Box<dyn std::error::Error + Send + Sync>>,
300    },
301
302    /// Stored data is malformed or corrupted and cannot be interpreted.
303    #[error("data corruption in `{field}`: {message}")]
304    DataCorruption {
305        /// Name of the corrupted field (e.g., "session.id", "message.metadata").
306        field: String,
307        /// Human-readable description of the corruption.
308        message: String,
309        /// Underlying parse or deserialization error, if available.
310        #[source]
311        source: Option<Box<dyn std::error::Error + Send + Sync>>,
312    },
313}