Skip to main content

agent_client_protocol/
error.rs

1//! The SDK's protocol-version-neutral JSON-RPC error.
2//!
3//! Every SDK callback and connection method uses [`Error`], whichever ACP
4//! version the connection speaks. It holds exactly what a JSON-RPC error
5//! object carries on the wire (a code, a message and optional data), so it
6//! converts losslessly to and from [`v1::schema::Error`] and
7//! `v2::schema::Error`.
8//!
9//! Constructors for codes that mean the same in every protocol version live
10//! here. Constructors whose error data differs between versions stay on the
11//! schema types; convert their result with `.into()`:
12//!
13//! ```
14//! use agent_client_protocol::{Error, v1::schema};
15//!
16//! let missing: Error = schema::Error::resource_not_found(Some("file:///a.txt".into())).into();
17//! assert_eq!(missing.code, schema::ErrorCode::ResourceNotFound);
18//! ```
19//!
20//! [`v1::schema::Error`]: crate::v1::schema::Error
21
22use std::fmt;
23
24use serde::{Deserialize, Serialize};
25
26#[cfg(feature = "unstable_protocol_v2")]
27use crate::v2::schema as v2;
28use crate::{schema::IntoOption, v1::schema as v1};
29
30/// Result type whose error defaults to the SDK's [`Error`].
31pub type Result<T, E = Error> = std::result::Result<T, E>;
32
33/// A JSON-RPC error code.
34///
35/// This is the integer on the wire. It is not tied to one ACP version: it
36/// can hold any `i32`, converts to and from `i32`, and converts losslessly to
37/// and from [`v1::schema::ErrorCode`](crate::v1::schema::ErrorCode) and
38/// `v2::schema::ErrorCode`. It can also be compared with either schema enum
39/// directly:
40///
41/// ```
42/// use agent_client_protocol::{Error, ErrorCode, v1::schema};
43///
44/// let error = Error::invalid_params();
45/// assert_eq!(error.code, ErrorCode::INVALID_PARAMS);
46/// assert_eq!(error.code, schema::ErrorCode::InvalidParams);
47/// assert_eq!(i32::from(error.code), -32602);
48/// ```
49///
50/// The associated constants cover the codes that every ACP version defines
51/// the same way. Codes defined by only some versions, such as v2's `-32022`
52/// (Unsupported protocol version), are on that version's `ErrorCode`.
53#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
54#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
55#[serde(transparent)]
56#[repr(transparent)]
57pub struct ErrorCode(i32);
58
59impl ErrorCode {
60    /// `-32700`: invalid JSON was received.
61    pub const PARSE_ERROR: Self = Self(-32700);
62    /// `-32600`: the JSON sent is not a valid request object.
63    pub const INVALID_REQUEST: Self = Self(-32600);
64    /// `-32601`: the method does not exist or is not available.
65    pub const METHOD_NOT_FOUND: Self = Self(-32601);
66    /// `-32602`: invalid method parameters.
67    pub const INVALID_PARAMS: Self = Self(-32602);
68    /// `-32603`: internal JSON-RPC error.
69    pub const INTERNAL_ERROR: Self = Self(-32603);
70    /// `-32800`: the request was cancelled.
71    pub const REQUEST_CANCELLED: Self = Self(-32800);
72    /// `-32000`: authentication is required first.
73    pub const AUTH_REQUIRED: Self = Self(-32000);
74    /// `-32002`: a resource, such as a session or file, was not found.
75    ///
76    /// The error data for this code differs between protocol versions; build
77    /// it with the schema types' constructors.
78    pub const RESOURCE_NOT_FOUND: Self = Self(-32002);
79
80    /// Wraps a raw JSON-RPC error code.
81    #[must_use]
82    pub const fn new(code: i32) -> Self {
83        Self(code)
84    }
85
86    /// The raw JSON-RPC error code.
87    #[must_use]
88    pub const fn value(self) -> i32 {
89        self.0
90    }
91
92    /// The default message for a code shared by every protocol version.
93    fn name(self) -> Option<&'static str> {
94        Some(match self {
95            Self::PARSE_ERROR => "Parse error",
96            Self::INVALID_REQUEST => "Invalid request",
97            Self::METHOD_NOT_FOUND => "Method not found",
98            Self::INVALID_PARAMS => "Invalid params",
99            Self::INTERNAL_ERROR => "Internal error",
100            Self::REQUEST_CANCELLED => "Request cancelled",
101            Self::AUTH_REQUIRED => "Authentication required",
102            Self::RESOURCE_NOT_FOUND => "Resource not found",
103            _ => return None,
104        })
105    }
106}
107
108impl fmt::Display for ErrorCode {
109    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110        f.write_str(self.name().unwrap_or("Unknown error"))
111    }
112}
113
114impl fmt::Debug for ErrorCode {
115    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116        write!(f, "{}: {self}", self.0)
117    }
118}
119
120impl From<i32> for ErrorCode {
121    fn from(code: i32) -> Self {
122        Self(code)
123    }
124}
125
126impl From<ErrorCode> for i32 {
127    fn from(code: ErrorCode) -> Self {
128        code.0
129    }
130}
131
132macro_rules! schema_error_code_conversions {
133    ($version:ident) => {
134        impl From<$version::ErrorCode> for ErrorCode {
135            fn from(code: $version::ErrorCode) -> Self {
136                Self(code.into())
137            }
138        }
139
140        impl From<ErrorCode> for $version::ErrorCode {
141            fn from(code: ErrorCode) -> Self {
142                code.0.into()
143            }
144        }
145
146        impl PartialEq<$version::ErrorCode> for ErrorCode {
147            fn eq(&self, other: &$version::ErrorCode) -> bool {
148                self.0 == i32::from(*other)
149            }
150        }
151
152        impl PartialEq<ErrorCode> for $version::ErrorCode {
153            fn eq(&self, other: &ErrorCode) -> bool {
154                i32::from(*self) == other.0
155            }
156        }
157
158        impl From<$version::Error> for Error {
159            fn from(error: $version::Error) -> Self {
160                Self {
161                    code: error.code.into(),
162                    message: error.message,
163                    data: error.data,
164                    origin: ErrorOrigin::Unmarked,
165                }
166            }
167        }
168
169        impl From<Error> for $version::Error {
170            fn from(error: Error) -> Self {
171                $version::Error::new(error.code.0, error.message).data(error.data)
172            }
173        }
174    };
175}
176
177schema_error_code_conversions!(v1);
178#[cfg(feature = "unstable_protocol_v2")]
179schema_error_code_conversions!(v2);
180
181/// A JSON-RPC error object, independent of the ACP protocol version.
182///
183/// This is the error type of every SDK callback and connection method. An
184/// error returned from a request handler is sent to the peer as an Error
185/// Response with exactly this code, message and data.
186///
187/// Build errors with the shared constructors below, or with a schema
188/// version's constructors followed by `.into()` when the error data differs
189/// between versions:
190///
191/// | Code | Constructor |
192/// |------|-------------|
193/// | `-32700` | [`Error::parse_error`] |
194/// | `-32600` | [`Error::invalid_request`] |
195/// | `-32601` | [`Error::method_not_found`] |
196/// | `-32602` | [`Error::invalid_params`] |
197/// | `-32603` | [`Error::internal_error`], [`Error::into_internal_error`] |
198/// | `-32800` | [`Error::request_cancelled`] |
199/// | `-32000` | [`Error::auth_required`] |
200/// | `-32002` | `v1::schema::Error::resource_not_found(uri)`, `v1::schema::Error::session_not_found(id)`, `v2::schema::Error::resource_not_found()`, `v2::schema::Error::session_not_found(id)` |
201/// | `-32022` | `v2::schema::Error::unsupported_protocol_version(requested, supported)` |
202///
203/// Convert back with `v1::schema::Error::from(error)` or
204/// `v2::schema::Error::from(error)` to use a schema version's view of the
205/// error. Typed v2 error data is read with `serde_json`:
206///
207/// ```
208/// # #[cfg(feature = "unstable_protocol_v2")] {
209/// use agent_client_protocol::{Error, v2::schema};
210///
211/// let error: Error = schema::Error::session_not_found("sess_1").into();
212/// if error.code == schema::ErrorCode::ResourceNotFound
213///     && let Some(data) = error.data.clone()
214///     && let Ok(data) = serde_json::from_value::<schema::SessionNotFoundErrorData>(data)
215/// {
216///     assert_eq!(data.session_id.0.as_ref(), "sess_1");
217/// }
218/// # }
219/// ```
220#[derive(Clone, Eq, Serialize, Deserialize)]
221#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
222#[non_exhaustive]
223pub struct Error {
224    /// The JSON-RPC error code.
225    pub code: ErrorCode,
226    /// A short description of the error.
227    pub message: String,
228    /// Optional additional information about the error.
229    ///
230    /// `Some(Value::Null)` is sent as an explicit `"data": null`; `None`
231    /// omits the field.
232    #[serde(default, skip_serializing_if = "Option::is_none")]
233    pub data: Option<serde_json::Value>,
234    /// Marks errors the SDK produced locally. It is never serialized, so an
235    /// error received from a peer can't carry it.
236    #[serde(skip)]
237    pub(crate) origin: ErrorOrigin,
238}
239
240/// Which locally produced condition an [`Error`] reports, if any.
241///
242/// [`is_connection_terminated`](crate::is_connection_terminated) and
243/// [`is_incoming_transport_closed`](crate::is_incoming_transport_closed) check
244/// this marker instead of the error's `data`, which a peer controls.
245#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
246pub(crate) enum ErrorOrigin {
247    /// Any error not marked below, including every error received from a peer.
248    #[default]
249    Unmarked,
250    /// The incoming transport reached clean EOF before a response arrived.
251    IncomingTransportClosed,
252    /// The connection terminated before a response arrived.
253    ConnectionTerminated,
254}
255
256// The origin marker is not part of an error's value: it is not serialized,
257// compared or printed, so errors behave as the JSON-RPC objects they hold.
258impl PartialEq for Error {
259    fn eq(&self, other: &Self) -> bool {
260        self.code == other.code && self.message == other.message && self.data == other.data
261    }
262}
263
264#[expect(
265    clippy::missing_fields_in_debug,
266    reason = "the origin marker is not part of the error's value"
267)]
268impl fmt::Debug for Error {
269    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
270        f.debug_struct("Error")
271            .field("code", &self.code)
272            .field("message", &self.message)
273            .field("data", &self.data)
274            .finish()
275    }
276}
277
278impl Error {
279    /// Creates an error with the given code and message and no data.
280    #[must_use]
281    pub fn new(code: impl Into<ErrorCode>, message: impl Into<String>) -> Self {
282        Self {
283            code: code.into(),
284            message: message.into(),
285            data: None,
286            origin: ErrorOrigin::Unmarked,
287        }
288    }
289
290    /// Sets the error's data, replacing any existing data.
291    #[must_use]
292    pub fn data(mut self, data: impl IntoOption<serde_json::Value>) -> Self {
293        self.data = data.into_option();
294        self
295    }
296
297    /// `-32700`: invalid JSON was received.
298    #[must_use]
299    pub fn parse_error() -> Self {
300        ErrorCode::PARSE_ERROR.into()
301    }
302
303    /// `-32600`: the JSON sent is not a valid request object.
304    #[must_use]
305    pub fn invalid_request() -> Self {
306        ErrorCode::INVALID_REQUEST.into()
307    }
308
309    /// `-32601`: the method does not exist or is not available.
310    #[must_use]
311    pub fn method_not_found() -> Self {
312        ErrorCode::METHOD_NOT_FOUND.into()
313    }
314
315    /// `-32602`: invalid method parameters.
316    #[must_use]
317    pub fn invalid_params() -> Self {
318        ErrorCode::INVALID_PARAMS.into()
319    }
320
321    /// `-32603`: internal JSON-RPC error.
322    #[must_use]
323    pub fn internal_error() -> Self {
324        ErrorCode::INTERNAL_ERROR.into()
325    }
326
327    /// `-32800`: the request was cancelled, either by the caller or because
328    /// of resource constraints or shutdown.
329    #[must_use]
330    pub fn request_cancelled() -> Self {
331        ErrorCode::REQUEST_CANCELLED.into()
332    }
333
334    /// `-32000`: authentication is required before this operation.
335    #[must_use]
336    pub fn auth_required() -> Self {
337        ErrorCode::AUTH_REQUIRED.into()
338    }
339
340    /// Converts any error into an internal error (`-32603`) whose data is the
341    /// error's string representation.
342    #[must_use]
343    pub fn into_internal_error(err: impl std::error::Error) -> Self {
344        Self::internal_error().data(err.to_string())
345    }
346}
347
348impl From<ErrorCode> for Error {
349    /// An error with the code's default message and no data.
350    fn from(code: ErrorCode) -> Self {
351        Self::new(code, code.to_string())
352    }
353}
354
355impl std::error::Error for Error {}
356
357impl fmt::Display for Error {
358    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
359        if self.message.is_empty() {
360            write!(f, "{}", self.code.0)?;
361        } else {
362            write!(f, "{}", self.message)?;
363        }
364
365        if let Some(data) = &self.data {
366            let pretty = serde_json::to_string_pretty(data).unwrap_or_else(|_| data.to_string());
367            write!(f, ": {pretty}")?;
368        }
369
370        Ok(())
371    }
372}
373
374impl From<anyhow::Error> for Error {
375    /// Recovers an SDK or schema error wrapped in the `anyhow::Error`, and
376    /// converts anything else into an internal error.
377    fn from(error: anyhow::Error) -> Self {
378        let error = match error.downcast::<Self>() {
379            Ok(error) => return error,
380            Err(error) => error,
381        };
382        let error = match error.downcast::<v1::Error>() {
383            Ok(error) => return error.into(),
384            Err(error) => error,
385        };
386        #[cfg(feature = "unstable_protocol_v2")]
387        let error = match error.downcast::<v2::Error>() {
388            Ok(error) => return error.into(),
389            Err(error) => error,
390        };
391        Self::into_internal_error(&*error)
392    }
393}
394
395impl From<serde_json::Error> for Error {
396    /// An invalid params error (`-32602`) whose data is the serde message.
397    fn from(error: serde_json::Error) -> Self {
398        Self::invalid_params().data(error.to_string())
399    }
400}
401
402#[cfg(test)]
403mod tests {
404    use serde_json::{Value, json};
405
406    use super::*;
407
408    fn sample_codes() -> [i32; 13] {
409        [
410            -32700,
411            -32600,
412            -32601,
413            -32602,
414            -32603,
415            -32800,
416            -32000,
417            -32002,
418            -32022,
419            0,
420            12345,
421            i32::MIN,
422            i32::MAX,
423        ]
424    }
425
426    fn sample_data() -> [Option<Value>; 6] {
427        [
428            None,
429            Some(Value::Null),
430            Some(json!("text")),
431            Some(json!(42)),
432            Some(json!([1, null, { "a": [] }])),
433            Some(json!({ "sessionId": "s", "_meta": { "x": true }, "extra": null })),
434        ]
435    }
436
437    fn samples() -> impl Iterator<Item = Error> {
438        sample_codes().into_iter().flat_map(|code| {
439            sample_data().into_iter().flat_map(move |data| {
440                ["", "message", "Unknown error"].map(|message| Error {
441                    code: code.into(),
442                    message: message.into(),
443                    data: data.clone(),
444                    origin: ErrorOrigin::Unmarked,
445                })
446            })
447        })
448    }
449
450    #[test]
451    fn v1_round_trip_is_lossless() {
452        for error in samples() {
453            let schema = v1::Error::from(error.clone());
454            assert_eq!(i32::from(schema.code), error.code.value());
455            assert_eq!(schema.message, error.message);
456            assert_eq!(schema.data, error.data);
457            assert_eq!(Error::from(schema.clone()), error);
458            assert_eq!(v1::Error::from(Error::from(schema.clone())), schema);
459            assert_eq!(error.to_string(), schema.to_string());
460        }
461    }
462
463    #[cfg(feature = "unstable_protocol_v2")]
464    #[test]
465    fn v2_round_trip_is_lossless() {
466        for error in samples() {
467            let schema = v2::Error::from(error.clone());
468            assert_eq!(i32::from(schema.code), error.code.value());
469            assert_eq!(schema.message, error.message);
470            assert_eq!(schema.data, error.data);
471            assert_eq!(Error::from(schema.clone()), error);
472            assert_eq!(v2::Error::from(Error::from(schema.clone())), schema);
473            assert_eq!(error.to_string(), schema.to_string());
474        }
475    }
476
477    #[cfg(feature = "unstable_protocol_v2")]
478    #[test]
479    fn v1_and_v2_convert_through_the_sdk_error() {
480        for error in samples() {
481            let v1 = v1::Error::from(error.clone());
482            let v2 = v2::Error::from(Error::from(v1));
483            assert_eq!(Error::from(v2), error);
484        }
485    }
486
487    #[test]
488    fn serializes_like_the_schema_errors() {
489        for error in samples() {
490            let json = serde_json::to_value(&error).unwrap();
491            assert_eq!(
492                json,
493                serde_json::to_value(v1::Error::from(error.clone())).unwrap()
494            );
495            #[cfg(feature = "unstable_protocol_v2")]
496            assert_eq!(
497                json,
498                serde_json::to_value(v2::Error::from(error.clone())).unwrap()
499            );
500            let parsed: Error = serde_json::from_value(json.clone()).unwrap();
501            let schema: v1::Error = serde_json::from_value(json).unwrap();
502            assert_eq!(parsed, Error::from(schema));
503        }
504    }
505
506    #[test]
507    fn shared_constructors_match_both_schema_versions() {
508        let pairs = [
509            (Error::parse_error(), v1::Error::parse_error()),
510            (Error::invalid_request(), v1::Error::invalid_request()),
511            (Error::method_not_found(), v1::Error::method_not_found()),
512            (Error::invalid_params(), v1::Error::invalid_params()),
513            (Error::internal_error(), v1::Error::internal_error()),
514            (Error::request_cancelled(), v1::Error::request_cancelled()),
515            (Error::auth_required(), v1::Error::auth_required()),
516            (
517                Error::into_internal_error(std::io::Error::other("io")),
518                v1::Error::into_internal_error(std::io::Error::other("io")),
519            ),
520        ];
521        for (sdk, schema) in pairs {
522            assert_eq!(sdk, Error::from(schema));
523        }
524        #[cfg(feature = "unstable_protocol_v2")]
525        {
526            let pairs = [
527                (Error::parse_error(), v2::Error::parse_error()),
528                (Error::invalid_request(), v2::Error::invalid_request()),
529                (Error::method_not_found(), v2::Error::method_not_found()),
530                (Error::invalid_params(), v2::Error::invalid_params()),
531                (Error::internal_error(), v2::Error::internal_error()),
532                (Error::request_cancelled(), v2::Error::request_cancelled()),
533                (Error::auth_required(), v2::Error::auth_required()),
534            ];
535            for (sdk, schema) in pairs {
536                assert_eq!(sdk, Error::from(schema));
537            }
538        }
539    }
540
541    #[test]
542    fn error_codes_compare_with_schema_codes() {
543        for code in sample_codes() {
544            let sdk = ErrorCode::from(code);
545            let v1 = v1::ErrorCode::from(code);
546            assert_eq!(sdk, v1);
547            assert_eq!(v1, sdk);
548            assert_eq!(ErrorCode::from(v1), sdk);
549            assert_eq!(v1::ErrorCode::from(sdk), v1);
550            assert_eq!(format!("{sdk:?}"), format!("{v1:?}"));
551            assert_eq!(sdk.to_string(), v1.to_string());
552            #[cfg(feature = "unstable_protocol_v2")]
553            {
554                let v2 = v2::ErrorCode::from(code);
555                assert_eq!(sdk, v2);
556                assert_eq!(ErrorCode::from(v2), sdk);
557                assert_eq!(v2::ErrorCode::from(sdk), v2);
558            }
559        }
560        assert_ne!(ErrorCode::INVALID_PARAMS, v1::ErrorCode::InvalidRequest);
561    }
562
563    #[cfg(feature = "unstable_protocol_v2")]
564    #[test]
565    fn typed_v2_data_survives_conversion() {
566        let error: Error = v2::Error::unsupported_protocol_version(
567            crate::schema::ProtocolVersion::V2,
568            &[crate::schema::ProtocolVersion::V1],
569        )
570        .into();
571        assert_eq!(error.code, v2::ErrorCode::UnsupportedProtocolVersion);
572        let data: v2::UnsupportedProtocolVersionErrorData =
573            serde_json::from_value(v2::Error::from(error).data.unwrap()).unwrap();
574        assert_eq!(
575            data,
576            v2::UnsupportedProtocolVersionErrorData::new(
577                crate::schema::ProtocolVersion::V2,
578                vec![crate::schema::ProtocolVersion::V1],
579            )
580        );
581    }
582
583    #[test]
584    fn anyhow_recovers_wrapped_errors() {
585        let error = Error::new(12345, "custom").data(json!({ "kept": true }));
586        assert_eq!(Error::from(anyhow::Error::new(error.clone())), error);
587        let schema = v1::Error::new(-32002, "missing").data(Value::Null);
588        assert_eq!(
589            Error::from(anyhow::Error::new(schema.clone())),
590            Error::from(schema)
591        );
592        #[cfg(feature = "unstable_protocol_v2")]
593        {
594            let schema = v2::Error::session_not_found("sess_1");
595            assert_eq!(
596                Error::from(anyhow::Error::new(schema.clone())),
597                Error::from(schema)
598            );
599        }
600        assert_eq!(
601            Error::from(anyhow::anyhow!("boom")),
602            Error::internal_error().data("boom")
603        );
604    }
605}