Skip to main content

rullama_a2a/
error.rs

1//! A2A error types and JSON-RPC error codes.
2
3use serde::{Deserialize, Serialize};
4
5// ---------------------------------------------------------------------------
6// JSON-RPC error codes (spec-defined)
7// ---------------------------------------------------------------------------
8
9/// Invalid JSON payload.
10pub const JSON_PARSE_ERROR: i32 = -32700;
11/// Request payload validation error.
12pub const INVALID_REQUEST: i32 = -32600;
13/// Method not found.
14pub const METHOD_NOT_FOUND: i32 = -32601;
15/// Invalid parameters.
16pub const INVALID_PARAMS: i32 = -32602;
17/// Internal error.
18pub const INTERNAL_ERROR: i32 = -32603;
19/// Task not found.
20pub const TASK_NOT_FOUND: i32 = -32001;
21/// Task cannot be canceled.
22pub const TASK_NOT_CANCELABLE: i32 = -32002;
23/// Push notification is not supported.
24pub const PUSH_NOT_SUPPORTED: i32 = -32003;
25/// This operation is not supported.
26pub const UNSUPPORTED_OPERATION: i32 = -32004;
27/// Incompatible content types.
28pub const CONTENT_TYPE_NOT_SUPPORTED: i32 = -32005;
29/// Invalid agent response.
30pub const INVALID_AGENT_RESPONSE: i32 = -32006;
31/// Authenticated Extended Card is not configured.
32pub const EXTENDED_CARD_NOT_CONFIGURED: i32 = -32007;
33/// Extension support is required but not available.
34pub const EXTENSION_SUPPORT_REQUIRED: i32 = -32008;
35/// Protocol version is not supported.
36pub const VERSION_NOT_SUPPORTED: i32 = -32009;
37
38// ---------------------------------------------------------------------------
39// Error type
40// ---------------------------------------------------------------------------
41
42/// A2A protocol error.
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct A2aError {
45    /// Numeric error code.
46    pub code: i32,
47    /// Human-readable error message.
48    pub message: String,
49    /// Optional additional data.
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub data: Option<serde_json::Value>,
52}
53
54impl A2aError {
55    /// Create a new error from code and message.
56    pub fn new(code: i32, message: impl Into<String>) -> Self {
57        Self {
58            code,
59            message: message.into(),
60            data: None,
61        }
62    }
63
64    /// Attach extra data to the error.
65    pub fn with_data(mut self, data: serde_json::Value) -> Self {
66        self.data = Some(data);
67        self
68    }
69
70    /// Task not found error.
71    pub fn task_not_found(task_id: &str) -> Self {
72        Self::new(TASK_NOT_FOUND, format!("Task not found: {task_id}"))
73    }
74
75    /// Task not cancelable error.
76    pub fn task_not_cancelable(task_id: &str) -> Self {
77        Self::new(
78            TASK_NOT_CANCELABLE,
79            format!("Task cannot be canceled: {task_id}"),
80        )
81    }
82
83    /// Push notifications not supported error.
84    pub fn push_not_supported() -> Self {
85        Self::new(PUSH_NOT_SUPPORTED, "Push notifications are not supported")
86    }
87
88    /// Unsupported operation error.
89    pub fn unsupported_operation(detail: &str) -> Self {
90        Self::new(
91            UNSUPPORTED_OPERATION,
92            format!("Unsupported operation: {detail}"),
93        )
94    }
95
96    /// Content type not supported error.
97    pub fn content_type_not_supported(detail: &str) -> Self {
98        Self::new(
99            CONTENT_TYPE_NOT_SUPPORTED,
100            format!("Content type not supported: {detail}"),
101        )
102    }
103
104    /// Invalid request error.
105    pub fn invalid_request(detail: impl Into<String>) -> Self {
106        Self::new(INVALID_REQUEST, detail)
107    }
108
109    /// Internal error.
110    pub fn internal(message: impl Into<String>) -> Self {
111        Self::new(INTERNAL_ERROR, message)
112    }
113
114    /// Method not found error.
115    pub fn method_not_found(method: &str) -> Self {
116        Self::new(METHOD_NOT_FOUND, format!("Method not found: {method}"))
117    }
118
119    /// Invalid params error.
120    pub fn invalid_params(detail: impl Into<String>) -> Self {
121        Self::new(INVALID_PARAMS, detail)
122    }
123
124    /// Parse error.
125    pub fn parse_error(detail: impl Into<String>) -> Self {
126        Self::new(JSON_PARSE_ERROR, detail)
127    }
128
129    /// Extended card not configured.
130    pub fn extended_card_not_configured() -> Self {
131        Self::new(
132            EXTENDED_CARD_NOT_CONFIGURED,
133            "Authenticated Extended Card is not configured",
134        )
135    }
136
137    /// Extension support is required but not available.
138    pub fn extension_support_required() -> Self {
139        Self::new(
140            EXTENSION_SUPPORT_REQUIRED,
141            "Extension support is required but not available",
142        )
143    }
144
145    /// Protocol version is not supported.
146    pub fn version_not_supported() -> Self {
147        Self::new(VERSION_NOT_SUPPORTED, "Protocol version is not supported")
148    }
149}
150
151impl std::fmt::Display for A2aError {
152    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153        write!(f, "A2A error {}: {}", self.code, self.message)
154    }
155}
156
157impl std::error::Error for A2aError {}
158
159impl From<serde_json::Error> for A2aError {
160    fn from(err: serde_json::Error) -> Self {
161        Self::parse_error(err.to_string())
162    }
163}
164
165impl From<anyhow::Error> for A2aError {
166    fn from(err: anyhow::Error) -> Self {
167        Self::internal(err.to_string())
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    #[test]
176    fn new_sets_code_and_message() {
177        let e = A2aError::new(TASK_NOT_FOUND, "missing");
178        assert_eq!(e.code, TASK_NOT_FOUND);
179        assert_eq!(e.message, "missing");
180        assert!(e.data.is_none());
181    }
182
183    #[test]
184    fn with_data_attaches_payload() {
185        let e = A2aError::new(INTERNAL_ERROR, "boom").with_data(serde_json::json!({"detail": "x"}));
186        assert!(e.data.is_some());
187    }
188
189    #[test]
190    fn constructors_use_correct_codes() {
191        assert_eq!(A2aError::task_not_found("t1").code, TASK_NOT_FOUND);
192        assert_eq!(
193            A2aError::task_not_cancelable("t1").code,
194            TASK_NOT_CANCELABLE
195        );
196        assert_eq!(A2aError::push_not_supported().code, PUSH_NOT_SUPPORTED);
197        assert_eq!(
198            A2aError::unsupported_operation("x").code,
199            UNSUPPORTED_OPERATION
200        );
201        assert_eq!(
202            A2aError::content_type_not_supported("x").code,
203            CONTENT_TYPE_NOT_SUPPORTED
204        );
205        assert_eq!(A2aError::invalid_request("x").code, INVALID_REQUEST);
206        assert_eq!(A2aError::internal("x").code, INTERNAL_ERROR);
207        assert_eq!(A2aError::method_not_found("m").code, METHOD_NOT_FOUND);
208        assert_eq!(A2aError::invalid_params("x").code, INVALID_PARAMS);
209        assert_eq!(A2aError::parse_error("x").code, JSON_PARSE_ERROR);
210        assert_eq!(
211            A2aError::extended_card_not_configured().code,
212            EXTENDED_CARD_NOT_CONFIGURED
213        );
214        assert_eq!(
215            A2aError::extension_support_required().code,
216            EXTENSION_SUPPORT_REQUIRED
217        );
218        assert_eq!(
219            A2aError::version_not_supported().code,
220            VERSION_NOT_SUPPORTED
221        );
222    }
223
224    #[test]
225    fn task_not_found_includes_id_in_message() {
226        let e = A2aError::task_not_found("abc-123");
227        assert!(e.message.contains("abc-123"));
228    }
229
230    #[test]
231    fn display_includes_code_and_message() {
232        let e = A2aError::new(INTERNAL_ERROR, "oops");
233        let s = e.to_string();
234        assert!(s.contains(&INTERNAL_ERROR.to_string()));
235        assert!(s.contains("oops"));
236    }
237
238    #[test]
239    fn serde_roundtrip_without_data() {
240        let original = A2aError::task_not_found("t42");
241        let json = serde_json::to_string(&original).unwrap();
242        let decoded: A2aError = serde_json::from_str(&json).unwrap();
243        assert_eq!(decoded.code, original.code);
244        assert_eq!(decoded.message, original.message);
245        assert!(decoded.data.is_none());
246    }
247
248    #[test]
249    fn serde_roundtrip_with_data() {
250        let original = A2aError::internal("err").with_data(serde_json::json!({"k": 1}));
251        let json = serde_json::to_string(&original).unwrap();
252        let decoded: A2aError = serde_json::from_str(&json).unwrap();
253        assert_eq!(decoded.code, original.code);
254        assert!(decoded.data.is_some());
255    }
256
257    #[test]
258    fn from_serde_json_error() {
259        let bad: Result<serde_json::Value, _> = serde_json::from_str("{bad json}");
260        let a2a_err: A2aError = bad.unwrap_err().into();
261        assert_eq!(a2a_err.code, JSON_PARSE_ERROR);
262    }
263
264    #[test]
265    fn from_anyhow_error() {
266        let anyhow_err = anyhow::anyhow!("something went wrong");
267        let a2a_err: A2aError = anyhow_err.into();
268        assert_eq!(a2a_err.code, INTERNAL_ERROR);
269        assert!(a2a_err.message.contains("something went wrong"));
270    }
271
272    #[test]
273    fn data_field_omitted_when_none_in_json() {
274        let e = A2aError::new(TASK_NOT_FOUND, "x");
275        let json = serde_json::to_string(&e).unwrap();
276        assert!(!json.contains("data"));
277    }
278}