Skip to main content

agent_client_protocol_schema/v2/
error.rs

1//! Error handling for the Agent Client Protocol.
2//!
3//! This module provides error types and codes following the JSON-RPC 2.0 specification,
4//! with additional protocol-specific error codes for authentication and other ACP-specific scenarios.
5//!
6//! All methods in the protocol follow standard JSON-RPC 2.0 error handling:
7//! - Successful responses include a `result` field
8//! - Errors include an `error` object with `code` and `message`
9//! - Notifications never receive responses (success or error)
10//!
11//! See: [Error Handling](https://agentclientprotocol.com/protocol/v2/overview#error-handling)
12
13use std::{fmt::Display, str};
14
15#[cfg(feature = "schemars")]
16use schemars::Schema;
17use serde::{Deserialize, Serialize};
18use serde_with::{DefaultOnError, serde_as, skip_serializing_none};
19
20use super::{Meta, SessionId};
21use crate::{IntoOption, ProtocolVersion};
22
23/// Convenience result type using this protocol version's error type.
24pub type Result<T, E = Error> = std::result::Result<T, E>;
25
26/// JSON-RPC error object.
27///
28/// Represents an error that occurred during method execution, following the
29/// JSON-RPC 2.0 error object specification with optional additional data.
30///
31/// See protocol docs: [JSON-RPC Error Object](https://www.jsonrpc.org/specification#error_object)
32#[serde_as]
33#[skip_serializing_none]
34#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
35#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
36#[non_exhaustive]
37pub struct Error {
38    /// A number indicating the error type that occurred.
39    /// This must be an integer as defined in the JSON-RPC specification.
40    pub code: ErrorCode,
41    /// A string providing a short description of the error.
42    /// The message should be limited to a concise single sentence.
43    pub message: String,
44    /// Optional primitive or structured value that contains additional information about the error.
45    /// This may include debugging information or context-specific details.
46    ///
47    /// Omitted and `null` are equivalent and mean no additional data is provided. Some codes
48    /// define a shape: [`UnsupportedProtocolVersionErrorData`] for `-32022`, and
49    /// [`SessionNotFoundErrorData`] for `-32002` when a session is not found.
50    #[serde_as(deserialize_as = "DefaultOnError")]
51    #[cfg_attr(feature = "schemars", schemars(with = "Option<ErrorData>", extend("x-deserialize-default-on-error" = true)))]
52    #[serde(default)]
53    pub data: Option<serde_json::Value>,
54}
55
56/// Schema for [`Error::data`]: the shapes ACP defines, or any other value.
57#[cfg(feature = "schemars")]
58#[derive(schemars::JsonSchema)]
59#[serde(untagged)]
60#[expect(
61    dead_code,
62    reason = "Only used to describe the schema of `Error::data`"
63)]
64enum ErrorData {
65    UnsupportedProtocolVersion(UnsupportedProtocolVersionErrorData),
66    SessionNotFound(SessionNotFoundErrorData),
67    Other(serde_json::Value),
68}
69
70/// Data for the `-32022` (Unsupported protocol version) error.
71///
72/// See protocol docs: [Version Negotiation](https://agentclientprotocol.com/protocol/v2/initialization#version-negotiation)
73#[serde_as]
74#[skip_serializing_none]
75#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
76#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
77#[serde(rename_all = "camelCase")]
78#[non_exhaustive]
79pub struct UnsupportedProtocolVersionErrorData {
80    /// The protocol version the Client requested.
81    pub requested: ProtocolVersion,
82    /// The protocol versions the Agent supports.
83    pub supported: Vec<ProtocolVersion>,
84    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
85    /// metadata to their interactions. Omitted and `null` are equivalent and mean no metadata.
86    /// Implementations MUST NOT assign meaning to keys they do not recognize.
87    ///
88    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
89    #[serde_as(deserialize_as = "DefaultOnError")]
90    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
91    #[serde(default, rename = "_meta")]
92    pub meta: Option<Meta>,
93}
94
95impl UnsupportedProtocolVersionErrorData {
96    /// Builds the data with the requested and supported versions.
97    #[must_use]
98    pub fn new(requested: ProtocolVersion, supported: Vec<ProtocolVersion>) -> Self {
99        Self {
100            requested,
101            supported,
102            meta: None,
103        }
104    }
105
106    /// Sets optional metadata.
107    #[must_use]
108    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
109        self.meta = meta.into_option();
110        self
111    }
112}
113
114/// Data for the `-32002` (Resource not found) error when a session-scoped request names a
115/// session that does not exist or is not active.
116///
117/// See protocol docs: [Unknown Sessions](https://agentclientprotocol.com/protocol/v2/error#unknown-sessions)
118#[serde_as]
119#[skip_serializing_none]
120#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
121#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
122#[serde(rename_all = "camelCase")]
123#[non_exhaustive]
124pub struct SessionNotFoundErrorData {
125    /// The session ID from the request.
126    pub session_id: SessionId,
127    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
128    /// metadata to their interactions. Omitted and `null` are equivalent and mean no metadata.
129    /// Implementations MUST NOT assign meaning to keys they do not recognize.
130    ///
131    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
132    #[serde_as(deserialize_as = "DefaultOnError")]
133    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
134    #[serde(default, rename = "_meta")]
135    pub meta: Option<Meta>,
136}
137
138impl SessionNotFoundErrorData {
139    /// Builds the data for the given session ID.
140    #[must_use]
141    pub fn new(session_id: impl Into<SessionId>) -> Self {
142        Self {
143            session_id: session_id.into(),
144            meta: None,
145        }
146    }
147
148    /// Sets optional metadata.
149    #[must_use]
150    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
151        self.meta = meta.into_option();
152        self
153    }
154}
155
156impl Error {
157    /// Creates a new error with the given code and message.
158    ///
159    /// The code parameter can be an `ErrorCode` constant or a tuple of (code, message).
160    #[must_use]
161    pub fn new(code: i32, message: impl Into<String>) -> Self {
162        Error {
163            code: code.into(),
164            message: message.into(),
165            data: None,
166        }
167    }
168
169    /// Adds additional data to the error.
170    ///
171    /// This method is chainable and allows attaching context-specific information
172    /// to help with debugging or provide more details about the error.
173    #[must_use]
174    pub fn data(mut self, data: impl IntoOption<serde_json::Value>) -> Self {
175        self.data = data.into_option();
176        self
177    }
178
179    /// Invalid JSON was received by the server. An error occurred on the server while parsing the JSON text.
180    #[must_use]
181    pub fn parse_error() -> Self {
182        ErrorCode::ParseError.into()
183    }
184
185    /// The JSON sent is not a valid Request object.
186    #[must_use]
187    pub fn invalid_request() -> Self {
188        ErrorCode::InvalidRequest.into()
189    }
190
191    /// The method does not exist / is not available.
192    #[must_use]
193    pub fn method_not_found() -> Self {
194        ErrorCode::MethodNotFound.into()
195    }
196
197    /// Invalid method parameter(s).
198    #[must_use]
199    pub fn invalid_params() -> Self {
200        ErrorCode::InvalidParams.into()
201    }
202
203    /// Internal JSON-RPC error.
204    #[must_use]
205    pub fn internal_error() -> Self {
206        ErrorCode::InternalError.into()
207    }
208
209    /// Request was cancelled.
210    ///
211    /// Execution of the method was aborted either due to a cancellation request from the caller
212    /// or because of resource constraints or shutdown.
213    #[must_use]
214    pub fn request_cancelled() -> Self {
215        ErrorCode::RequestCancelled.into()
216    }
217
218    /// Authentication required.
219    #[must_use]
220    pub fn auth_required() -> Self {
221        ErrorCode::AuthRequired.into()
222    }
223
224    /// A referenced resource was not found.
225    #[must_use]
226    pub fn resource_not_found() -> Self {
227        ErrorCode::ResourceNotFound.into()
228    }
229
230    /// The session named by a session-scoped request does not exist or is not active.
231    #[must_use]
232    pub fn session_not_found(session_id: impl Into<super::SessionId>) -> Self {
233        Error::from(ErrorCode::ResourceNotFound)
234            .data(serde_json::to_value(SessionNotFoundErrorData::new(session_id)).ok())
235    }
236
237    /// No supported protocol version is at or below the requested version.
238    #[must_use]
239    pub fn unsupported_protocol_version(
240        requested: ProtocolVersion,
241        supported: &[ProtocolVersion],
242    ) -> Self {
243        let error: Self = ErrorCode::UnsupportedProtocolVersion.into();
244        error.data(
245            serde_json::to_value(UnsupportedProtocolVersionErrorData::new(
246                requested,
247                supported.to_vec(),
248            ))
249            .ok(),
250        )
251    }
252
253    /// Converts a standard error into an internal JSON-RPC error.
254    ///
255    /// The error's string representation is included as additional data.
256    #[must_use]
257    pub fn into_internal_error(err: impl std::error::Error) -> Self {
258        Error::internal_error().data(err.to_string())
259    }
260}
261
262/// Predefined error codes for common JSON-RPC and ACP-specific errors.
263///
264/// Standard errors use the JSON-RPC 2.0 codes. ACP-specific errors use the JSON-RPC
265/// server-error range (-32000 to -32099), and request cancellation uses -32800 from the
266/// range LSP reserves (-32899 to -32800).
267#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
268#[derive(Clone, Copy, Deserialize, Eq, PartialEq, Serialize, strum::Display)]
269#[cfg_attr(test, derive(strum::EnumIter))]
270#[serde(from = "i32", into = "i32")]
271#[cfg_attr(feature = "schemars", schemars(!from, !into))]
272#[non_exhaustive]
273pub enum ErrorCode {
274    // JSON-RPC errors
275    /// Invalid JSON was received by the server.
276    /// An error occurred on the server while parsing the JSON text.
277    #[cfg_attr(feature = "schemars", schemars(transform = error_code_transform))]
278    #[strum(to_string = "Parse error")]
279    ParseError, // -32700
280    /// The JSON sent is not a valid Request object.
281    #[cfg_attr(feature = "schemars", schemars(transform = error_code_transform))]
282    #[strum(to_string = "Invalid request")]
283    InvalidRequest, // -32600
284    /// The method does not exist or is not available.
285    #[cfg_attr(feature = "schemars", schemars(transform = error_code_transform))]
286    #[strum(to_string = "Method not found")]
287    MethodNotFound, // -32601
288    /// Invalid method parameter(s).
289    #[cfg_attr(feature = "schemars", schemars(transform = error_code_transform))]
290    #[strum(to_string = "Invalid params")]
291    InvalidParams, // -32602
292    /// Internal JSON-RPC error.
293    #[cfg_attr(feature = "schemars", schemars(transform = error_code_transform))]
294    #[strum(to_string = "Internal error")]
295    InternalError, // -32603
296    // Request cancellation (from LSP)
297    /// Execution of the method was aborted either due to a cancellation request from the caller or
298    /// because of resource constraints or shutdown.
299    #[cfg_attr(feature = "schemars", schemars(transform = error_code_transform))]
300    #[strum(to_string = "Request cancelled")]
301    RequestCancelled, // -32800
302
303    // Custom errors
304    /// Authentication is required before this operation can be performed.
305    #[cfg_attr(feature = "schemars", schemars(transform = error_code_transform))]
306    #[strum(to_string = "Authentication required")]
307    AuthRequired, // -32000
308    /// A given resource, such as a session, was not found.
309    #[cfg_attr(feature = "schemars", schemars(transform = error_code_transform))]
310    #[strum(to_string = "Resource not found")]
311    ResourceNotFound, // -32002
312    /// No protocol version supported by the Agent is at or below the requested version.
313    /// The error data is an [`UnsupportedProtocolVersionErrorData`].
314    #[cfg_attr(feature = "schemars", schemars(transform = error_code_transform))]
315    #[strum(to_string = "Unsupported protocol version")]
316    UnsupportedProtocolVersion, // -32022
317    /// Other undefined error code.
318    #[cfg_attr(feature = "schemars", schemars(untagged))]
319    #[strum(to_string = "Unknown error")]
320    Other(i32),
321}
322
323impl From<i32> for ErrorCode {
324    fn from(value: i32) -> Self {
325        match value {
326            -32700 => ErrorCode::ParseError,
327            -32600 => ErrorCode::InvalidRequest,
328            -32601 => ErrorCode::MethodNotFound,
329            -32602 => ErrorCode::InvalidParams,
330            -32603 => ErrorCode::InternalError,
331            -32800 => ErrorCode::RequestCancelled,
332            -32000 => ErrorCode::AuthRequired,
333            -32002 => ErrorCode::ResourceNotFound,
334            -32022 => ErrorCode::UnsupportedProtocolVersion,
335            _ => ErrorCode::Other(value),
336        }
337    }
338}
339
340impl From<ErrorCode> for i32 {
341    fn from(value: ErrorCode) -> Self {
342        match value {
343            ErrorCode::ParseError => -32700,
344            ErrorCode::InvalidRequest => -32600,
345            ErrorCode::MethodNotFound => -32601,
346            ErrorCode::InvalidParams => -32602,
347            ErrorCode::InternalError => -32603,
348            ErrorCode::RequestCancelled => -32800,
349            ErrorCode::AuthRequired => -32000,
350            ErrorCode::ResourceNotFound => -32002,
351            ErrorCode::UnsupportedProtocolVersion => -32022,
352            ErrorCode::Other(value) => value,
353        }
354    }
355}
356
357impl std::fmt::Debug for ErrorCode {
358    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
359        write!(f, "{}: {self}", i32::from(*self))
360    }
361}
362
363#[cfg(feature = "schemars")]
364fn error_code_transform(schema: &mut Schema) {
365    let name = schema
366        .get("const")
367        .expect("Unexpected schema for ErrorCode")
368        .as_str()
369        .expect("unexpected type for schema");
370    let code = match name {
371        "ParseError" => ErrorCode::ParseError,
372        "InvalidRequest" => ErrorCode::InvalidRequest,
373        "MethodNotFound" => ErrorCode::MethodNotFound,
374        "InvalidParams" => ErrorCode::InvalidParams,
375        "InternalError" => ErrorCode::InternalError,
376        "RequestCancelled" => ErrorCode::RequestCancelled,
377        "AuthRequired" => ErrorCode::AuthRequired,
378        "ResourceNotFound" => ErrorCode::ResourceNotFound,
379        "UnsupportedProtocolVersion" => ErrorCode::UnsupportedProtocolVersion,
380        _ => panic!("Unexpected error code name {name}"),
381    };
382    let mut description = schema
383        .get("description")
384        .expect("Missing description")
385        .as_str()
386        .expect("Unexpected type for description")
387        .to_owned();
388    schema.insert("title".into(), code.to_string().into());
389    description.insert_str(0, &format!("**{code}**: "));
390    schema.insert("description".into(), description.into());
391    schema.insert("const".into(), i32::from(code).into());
392    schema.insert("type".into(), "integer".into());
393    schema.insert("format".into(), "int32".into());
394}
395
396impl From<ErrorCode> for Error {
397    fn from(error_code: ErrorCode) -> Self {
398        Error::new(error_code.into(), error_code.to_string())
399    }
400}
401
402impl std::error::Error for Error {}
403
404impl Display for Error {
405    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
406        if self.message.is_empty() {
407            write!(f, "{}", i32::from(self.code))?;
408        } else {
409            write!(f, "{}", self.message)?;
410        }
411
412        if let Some(data) = &self.data {
413            let pretty = serde_json::to_string_pretty(data).unwrap_or_else(|_| data.to_string());
414            write!(f, ": {pretty}")?;
415        }
416
417        Ok(())
418    }
419}
420
421impl From<anyhow::Error> for Error {
422    fn from(error: anyhow::Error) -> Self {
423        match error.downcast::<Self>() {
424            Ok(error) => error,
425            Err(error) => Error::into_internal_error(&*error),
426        }
427    }
428}
429
430impl From<serde_json::Error> for Error {
431    fn from(error: serde_json::Error) -> Self {
432        Error::invalid_params().data(error.to_string())
433    }
434}
435
436#[cfg(test)]
437mod tests {
438    use strum::IntoEnumIterator;
439
440    #[test]
441    fn helper_data_parses_as_typed_error_data() {
442        let error = Error::session_not_found("sess_1");
443        let data: SessionNotFoundErrorData = serde_json::from_value(error.data.unwrap()).unwrap();
444        assert_eq!(data, SessionNotFoundErrorData::new("sess_1"));
445
446        let error =
447            Error::unsupported_protocol_version(ProtocolVersion::V2, &[ProtocolVersion::V1]);
448        let data: UnsupportedProtocolVersionErrorData =
449            serde_json::from_value(error.data.unwrap()).unwrap();
450        assert_eq!(
451            data,
452            UnsupportedProtocolVersionErrorData::new(
453                ProtocolVersion::V2,
454                vec![ProtocolVersion::V1]
455            )
456        );
457    }
458
459    use super::*;
460
461    #[test]
462    fn unsupported_protocol_version_includes_requested_and_supported() {
463        let error = Error::unsupported_protocol_version(
464            ProtocolVersion::V1,
465            &[ProtocolVersion::from(2_u16)],
466        );
467        assert_eq!(
468            serde_json::to_value(error).unwrap(),
469            serde_json::json!({
470                "code": -32022,
471                "message": "Unsupported protocol version",
472                "data": { "requested": 1, "supported": [2] },
473            })
474        );
475        assert_eq!(
476            Error::unsupported_protocol_version(ProtocolVersion::V1, &[]).data,
477            Some(serde_json::json!({ "requested": 1, "supported": [] }))
478        );
479    }
480
481    #[test]
482    fn serialize_error_code() {
483        assert_eq!(
484            serde_json::from_value::<ErrorCode>(serde_json::json!(-32700)).unwrap(),
485            ErrorCode::ParseError
486        );
487        assert_eq!(
488            serde_json::to_value(ErrorCode::ParseError).unwrap(),
489            serde_json::json!(-32700)
490        );
491
492        assert_eq!(
493            serde_json::from_value::<ErrorCode>(serde_json::json!(1)).unwrap(),
494            ErrorCode::Other(1)
495        );
496        assert_eq!(
497            serde_json::to_value(ErrorCode::Other(1)).unwrap(),
498            serde_json::json!(1)
499        );
500    }
501
502    #[test]
503    fn serialize_error_code_equality() {
504        // Make sure schema generation doesn't panic when enabled.
505        #[cfg(feature = "schemars")]
506        let _schema = schemars::schema_for!(ErrorCode);
507        for error in ErrorCode::iter() {
508            assert_eq!(
509                error,
510                serde_json::from_value(serde_json::to_value(error).unwrap()).unwrap()
511            );
512        }
513    }
514}