Skip to main content

agent_client_protocol/concepts/
error_handling.rs

1//! Error handling patterns in agent-client-protocol.
2//!
3//! This chapter explains how errors work in agent-client-protocol callbacks and the difference
4//! between *protocol errors* (sent to the peer) and *connection errors* (which
5//! shut down the connection).
6//!
7//! # Callback Return Types
8//!
9//! Almost all agent-client-protocol callbacks return `Result<_, crate::Error>`.
10//! What happens when you return an `Err` depends on the context. An incoming
11//! request-handler error is sent to the peer as an Error Response, while an
12//! incoming notification-handler error is logged without a reply. Errors from
13//! connection-lifecycle callbacks can shut down the connection.
14//!
15//! # Sending Protocol Errors
16//!
17//! To choose an error response explicitly while handling a request, use the
18//! request context's `respond` method:
19//!
20//! ```
21//! # use agent_client_protocol::{Client, Agent, ConnectTo};
22//! # use agent_client_protocol_test::{ValidateRequest, ValidateResponse};
23//! # async fn example(transport: impl ConnectTo<Client>) -> Result<(), agent_client_protocol::Error> {
24//! Client.v1()
25//!     .on_receive_request(async |request: ValidateRequest, responder, _cx| {
26//!         if request.data.is_empty() {
27//!             // Send error to peer, keep connection alive
28//!             responder.respond_with_error(agent_client_protocol::Error::invalid_params())?;
29//!             return Ok(());
30//!         }
31//!
32//!         // Process valid request...
33//!         responder.respond(ValidateResponse { is_valid: true, error: None })?;
34//!         Ok(())
35//!     }, agent_client_protocol::on_receive_request!())
36//! #   .connect_with(transport, async |_| Ok(())).await?;
37//! # Ok(())
38//! # }
39//! ```
40//!
41//! JSON-RPC notifications are one-way and cannot receive success or error replies.
42//! The SDK logs notification parse and handler errors without answering them. If an
43//! application needs to report a one-way failure, define a notification method for
44//! that purpose.
45//!
46//! Malformed JSON and invalid request envelopes are handled at the SDK's transport
47//! boundary. Applications do not need to construct uncorrelated Error Responses.
48//!
49//! # Dropped Responders
50//!
51//! Every request gets a response. If a [`Responder`][crate::Responder] is
52//! dropped without responding (and was not moved somewhere that responds,
53//! such as [`SentRequest::forward_response_to`][crate::SentRequest::forward_response_to]),
54//! the SDK answers the request with `-32603` (Internal error), with data naming
55//! the method, or with `-32800` (Request cancelled) if the peer had cancelled
56//! the request with `$/cancel_request`. If the handler that dropped the
57//! responder returns an error, that error is sent instead. Nothing is sent if
58//! a response was already sent or the connection is shutting down.
59//!
60//! The automatic error is an ordinary error response: a dropped `initialize`
61//! responder fails initialization, so the client can retry.
62//!
63//! # The `into_internal_error` Helper
64//!
65//! When working with external libraries that return their own error types,
66//! you need to convert them to `agent_client_protocol::Error`. The
67//! [`Error::into_internal_error`][crate::Error::into_internal_error] method
68//! provides a convenient way to do this:
69//!
70//! ```
71//! use agent_client_protocol::Error;
72//!
73//! # fn example() -> Result<(), agent_client_protocol::Error> {
74//! # let data = "hello";
75//! # let path = "/tmp/test.txt";
76//! // Convert any error type to agent_client_protocol::Error
77//! let value = serde_json::to_value(&data)
78//!     .map_err(Error::into_internal_error)?;
79//!
80//! // Or with a file operation
81//! let contents = std::fs::read_to_string(path)
82//!     .map_err(Error::into_internal_error);
83//! # Ok(())
84//! # }
85//! ```
86//!
87//! This wraps the original error's message in an internal error, which is
88//! appropriate for unexpected failures. For expected error conditions that
89//! should be communicated to the peer, create specific error types instead.
90//!
91//! # Error Types
92//!
93//! [`Error`][crate::Error] is the same type on v1 and v2 connections. It holds
94//! exactly what a JSON-RPC error object carries: a [`code`][crate::ErrorCode],
95//! a `message`, and optional `data`.
96//!
97//! It provides factory methods for the codes that mean the same in every
98//! protocol version:
99//!
100//! - [`Error::parse_error()`][crate::Error::parse_error] - `-32700`, invalid JSON
101//! - [`Error::invalid_request()`][crate::Error::invalid_request] - `-32600`, malformed request
102//! - [`Error::method_not_found()`][crate::Error::method_not_found] - `-32601`, unknown method
103//! - [`Error::invalid_params()`][crate::Error::invalid_params] - `-32602`, bad parameters
104//! - [`Error::internal_error()`][crate::Error::internal_error] - `-32603`, server error
105//! - [`Error::request_cancelled()`][crate::Error::request_cancelled] -
106//!   `-32800`, the request was cancelled
107//! - [`Error::auth_required()`][crate::Error::auth_required] - `-32000`, the
108//!   request needs authentication first; a failed or abandoned v2
109//!   `auth/login` also uses `-32000`
110//!
111//! Errors whose data differs between protocol versions are built with the
112//! schema version's constructors and converted with `.into()`:
113//!
114//! - `-32002` for a missing resource:
115//!   [`v1::schema::Error::resource_not_found(uri)`][crate::v1::schema::Error::resource_not_found]
116//!   takes an optional `uri`, and `v2::schema::Error::resource_not_found()`
117//!   takes none
118//! - `-32002` with `data.sessionId`, for a session-scoped request naming a
119//!   session that does not exist or is not active on the connection:
120//!   [`v1::schema::Error::session_not_found(id)`][crate::v1::schema::Error::session_not_found]
121//!   or `v2::schema::Error::session_not_found(id)`
122//! - `-32022` for an agent that supports no protocol version at or below the
123//!   requested one:
124//!   `v2::schema::Error::unsupported_protocol_version(requested, supported)`
125//!   (protocol v2 only)
126//!
127//! ```
128//! use agent_client_protocol::{Error, v1::schema};
129//!
130//! # fn lookup(session_id: schema::SessionId) -> Result<(), Error> {
131//! return Err(schema::Error::session_not_found(session_id).into());
132//! # }
133//! ```
134//!
135//! Compare codes with [`ErrorCode`][crate::ErrorCode]'s constants, or with a
136//! schema version's `ErrorCode` enum. `v1::schema::Error::from(error)` and
137//! `v2::schema::Error::from(error)` convert back without losing anything, which is
138//! how to read typed v2 error data such as `SessionNotFoundErrorData`.
139//!
140//! Codes from `-32768` to `-32000` and from `-32899` to `-32800` are reserved
141//! for JSON-RPC and ACP. Implementation-specific errors should use codes
142//! outside both ranges, and receivers treat an unrecognized code as a generic
143//! failure.
144//!
145//! See the protocol's error pages for when each code applies:
146//! [v1](https://agentclientprotocol.com/protocol/v1/error) and
147//! [v2](https://agentclientprotocol.com/protocol/v2/error).
148//!
149//! You can add context with `.data()`:
150//!
151//! ```
152//! let error = agent_client_protocol::Error::invalid_params()
153//!     .data(serde_json::json!({
154//!         "field": "timeout",
155//!         "reason": "must be positive"
156//!     }));
157//! ```
158//!
159//! # Summary
160//!
161//! | Situation | What to do |
162//! |-----------|------------|
163//! | Send error response to request | `responder.respond_with_error(error)` |
164//! | Handle notification failure | Log or return the error; the SDK sends no reply |
165//! | Fail connection lifecycle work | Return `Err(error)` from a lifecycle callback |
166//! | Convert external error | `.map_err(Error::into_internal_error)?` |