Skip to main content

autumn_plugin_agent/
error.rs

1//! Error types for the agent plugin.
2//!
3//! Every failure the plugin can produce carries an [`ErrorKind`]. The kind
4//! maps to an HTTP status code through [`ErrorKind::status_code`], and to an
5//! [`AutumnError`](autumn_web::AutumnError) through
6//! [`AgentError::into_autumn_error`] (a method, not a `From` impl, because
7//! Autumn already provides a blanket `From<E: Error>` that would conflict).
8//! Handlers and jobs translate agent failures into framework responses with
9//! one call.
10
11use http::StatusCode;
12use thiserror::Error;
13
14/// Machine-readable category for an [`AgentError`].
15///
16/// Variants stay coarse on purpose: callers match on these, humans read the
17/// message.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19#[non_exhaustive]
20pub enum ErrorKind {
21    /// Plugin configuration is missing or invalid.
22    Config,
23    /// The provider rejected the credentials (`AGENT_API_KEY` is wrong or
24    /// revoked).
25    Authentication,
26    /// The provider rate-limited the request.
27    RateLimited,
28    /// The provider answered with an error payload.
29    Provider,
30    /// The HTTP transport failed before a response arrived.
31    Transport,
32    /// A tool failed while executing.
33    Tool,
34    /// A budget (steps, tokens, or context window) overflowed.
35    Budget,
36    /// A provider response could not be decoded.
37    Decode,
38}
39
40impl ErrorKind {
41    /// Map the kind to the HTTP status an app should answer with.
42    ///
43    /// The mapping treats the provider as an upstream dependency: provider
44    /// failures surface as 502/503/429, never as 4xx against the caller.
45    #[must_use]
46    pub const fn status_code(self) -> StatusCode {
47        match self {
48            Self::Config | Self::Tool | Self::Decode => StatusCode::INTERNAL_SERVER_ERROR,
49            Self::Authentication | Self::Provider => StatusCode::BAD_GATEWAY,
50            Self::RateLimited => StatusCode::TOO_MANY_REQUESTS,
51            Self::Transport => StatusCode::SERVICE_UNAVAILABLE,
52            Self::Budget => StatusCode::PAYLOAD_TOO_LARGE,
53        }
54    }
55}
56
57/// The plugin's error type.
58///
59/// Build one with [`AgentError::new`] or the `From` impls for the usual
60/// failure sources. Inspect it with [`AgentError::kind`] and
61/// [`AgentError::status_code`].
62#[derive(Debug, Error)]
63#[error("[{kind:?}] {message}")]
64pub struct AgentError {
65    kind: ErrorKind,
66    message: String,
67    #[source]
68    source: Option<Box<dyn std::error::Error + Send + Sync>>,
69}
70
71impl AgentError {
72    /// Build an error from a kind and a message.
73    #[must_use]
74    pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
75        Self {
76            kind,
77            message: message.into(),
78            source: None,
79        }
80    }
81
82    /// Build an error that keeps the underlying cause for `Error::source`.
83    pub fn with_source<E>(kind: ErrorKind, message: impl Into<String>, source: E) -> Self
84    where
85        E: std::error::Error + Send + Sync + 'static,
86    {
87        Self {
88            kind,
89            message: message.into(),
90            source: Some(Box::new(source)),
91        }
92    }
93
94    /// Return the machine-readable category.
95    #[must_use]
96    pub const fn kind(&self) -> ErrorKind {
97        self.kind
98    }
99
100    /// Return the HTTP status an app should answer with.
101    #[must_use]
102    pub const fn status_code(&self) -> StatusCode {
103        self.kind.status_code()
104    }
105
106    /// Return the human-readable message.
107    #[must_use]
108    pub fn message(&self) -> &str {
109        &self.message
110    }
111
112    /// Translate the agent failure into a framework error.
113    ///
114    /// Kinds that describe the app's own misconfiguration or tool code map
115    /// to 500 helpers; upstream provider failures map to 502/503/429 through
116    /// [`AutumnError::with_status`](autumn_web::AutumnError::with_status).
117    /// This is a method rather than a `From` impl because Autumn ships a
118    /// blanket `From<E: Error>` for `AutumnError` that would conflict.
119    #[must_use]
120    pub fn into_autumn_error(self) -> autumn_web::AutumnError {
121        let message = self.to_string();
122        match self.kind() {
123            ErrorKind::Config | ErrorKind::Tool | ErrorKind::Decode => {
124                autumn_web::AutumnError::internal_server_error_msg(message)
125            }
126            ErrorKind::Authentication | ErrorKind::Provider => {
127                autumn_web::AutumnError::internal_server_error_msg(message)
128                    .with_status(StatusCode::BAD_GATEWAY)
129            }
130            ErrorKind::RateLimited => autumn_web::AutumnError::service_unavailable_msg(message)
131                .with_status(StatusCode::TOO_MANY_REQUESTS),
132            ErrorKind::Transport => autumn_web::AutumnError::service_unavailable_msg(message),
133            ErrorKind::Budget => autumn_web::AutumnError::internal_server_error_msg(message)
134                .with_status(StatusCode::PAYLOAD_TOO_LARGE),
135        }
136    }
137}
138
139impl From<serde_json::Error> for AgentError {
140    fn from(err: serde_json::Error) -> Self {
141        Self::with_source(ErrorKind::Decode, "failed to decode JSON", err)
142    }
143}
144
145impl From<reqwest::Error> for AgentError {
146    fn from(err: reqwest::Error) -> Self {
147        let kind = if err.is_timeout() || err.is_connect() {
148            ErrorKind::Transport
149        } else if err.is_decode() {
150            ErrorKind::Decode
151        } else {
152            ErrorKind::Transport
153        };
154        Self::with_source(kind, "LLM provider request failed", err)
155    }
156}
157
158#[cfg(test)]
159mod tests;