Skip to main content

a2a_protocol_types/
error.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Tom F.
3
4//! A2A protocol error types.
5//!
6//! This module defines [`A2aError`], the canonical error type for all A2A
7//! protocol operations, along with [`ErrorCode`] carrying every standard error
8//! code defined by A2A v1.0 and the underlying JSON-RPC 2.0 specification.
9
10use std::fmt;
11
12use serde::{Deserialize, Serialize};
13
14// ── Error codes ──────────────────────────────────────────────────────────────
15
16/// Numeric error codes defined by JSON-RPC 2.0 and the A2A v1.0 specification.
17///
18/// JSON-RPC standard codes occupy the `-32700` to `-32600` range.
19/// A2A-specific codes occupy `-32001` to `-32099`.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
21#[serde(into = "i32", try_from = "i32")]
22#[non_exhaustive]
23pub enum ErrorCode {
24    // ── JSON-RPC 2.0 standard ─────────────────────────────────────────────
25    /// Invalid JSON was received by the server (`-32700`).
26    ParseError = -32700,
27    /// The JSON sent is not a valid Request object (`-32600`).
28    InvalidRequest = -32600,
29    /// The method does not exist or is not available (`-32601`).
30    MethodNotFound = -32601,
31    /// Invalid method parameters (`-32602`).
32    InvalidParams = -32602,
33    /// Internal JSON-RPC error (`-32603`).
34    InternalError = -32603,
35
36    // ── A2A-specific ──────────────────────────────────────────────────────
37    /// The requested task was not found (`-32001`).
38    TaskNotFound = -32001,
39    /// The task cannot be canceled in its current state (`-32002`).
40    TaskNotCancelable = -32002,
41    /// The agent does not support push notifications (`-32003`).
42    PushNotificationNotSupported = -32003,
43    /// The requested operation is not supported by this agent (`-32004`).
44    UnsupportedOperation = -32004,
45    /// The requested content type is not supported (`-32005`).
46    ContentTypeNotSupported = -32005,
47    /// The agent returned an invalid response (`-32006`).
48    InvalidAgentResponse = -32006,
49    /// Extended agent card not configured (`-32007`).
50    ExtendedAgentCardNotConfigured = -32007,
51    /// A required extension is not supported (`-32008`).
52    ExtensionSupportRequired = -32008,
53    /// The requested protocol version is not supported (`-32009`).
54    VersionNotSupported = -32009,
55}
56
57impl ErrorCode {
58    /// Returns the numeric value of this error code.
59    #[must_use]
60    pub const fn as_i32(self) -> i32 {
61        self as i32
62    }
63
64    /// Returns a short human-readable description of the code.
65    #[must_use]
66    pub const fn default_message(self) -> &'static str {
67        match self {
68            Self::ParseError => "Parse error",
69            Self::InvalidRequest => "Invalid request",
70            Self::MethodNotFound => "Method not found",
71            Self::InvalidParams => "Invalid params",
72            Self::InternalError => "Internal error",
73            Self::TaskNotFound => "Task not found",
74            Self::TaskNotCancelable => "Task not cancelable",
75            Self::PushNotificationNotSupported => "Push notification not supported",
76            Self::UnsupportedOperation => "Unsupported operation",
77            Self::ContentTypeNotSupported => "Content type not supported",
78            Self::InvalidAgentResponse => "Invalid agent response",
79            Self::ExtendedAgentCardNotConfigured => "Extended agent card not configured",
80            Self::ExtensionSupportRequired => "Extension support required",
81            Self::VersionNotSupported => "Version not supported",
82        }
83    }
84}
85
86impl From<ErrorCode> for i32 {
87    fn from(code: ErrorCode) -> Self {
88        code as Self
89    }
90}
91
92impl TryFrom<i32> for ErrorCode {
93    type Error = i32;
94
95    fn try_from(v: i32) -> Result<Self, Self::Error> {
96        match v {
97            -32700 => Ok(Self::ParseError),
98            -32600 => Ok(Self::InvalidRequest),
99            -32601 => Ok(Self::MethodNotFound),
100            -32602 => Ok(Self::InvalidParams),
101            -32603 => Ok(Self::InternalError),
102            -32001 => Ok(Self::TaskNotFound),
103            -32002 => Ok(Self::TaskNotCancelable),
104            -32003 => Ok(Self::PushNotificationNotSupported),
105            -32004 => Ok(Self::UnsupportedOperation),
106            -32005 => Ok(Self::ContentTypeNotSupported),
107            -32006 => Ok(Self::InvalidAgentResponse),
108            -32007 => Ok(Self::ExtendedAgentCardNotConfigured),
109            -32008 => Ok(Self::ExtensionSupportRequired),
110            -32009 => Ok(Self::VersionNotSupported),
111            other => Err(other),
112        }
113    }
114}
115
116impl fmt::Display for ErrorCode {
117    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118        write!(f, "{} ({})", self.default_message(), self.as_i32())
119    }
120}
121
122// ── A2aError ──────────────────────────────────────────────────────────────────
123
124/// The canonical error type for A2A protocol operations.
125///
126/// Carries an [`ErrorCode`], a human-readable `message`, and an optional
127/// `data` payload (arbitrary JSON) for additional diagnostics.
128#[derive(Debug, Clone, Serialize, Deserialize)]
129#[non_exhaustive]
130pub struct A2aError {
131    /// Machine-readable error code.
132    pub code: ErrorCode,
133    /// Human-readable error message.
134    pub message: String,
135    /// Optional structured error details.
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub data: Option<serde_json::Value>,
138}
139
140impl A2aError {
141    /// Creates a new `A2aError` with the given code and message.
142    #[must_use]
143    pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
144        Self {
145            code,
146            message: message.into(),
147            data: None,
148        }
149    }
150
151    /// Creates a new `A2aError` with the given code, message, and data.
152    #[must_use]
153    pub fn with_data(code: ErrorCode, message: impl Into<String>, data: serde_json::Value) -> Self {
154        Self {
155            code,
156            message: message.into(),
157            data: Some(data),
158        }
159    }
160
161    // ── Named constructors ────────────────────────────────────────────────
162
163    /// Creates a "Task not found" error for the given task ID string.
164    #[must_use]
165    pub fn task_not_found(task_id: impl fmt::Display) -> Self {
166        Self::new(
167            ErrorCode::TaskNotFound,
168            format!("Task not found: {task_id}"),
169        )
170    }
171
172    /// Creates a "Task not cancelable" error.
173    #[must_use]
174    pub fn task_not_cancelable(task_id: impl fmt::Display) -> Self {
175        Self::new(
176            ErrorCode::TaskNotCancelable,
177            format!("Task cannot be canceled: {task_id}"),
178        )
179    }
180
181    /// Creates an internal error with the provided message.
182    #[must_use]
183    pub fn internal(msg: impl Into<String>) -> Self {
184        Self::new(ErrorCode::InternalError, msg)
185    }
186
187    /// Creates an "Invalid params" error.
188    #[must_use]
189    pub fn invalid_params(msg: impl Into<String>) -> Self {
190        Self::new(ErrorCode::InvalidParams, msg)
191    }
192
193    /// Creates an "Unsupported operation" error.
194    #[must_use]
195    pub fn unsupported_operation(msg: impl Into<String>) -> Self {
196        Self::new(ErrorCode::UnsupportedOperation, msg)
197    }
198
199    /// Creates a "Parse error" error.
200    #[must_use]
201    pub fn parse_error(msg: impl Into<String>) -> Self {
202        Self::new(ErrorCode::ParseError, msg)
203    }
204
205    /// Creates an "Invalid agent response" error.
206    #[must_use]
207    pub fn invalid_agent_response(msg: impl Into<String>) -> Self {
208        Self::new(ErrorCode::InvalidAgentResponse, msg)
209    }
210
211    /// Creates an "Extended agent card not configured" error.
212    #[must_use]
213    pub fn extended_card_not_configured(msg: impl Into<String>) -> Self {
214        Self::new(ErrorCode::ExtendedAgentCardNotConfigured, msg)
215    }
216}
217
218impl fmt::Display for A2aError {
219    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
220        write!(f, "[{}] {}", self.code.as_i32(), self.message)
221    }
222}
223
224impl std::error::Error for A2aError {}
225
226// ── A2aResult ─────────────────────────────────────────────────────────────────
227
228/// Convenience type alias: `Result<T, A2aError>`.
229pub type A2aResult<T> = Result<T, A2aError>;
230
231// ── Tests ─────────────────────────────────────────────────────────────────────
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236
237    #[test]
238    fn error_code_roundtrip() {
239        let code = ErrorCode::TaskNotFound;
240        let n: i32 = code.into();
241        assert_eq!(n, -32001);
242        assert_eq!(ErrorCode::try_from(n), Ok(ErrorCode::TaskNotFound));
243    }
244
245    #[test]
246    fn error_code_unknown_value() {
247        assert!(ErrorCode::try_from(-99999).is_err());
248    }
249
250    #[test]
251    fn a2a_error_display() {
252        let err = A2aError::task_not_found("abc123");
253        let s = err.to_string();
254        assert!(s.contains("-32001"), "expected code in display: {s}");
255        assert!(s.contains("abc123"), "expected task id in display: {s}");
256    }
257
258    #[test]
259    fn a2a_error_serialization() {
260        let err = A2aError::internal("something went wrong");
261        let json = serde_json::to_string(&err).expect("serialize");
262        let back: A2aError = serde_json::from_str(&json).expect("deserialize");
263        assert_eq!(back.code, ErrorCode::InternalError);
264        assert_eq!(back.message, "something went wrong");
265        assert!(back.data.is_none());
266    }
267
268    #[test]
269    fn a2a_error_with_data() {
270        let data = serde_json::json!({"detail": "extra info"});
271        let err = A2aError::with_data(ErrorCode::InvalidParams, "bad input", data.clone());
272        let json = serde_json::to_string(&err).expect("serialize");
273        assert!(json.contains("\"data\""), "data field should be present");
274        let back: A2aError = serde_json::from_str(&json).expect("deserialize");
275        assert_eq!(back.data, Some(data));
276    }
277}