1use std::collections::HashMap;
4
5use chrono::Utc;
6use serde_json::Value;
7
8use crate::errordetails::{self, TypedDetail};
9
10pub mod error_code {
12 pub const TASK_NOT_FOUND: i32 = -32001;
14 pub const TASK_NOT_CANCELABLE: i32 = -32002;
15 pub const PUSH_NOTIFICATION_NOT_SUPPORTED: i32 = -32003;
16 pub const UNSUPPORTED_OPERATION: i32 = -32004;
17 pub const CONTENT_TYPE_NOT_SUPPORTED: i32 = -32005;
18 pub const INVALID_AGENT_RESPONSE: i32 = -32006;
19 pub const EXTENDED_CARD_NOT_CONFIGURED: i32 = -32007;
20 pub const EXTENSION_SUPPORT_REQUIRED: i32 = -32008;
21 pub const VERSION_NOT_SUPPORTED: i32 = -32009;
22
23 pub const PARSE_ERROR: i32 = -32700;
25 pub const INVALID_REQUEST: i32 = -32600;
26 pub const METHOD_NOT_FOUND: i32 = -32601;
27 pub const INVALID_PARAMS: i32 = -32602;
28 pub const INTERNAL_ERROR: i32 = -32603;
29}
30
31pub fn error_reason(code: i32) -> &'static str {
33 match code {
34 error_code::TASK_NOT_FOUND => "TASK_NOT_FOUND",
35 error_code::TASK_NOT_CANCELABLE => "TASK_NOT_CANCELABLE",
36 error_code::PUSH_NOTIFICATION_NOT_SUPPORTED => "PUSH_NOTIFICATION_NOT_SUPPORTED",
37 error_code::UNSUPPORTED_OPERATION => "UNSUPPORTED_OPERATION",
38 error_code::CONTENT_TYPE_NOT_SUPPORTED => "CONTENT_TYPE_NOT_SUPPORTED",
39 error_code::INVALID_AGENT_RESPONSE => "INVALID_AGENT_RESPONSE",
40 error_code::EXTENDED_CARD_NOT_CONFIGURED => "EXTENDED_AGENT_CARD_NOT_CONFIGURED",
41 error_code::EXTENSION_SUPPORT_REQUIRED => "EXTENSION_SUPPORT_REQUIRED",
42 error_code::VERSION_NOT_SUPPORTED => "VERSION_NOT_SUPPORTED",
43 error_code::PARSE_ERROR => "PARSE_ERROR",
44 error_code::INVALID_REQUEST => "INVALID_REQUEST",
45 error_code::METHOD_NOT_FOUND => "METHOD_NOT_FOUND",
46 error_code::INVALID_PARAMS => "INVALID_PARAMS",
47 _ => "INTERNAL_ERROR",
48 }
49}
50
51pub fn reason_to_error_code(reason: &str) -> Option<i32> {
53 match reason {
54 "TASK_NOT_FOUND" => Some(error_code::TASK_NOT_FOUND),
55 "TASK_NOT_CANCELABLE" => Some(error_code::TASK_NOT_CANCELABLE),
56 "PUSH_NOTIFICATION_NOT_SUPPORTED" => Some(error_code::PUSH_NOTIFICATION_NOT_SUPPORTED),
57 "UNSUPPORTED_OPERATION" => Some(error_code::UNSUPPORTED_OPERATION),
58 "UNSUPPORTED_CONTENT_TYPE" | "CONTENT_TYPE_NOT_SUPPORTED" => {
59 Some(error_code::CONTENT_TYPE_NOT_SUPPORTED)
60 }
61 "INVALID_AGENT_RESPONSE" => Some(error_code::INVALID_AGENT_RESPONSE),
62 "EXTENDED_AGENT_CARD_NOT_CONFIGURED" | "EXTENDED_CARD_NOT_CONFIGURED" => {
63 Some(error_code::EXTENDED_CARD_NOT_CONFIGURED)
64 }
65 "EXTENSION_SUPPORT_REQUIRED" => Some(error_code::EXTENSION_SUPPORT_REQUIRED),
66 "VERSION_NOT_SUPPORTED" => Some(error_code::VERSION_NOT_SUPPORTED),
67 "PARSE_ERROR" => Some(error_code::PARSE_ERROR),
68 "INVALID_REQUEST" => Some(error_code::INVALID_REQUEST),
69 "METHOD_NOT_FOUND" => Some(error_code::METHOD_NOT_FOUND),
70 "INVALID_PARAMS" => Some(error_code::INVALID_PARAMS),
71 "INTERNAL_ERROR" => Some(error_code::INTERNAL_ERROR),
72 _ => None,
73 }
74}
75
76#[derive(Debug, Clone, thiserror::Error)]
78#[error("{message}")]
79pub struct A2AError {
80 pub code: i32,
81 pub message: String,
82 pub details: Option<Vec<TypedDetail>>,
83}
84
85impl A2AError {
86 pub fn new(code: i32, message: impl Into<String>) -> Self {
87 A2AError {
88 code,
89 message: message.into(),
90 details: None,
91 }
92 }
93
94 pub fn with_details(mut self, details: Vec<TypedDetail>) -> Self {
95 self.details = Some(details);
96 self
97 }
98
99 pub fn task_not_found(task_id: &str) -> Self {
102 A2AError::new(
103 error_code::TASK_NOT_FOUND,
104 format!("task not found: {task_id}"),
105 )
106 }
107
108 pub fn task_not_cancelable(task_id: &str) -> Self {
109 A2AError::new(
110 error_code::TASK_NOT_CANCELABLE,
111 format!("task cannot be canceled: {task_id}"),
112 )
113 }
114
115 pub fn push_notification_not_supported() -> Self {
116 A2AError::new(
117 error_code::PUSH_NOTIFICATION_NOT_SUPPORTED,
118 "push notification not supported",
119 )
120 }
121
122 pub fn unsupported_operation(msg: impl Into<String>) -> Self {
123 A2AError::new(error_code::UNSUPPORTED_OPERATION, msg)
124 }
125
126 pub fn content_type_not_supported() -> Self {
127 A2AError::new(
128 error_code::CONTENT_TYPE_NOT_SUPPORTED,
129 "incompatible content types",
130 )
131 }
132
133 pub fn invalid_agent_response() -> Self {
134 A2AError::new(error_code::INVALID_AGENT_RESPONSE, "invalid agent response")
135 }
136
137 pub fn version_not_supported(version: &str) -> Self {
138 A2AError::new(
139 error_code::VERSION_NOT_SUPPORTED,
140 format!("version not supported: {version}"),
141 )
142 }
143
144 pub fn internal(msg: impl Into<String>) -> Self {
145 A2AError::new(error_code::INTERNAL_ERROR, msg)
146 }
147
148 pub fn invalid_params(msg: impl Into<String>) -> Self {
149 A2AError::new(error_code::INVALID_PARAMS, msg)
150 }
151
152 pub fn parse_error(msg: impl Into<String>) -> Self {
153 A2AError::new(error_code::PARSE_ERROR, msg)
154 }
155
156 pub fn invalid_request(msg: impl Into<String>) -> Self {
157 A2AError::new(error_code::INVALID_REQUEST, msg)
158 }
159
160 pub fn method_not_found(method: &str) -> Self {
161 A2AError::new(
162 error_code::METHOD_NOT_FOUND,
163 format!("method not found: {method}"),
164 )
165 }
166
167 pub fn http_status_code(&self) -> u16 {
169 match self.code {
170 error_code::TASK_NOT_FOUND => 404,
171 error_code::TASK_NOT_CANCELABLE => 400,
172 error_code::PUSH_NOTIFICATION_NOT_SUPPORTED => 400,
173 error_code::UNSUPPORTED_OPERATION => 400,
174 error_code::CONTENT_TYPE_NOT_SUPPORTED => 400,
175 error_code::VERSION_NOT_SUPPORTED => 400,
176 error_code::PARSE_ERROR => 400,
177 error_code::INVALID_REQUEST => 400,
178 error_code::METHOD_NOT_FOUND => 501,
179 error_code::INVALID_PARAMS => 400,
180 error_code::INTERNAL_ERROR => 500,
181 _ => 500,
182 }
183 }
184
185 pub fn to_jsonrpc_error(&self) -> crate::JsonRpcError {
191 let reason = error_reason(self.code);
192 let metadata = HashMap::from([("timestamp".to_string(), Utc::now().to_rfc3339())]);
193
194 let mut data: Vec<Value> = self
195 .details
196 .as_ref()
197 .map(|d| {
198 d.iter()
199 .filter(|detail| detail.type_url != errordetails::ERROR_INFO_TYPE)
200 .map(|detail| serde_json::to_value(detail).unwrap_or_default())
201 .collect()
202 })
203 .unwrap_or_default();
204
205 let mut error_info =
206 TypedDetail::error_info(reason, errordetails::PROTOCOL_DOMAIN, Some(metadata));
207
208 if let Some(details) = &self.details {
209 for existing in details
210 .iter()
211 .filter(|d| d.type_url == errordetails::ERROR_INFO_TYPE)
212 {
213 if let Some(Value::Object(meta)) = existing.value.get("metadata") {
214 if let Some(Value::Object(info_meta)) = error_info.value.get_mut("metadata") {
215 for (k, v) in meta {
216 info_meta.entry(k.clone()).or_insert_with(|| v.clone());
217 }
218 }
219 }
220 }
221 }
222
223 data.push(serde_json::to_value(&error_info).unwrap_or_default());
224
225 crate::JsonRpcError {
226 code: self.code,
227 message: self.message.clone(),
228 data: Some(Value::Array(data)),
229 }
230 }
231}
232
233impl From<A2AError> for crate::JsonRpcError {
234 fn from(e: A2AError) -> Self {
235 e.to_jsonrpc_error()
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242
243 #[test]
244 fn test_error_constructors() {
245 let e = A2AError::task_not_found("t1");
246 assert_eq!(e.code, error_code::TASK_NOT_FOUND);
247 assert!(e.message.contains("t1"));
248
249 let e = A2AError::task_not_cancelable("t2");
250 assert_eq!(e.code, error_code::TASK_NOT_CANCELABLE);
251
252 let e = A2AError::push_notification_not_supported();
253 assert_eq!(e.code, error_code::PUSH_NOTIFICATION_NOT_SUPPORTED);
254
255 let e = A2AError::unsupported_operation("nope");
256 assert_eq!(e.code, error_code::UNSUPPORTED_OPERATION);
257
258 let e = A2AError::content_type_not_supported();
259 assert_eq!(e.code, error_code::CONTENT_TYPE_NOT_SUPPORTED);
260
261 let e = A2AError::invalid_agent_response();
262 assert_eq!(e.code, error_code::INVALID_AGENT_RESPONSE);
263
264 let e = A2AError::version_not_supported("2.0");
265 assert_eq!(e.code, error_code::VERSION_NOT_SUPPORTED);
266
267 let e = A2AError::internal("boom");
268 assert_eq!(e.code, error_code::INTERNAL_ERROR);
269
270 let e = A2AError::invalid_params("bad param");
271 assert_eq!(e.code, error_code::INVALID_PARAMS);
272
273 let e = A2AError::parse_error("bad json");
274 assert_eq!(e.code, error_code::PARSE_ERROR);
275
276 let e = A2AError::invalid_request("bad req");
277 assert_eq!(e.code, error_code::INVALID_REQUEST);
278
279 let e = A2AError::method_not_found("foo");
280 assert_eq!(e.code, error_code::METHOD_NOT_FOUND);
281 }
282
283 #[test]
284 fn test_http_status_codes() {
285 assert_eq!(A2AError::task_not_found("x").http_status_code(), 404);
286 assert_eq!(A2AError::task_not_cancelable("x").http_status_code(), 400);
287 assert_eq!(A2AError::internal("x").http_status_code(), 500);
288 assert_eq!(A2AError::invalid_params("x").http_status_code(), 400);
289 assert_eq!(
290 A2AError::content_type_not_supported().http_status_code(),
291 400
292 );
293 assert_eq!(A2AError::new(9999, "unknown").http_status_code(), 500);
294 }
295
296 #[test]
297 fn test_http_status_codes_for_remaining_a2a_mappings() {
298 assert_eq!(
299 A2AError::push_notification_not_supported().http_status_code(),
300 400
301 );
302 assert_eq!(
303 A2AError::unsupported_operation("nope").http_status_code(),
304 400
305 );
306 assert_eq!(
307 A2AError::version_not_supported("9.9").http_status_code(),
308 400
309 );
310 assert_eq!(A2AError::parse_error("bad").http_status_code(), 400);
311 assert_eq!(A2AError::invalid_request("bad").http_status_code(), 400);
312 assert_eq!(
313 A2AError::method_not_found("missing").http_status_code(),
314 501
315 );
316 }
317
318 #[test]
319 fn test_to_jsonrpc_error() {
320 let e = A2AError::task_not_found("t1");
321 let rpc = e.to_jsonrpc_error();
322 assert_eq!(rpc.code, error_code::TASK_NOT_FOUND);
323 assert!(rpc.message.contains("t1"));
324 let data = rpc.data.expect("data should always be present");
325 let arr = data.as_array().expect("data should be an array");
326 assert_eq!(arr.len(), 1);
327 assert_eq!(arr[0]["@type"], errordetails::ERROR_INFO_TYPE);
328 assert_eq!(arr[0]["reason"], "TASK_NOT_FOUND");
329 assert_eq!(arr[0]["domain"], errordetails::PROTOCOL_DOMAIN);
330 assert!(arr[0]["metadata"]["timestamp"].is_string());
331 }
332
333 #[test]
334 fn test_with_details() {
335 use std::collections::HashMap;
336 let struct_detail = TypedDetail::from_struct(HashMap::from([(
337 "key".to_string(),
338 Value::String("val".to_string()),
339 )]));
340 let e = A2AError::internal("err").with_details(vec![struct_detail]);
341
342 let rpc = e.to_jsonrpc_error();
343 let data = rpc.data.expect("data should always be present");
344 let arr = data.as_array().unwrap();
345 assert_eq!(arr.len(), 2);
346 assert_eq!(arr[0]["key"], "val");
347 assert_eq!(arr[1]["@type"], errordetails::ERROR_INFO_TYPE);
348 assert_eq!(arr[1]["reason"], "INTERNAL_ERROR");
349 }
350
351 #[test]
352 fn test_to_jsonrpc_error_merges_existing_error_info_metadata() {
353 let existing_info = TypedDetail::error_info(
354 "TASK_NOT_FOUND",
355 errordetails::PROTOCOL_DOMAIN,
356 Some(HashMap::from([("taskId".to_string(), "t1".to_string())])),
357 );
358 let e = A2AError::task_not_found("t1").with_details(vec![existing_info]);
359 let rpc = e.to_jsonrpc_error();
360 let data = rpc.data.unwrap();
361 let arr = data.as_array().unwrap();
362 assert_eq!(arr.len(), 1);
364 assert_eq!(arr[0]["@type"], errordetails::ERROR_INFO_TYPE);
365 assert_eq!(arr[0]["metadata"]["taskId"], "t1");
366 assert!(arr[0]["metadata"]["timestamp"].is_string());
367 }
368
369 #[test]
370 fn test_reason_to_error_code() {
371 assert_eq!(
372 reason_to_error_code("TASK_NOT_FOUND"),
373 Some(error_code::TASK_NOT_FOUND)
374 );
375 assert_eq!(
376 reason_to_error_code("TASK_NOT_CANCELABLE"),
377 Some(error_code::TASK_NOT_CANCELABLE)
378 );
379 assert_eq!(
380 reason_to_error_code("PUSH_NOTIFICATION_NOT_SUPPORTED"),
381 Some(error_code::PUSH_NOTIFICATION_NOT_SUPPORTED)
382 );
383 assert_eq!(
384 reason_to_error_code("UNSUPPORTED_OPERATION"),
385 Some(error_code::UNSUPPORTED_OPERATION)
386 );
387 assert_eq!(
388 reason_to_error_code("CONTENT_TYPE_NOT_SUPPORTED"),
389 Some(error_code::CONTENT_TYPE_NOT_SUPPORTED)
390 );
391 assert_eq!(
392 reason_to_error_code("UNSUPPORTED_CONTENT_TYPE"),
393 Some(error_code::CONTENT_TYPE_NOT_SUPPORTED)
394 );
395 assert_eq!(
396 reason_to_error_code("INVALID_AGENT_RESPONSE"),
397 Some(error_code::INVALID_AGENT_RESPONSE)
398 );
399 assert_eq!(
400 reason_to_error_code("EXTENDED_AGENT_CARD_NOT_CONFIGURED"),
401 Some(error_code::EXTENDED_CARD_NOT_CONFIGURED)
402 );
403 assert_eq!(
404 reason_to_error_code("EXTENDED_CARD_NOT_CONFIGURED"),
405 Some(error_code::EXTENDED_CARD_NOT_CONFIGURED)
406 );
407 assert_eq!(
408 reason_to_error_code("EXTENSION_SUPPORT_REQUIRED"),
409 Some(error_code::EXTENSION_SUPPORT_REQUIRED)
410 );
411 assert_eq!(
412 reason_to_error_code("VERSION_NOT_SUPPORTED"),
413 Some(error_code::VERSION_NOT_SUPPORTED)
414 );
415 assert_eq!(
416 reason_to_error_code("PARSE_ERROR"),
417 Some(error_code::PARSE_ERROR)
418 );
419 assert_eq!(
420 reason_to_error_code("INVALID_REQUEST"),
421 Some(error_code::INVALID_REQUEST)
422 );
423 assert_eq!(
424 reason_to_error_code("METHOD_NOT_FOUND"),
425 Some(error_code::METHOD_NOT_FOUND)
426 );
427 assert_eq!(
428 reason_to_error_code("INVALID_PARAMS"),
429 Some(error_code::INVALID_PARAMS)
430 );
431 assert_eq!(
432 reason_to_error_code("INTERNAL_ERROR"),
433 Some(error_code::INTERNAL_ERROR)
434 );
435 assert_eq!(reason_to_error_code("UNKNOWN_REASON"), None);
436 }
437
438 #[test]
439 fn test_error_display() {
440 let e = A2AError::internal("test message");
441 assert_eq!(format!("{e}"), "test message");
442 }
443
444 #[test]
445 fn test_jsonrpc_error_from() {
446 let e = A2AError::internal("test");
447 let rpc: crate::JsonRpcError = e.into();
448 assert_eq!(rpc.code, error_code::INTERNAL_ERROR);
449 }
450}