Skip to main content

agentic_core/executor/
error.rs

1use http::StatusCode;
2use thiserror::Error;
3
4use crate::StorageError;
5use crate::tool::ToolError;
6use crate::utils::common::serialize_to_vec_or_default;
7
8#[non_exhaustive]
9#[derive(Debug, Error)]
10pub enum ExecutorError {
11    /// A storage layer operation failed.
12    #[error("storage error: {0}")]
13    Storage(#[from] StorageError),
14
15    /// The LLM backend returned a non-2xx status or was unreachable.
16    #[error("LLM request failed ({status}): {body}")]
17    LLMRequest { status: StatusCode, body: String },
18
19    /// A network error occurred reading from the LLM response stream.
20    ///
21    /// The original `reqwest::Error` is preserved as the error source so
22    /// callers can inspect the underlying network failure.
23    #[error("network error: {0}")]
24    NetworkError(
25        #[from]
26        #[source]
27        reqwest::Error,
28    ),
29
30    /// JSON deserialisation failed.
31    ///
32    /// The original `serde_json::Error` is preserved as the error source so
33    /// callers can inspect the exact parse failure location and kind.
34    #[error("json error: {0}")]
35    JsonError(
36        #[from]
37        #[source]
38        serde_json::Error,
39    ),
40
41    /// A general stream processing error with a human-readable message.
42    ///
43    /// Used for non-network stream failures (e.g. worker thread panic).
44    #[error("stream error: {0}")]
45    StreamError(String),
46
47    /// A validation error on the request payload with a human-readable message.
48    ///
49    /// Used when required fields are missing or structurally invalid.
50    #[error("parse error: {0}")]
51    ParseError(String),
52
53    #[error("{entity} not found: {id}")]
54    NotFound { entity: String, id: String },
55
56    #[error("invalid request: {0}")]
57    InvalidRequest(String),
58
59    #[error("tool error: {0}")]
60    Tool(#[from] ToolError),
61}
62
63impl ExecutorError {
64    /// HTTP status code that best represents this error to an API caller.
65    #[must_use]
66    pub fn http_status(&self) -> StatusCode {
67        match self {
68            Self::Storage(e) if e.is_not_found() => StatusCode::NOT_FOUND,
69            Self::LLMRequest { status, .. } => *status,
70            Self::Tool(ToolError::Config(_)) | Self::InvalidRequest(_) | Self::JsonError(_) => StatusCode::BAD_REQUEST,
71            Self::Tool(ToolError::Execution(_)) => StatusCode::BAD_GATEWAY,
72            Self::ParseError(_) => StatusCode::UNPROCESSABLE_ENTITY,
73            _ => StatusCode::INTERNAL_SERVER_ERROR,
74        }
75    }
76
77    /// Short machine-readable error code for the API error envelope.
78    #[must_use]
79    pub fn error_code(&self) -> &'static str {
80        match self {
81            Self::Storage(e) if e.is_not_found() => "not_found",
82            Self::LLMRequest { .. } => "upstream_error",
83            Self::Tool(ToolError::Config(_)) | Self::InvalidRequest(_) | Self::ParseError(_) | Self::JsonError(_) => {
84                "invalid_request_error"
85            }
86            Self::Tool(ToolError::Execution(_)) => "tool_error",
87            _ => "server_error",
88        }
89    }
90
91    /// Serialise the error into the HTTP response body bytes.
92    ///
93    /// `LLMRequest` bodies are forwarded verbatim; all other variants are
94    /// wrapped in the standard `{"error": {"message", "type", "code"}}` envelope.
95    #[must_use]
96    pub fn into_response_body(self) -> Vec<u8> {
97        match self {
98            Self::LLMRequest { body, .. } => body.into_bytes(),
99            other => {
100                let code = other.error_code();
101                serialize_to_vec_or_default(&serde_json::json!({
102                    "error": { "message": other.to_string(), "type": code, "code": code }
103                }))
104            }
105        }
106    }
107}
108
109pub type ExecutorResult<T> = Result<T, ExecutorError>;
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn test_executor_error_display() {
117        let err = ExecutorError::InvalidRequest("test message".into());
118        assert!(err.to_string().contains("invalid request"));
119        assert!(err.to_string().contains("test message"));
120    }
121
122    #[test]
123    fn test_executor_error_stream() {
124        let err = ExecutorError::StreamError("connection lost".into());
125        assert!(err.to_string().contains("stream error"));
126    }
127
128    #[test]
129    fn test_executor_error_not_found() {
130        let err = ExecutorError::NotFound {
131            entity: "Conversation".into(),
132            id: "conv_123".into(),
133        };
134        assert!(err.to_string().contains("Conversation"));
135        assert!(err.to_string().contains("conv_123"));
136    }
137
138    #[test]
139    fn test_executor_error_from_storage() {
140        let storage_err = StorageError::NotConfigured;
141        let exec_err = ExecutorError::from(storage_err);
142        assert!(exec_err.to_string().contains("storage error"));
143    }
144
145    #[test]
146    fn test_executor_error_json_preserves_source() {
147        use std::error::Error;
148        let json_err: serde_json::Error = serde_json::from_str::<serde_json::Value>("{bad}").unwrap_err();
149        let exec_err = ExecutorError::from(json_err);
150        assert!(exec_err.source().is_some(), "source should be chained");
151        assert!(exec_err.to_string().contains("json error"));
152    }
153}