Skip to main content

a2a/
errors.rs

1// Copyright AGNTCY Contributors (https://github.com/agntcy)
2// SPDX-License-Identifier: Apache-2.0
3use serde_json::Value;
4use std::collections::HashMap;
5
6/// A2A-specific error codes (JSON-RPC).
7pub mod error_code {
8    // A2A application errors
9    pub const TASK_NOT_FOUND: i32 = -32001;
10    pub const TASK_NOT_CANCELABLE: i32 = -32002;
11    pub const PUSH_NOTIFICATION_NOT_SUPPORTED: i32 = -32003;
12    pub const UNSUPPORTED_OPERATION: i32 = -32004;
13    pub const CONTENT_TYPE_NOT_SUPPORTED: i32 = -32005;
14    pub const INVALID_AGENT_RESPONSE: i32 = -32006;
15    pub const EXTENDED_CARD_NOT_CONFIGURED: i32 = -32007;
16    pub const EXTENSION_SUPPORT_REQUIRED: i32 = -32008;
17    pub const VERSION_NOT_SUPPORTED: i32 = -32009;
18
19    // Standard JSON-RPC errors
20    pub const PARSE_ERROR: i32 = -32700;
21    pub const INVALID_REQUEST: i32 = -32600;
22    pub const METHOD_NOT_FOUND: i32 = -32601;
23    pub const INVALID_PARAMS: i32 = -32602;
24    pub const INTERNAL_ERROR: i32 = -32603;
25}
26
27/// An A2A protocol error.
28#[derive(Debug, Clone, thiserror::Error)]
29#[error("{message}")]
30pub struct A2AError {
31    pub code: i32,
32    pub message: String,
33    pub details: Option<HashMap<String, Value>>,
34}
35
36impl A2AError {
37    pub fn new(code: i32, message: impl Into<String>) -> Self {
38        A2AError {
39            code,
40            message: message.into(),
41            details: None,
42        }
43    }
44
45    pub fn with_details(mut self, details: HashMap<String, Value>) -> Self {
46        self.details = Some(details);
47        self
48    }
49
50    // Convenience constructors for common errors
51
52    pub fn task_not_found(task_id: &str) -> Self {
53        A2AError::new(
54            error_code::TASK_NOT_FOUND,
55            format!("task not found: {task_id}"),
56        )
57    }
58
59    pub fn task_not_cancelable(task_id: &str) -> Self {
60        A2AError::new(
61            error_code::TASK_NOT_CANCELABLE,
62            format!("task cannot be canceled: {task_id}"),
63        )
64    }
65
66    pub fn push_notification_not_supported() -> Self {
67        A2AError::new(
68            error_code::PUSH_NOTIFICATION_NOT_SUPPORTED,
69            "push notification not supported",
70        )
71    }
72
73    pub fn unsupported_operation(msg: impl Into<String>) -> Self {
74        A2AError::new(error_code::UNSUPPORTED_OPERATION, msg)
75    }
76
77    pub fn content_type_not_supported() -> Self {
78        A2AError::new(
79            error_code::CONTENT_TYPE_NOT_SUPPORTED,
80            "incompatible content types",
81        )
82    }
83
84    pub fn invalid_agent_response() -> Self {
85        A2AError::new(error_code::INVALID_AGENT_RESPONSE, "invalid agent response")
86    }
87
88    pub fn version_not_supported(version: &str) -> Self {
89        A2AError::new(
90            error_code::VERSION_NOT_SUPPORTED,
91            format!("version not supported: {version}"),
92        )
93    }
94
95    pub fn internal(msg: impl Into<String>) -> Self {
96        A2AError::new(error_code::INTERNAL_ERROR, msg)
97    }
98
99    pub fn invalid_params(msg: impl Into<String>) -> Self {
100        A2AError::new(error_code::INVALID_PARAMS, msg)
101    }
102
103    pub fn parse_error(msg: impl Into<String>) -> Self {
104        A2AError::new(error_code::PARSE_ERROR, msg)
105    }
106
107    pub fn invalid_request(msg: impl Into<String>) -> Self {
108        A2AError::new(error_code::INVALID_REQUEST, msg)
109    }
110
111    pub fn method_not_found(method: &str) -> Self {
112        A2AError::new(
113            error_code::METHOD_NOT_FOUND,
114            format!("method not found: {method}"),
115        )
116    }
117
118    /// Map A2A error code to HTTP status code for REST binding.
119    pub fn http_status_code(&self) -> u16 {
120        match self.code {
121            error_code::TASK_NOT_FOUND => 404,
122            error_code::TASK_NOT_CANCELABLE => 409,
123            error_code::PUSH_NOTIFICATION_NOT_SUPPORTED => 400,
124            error_code::UNSUPPORTED_OPERATION => 400,
125            error_code::CONTENT_TYPE_NOT_SUPPORTED => 415,
126            error_code::VERSION_NOT_SUPPORTED => 400,
127            error_code::PARSE_ERROR => 400,
128            error_code::INVALID_REQUEST => 400,
129            error_code::METHOD_NOT_FOUND => 404,
130            error_code::INVALID_PARAMS => 400,
131            error_code::INTERNAL_ERROR => 500,
132            _ => 500,
133        }
134    }
135
136    /// Convert to a JSON-RPC error object.
137    pub fn to_jsonrpc_error(&self) -> crate::JsonRpcError {
138        crate::JsonRpcError {
139            code: self.code,
140            message: self.message.clone(),
141            data: self
142                .details
143                .as_ref()
144                .map(|d| serde_json::to_value(d).unwrap_or_default()),
145        }
146    }
147}
148
149impl From<A2AError> for crate::JsonRpcError {
150    fn from(e: A2AError) -> Self {
151        e.to_jsonrpc_error()
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[test]
160    fn test_error_constructors() {
161        let e = A2AError::task_not_found("t1");
162        assert_eq!(e.code, error_code::TASK_NOT_FOUND);
163        assert!(e.message.contains("t1"));
164
165        let e = A2AError::task_not_cancelable("t2");
166        assert_eq!(e.code, error_code::TASK_NOT_CANCELABLE);
167
168        let e = A2AError::push_notification_not_supported();
169        assert_eq!(e.code, error_code::PUSH_NOTIFICATION_NOT_SUPPORTED);
170
171        let e = A2AError::unsupported_operation("nope");
172        assert_eq!(e.code, error_code::UNSUPPORTED_OPERATION);
173
174        let e = A2AError::content_type_not_supported();
175        assert_eq!(e.code, error_code::CONTENT_TYPE_NOT_SUPPORTED);
176
177        let e = A2AError::invalid_agent_response();
178        assert_eq!(e.code, error_code::INVALID_AGENT_RESPONSE);
179
180        let e = A2AError::version_not_supported("2.0");
181        assert_eq!(e.code, error_code::VERSION_NOT_SUPPORTED);
182
183        let e = A2AError::internal("boom");
184        assert_eq!(e.code, error_code::INTERNAL_ERROR);
185
186        let e = A2AError::invalid_params("bad param");
187        assert_eq!(e.code, error_code::INVALID_PARAMS);
188
189        let e = A2AError::parse_error("bad json");
190        assert_eq!(e.code, error_code::PARSE_ERROR);
191
192        let e = A2AError::invalid_request("bad req");
193        assert_eq!(e.code, error_code::INVALID_REQUEST);
194
195        let e = A2AError::method_not_found("foo");
196        assert_eq!(e.code, error_code::METHOD_NOT_FOUND);
197    }
198
199    #[test]
200    fn test_http_status_codes() {
201        assert_eq!(A2AError::task_not_found("x").http_status_code(), 404);
202        assert_eq!(A2AError::task_not_cancelable("x").http_status_code(), 409);
203        assert_eq!(A2AError::internal("x").http_status_code(), 500);
204        assert_eq!(A2AError::invalid_params("x").http_status_code(), 400);
205        assert_eq!(
206            A2AError::content_type_not_supported().http_status_code(),
207            415
208        );
209        assert_eq!(A2AError::new(9999, "unknown").http_status_code(), 500);
210    }
211
212    #[test]
213    fn test_http_status_codes_for_remaining_a2a_mappings() {
214        assert_eq!(
215            A2AError::push_notification_not_supported().http_status_code(),
216            400
217        );
218        assert_eq!(
219            A2AError::unsupported_operation("nope").http_status_code(),
220            400
221        );
222        assert_eq!(
223            A2AError::version_not_supported("9.9").http_status_code(),
224            400
225        );
226        assert_eq!(A2AError::parse_error("bad").http_status_code(), 400);
227        assert_eq!(A2AError::invalid_request("bad").http_status_code(), 400);
228        assert_eq!(
229            A2AError::method_not_found("missing").http_status_code(),
230            404
231        );
232    }
233
234    #[test]
235    fn test_to_jsonrpc_error() {
236        let e = A2AError::task_not_found("t1");
237        let rpc = e.to_jsonrpc_error();
238        assert_eq!(rpc.code, error_code::TASK_NOT_FOUND);
239        assert!(rpc.message.contains("t1"));
240        assert!(rpc.data.is_none());
241    }
242
243    #[test]
244    fn test_with_details() {
245        let mut details = HashMap::new();
246        details.insert("key".to_string(), Value::String("val".to_string()));
247        let e = A2AError::internal("err").with_details(details.clone());
248        assert_eq!(e.details.as_ref().unwrap(), &details);
249
250        let rpc = e.to_jsonrpc_error();
251        assert!(rpc.data.is_some());
252    }
253
254    #[test]
255    fn test_error_display() {
256        let e = A2AError::internal("test message");
257        assert_eq!(format!("{e}"), "test message");
258    }
259
260    #[test]
261    fn test_jsonrpc_error_from() {
262        let e = A2AError::internal("test");
263        let rpc: crate::JsonRpcError = e.into();
264        assert_eq!(rpc.code, error_code::INTERNAL_ERROR);
265    }
266}