Skip to main content

agent_client_protocol/concepts/
cancellation.rs

1//! Request cancellation with `$/cancel_request`.
2//!
3//! The SDK exposes the ACP `$/cancel_request` notification behind the
4//! `unstable_cancel_request` feature. The notification is protocol-level:
5//! either side may send it to ask the peer to cancel one outstanding JSON-RPC
6//! request by ID.
7//!
8//! Cancellation is **cooperative**. A peer may ignore `$/cancel_request`, may
9//! finish with normal data, or may respond to the original request with
10//! [`Error::request_cancelled`] (`-32800`). The requesting side always
11//! receives a response to the original request; cancellation only changes
12//! *which* response that is. Unhandled notifications are ignored by the SDK
13//! (even without this feature), so peers that do not support cancellation
14//! simply will not act on it.
15//!
16//! # Cancelling outgoing requests
17//!
18//! To cancel a request sent through [`ConnectionTo::send_request`], keep the
19//! returned [`SentRequest`] and call [`cancel`][`SentRequest::cancel`] on it:
20//!
21//! ```
22//! # use agent_client_protocol::{ConnectionTo, Error, UntypedRole};
23//! # use agent_client_protocol_test::MyRequest;
24//! # async fn example(cx: ConnectionTo<UntypedRole>) -> Result<(), Error> {
25//! let request = cx.send_request(MyRequest {});
26//! request.cancel()?;
27//!
28//! // The peer still responds to the request: with normal data if it raced
29//! // ahead, or with the standard cancellation error.
30//! let result = request.block_task().await;
31//! # let _ = result;
32//! # Ok(())
33//! # }
34//! ```
35//!
36//! The [`SentRequest`] remembers the peer and any proxy wrapping used for the
37//! original request, so this also works for requests sent through
38//! [`ConnectionTo::send_request_to`].
39//!
40//! Dropping a [`SentRequest`] before the SDK receives a response also sends
41//! `$/cancel_request`. This covers abandoned request handles and futures. For a
42//! request whose eventual response should be ignored, but which should continue
43//! running on the peer, call [`detach`][`SentRequest::detach`] instead; the
44//! eventual response is discarded, but no cancellation is sent. The peer is
45//! still expected to answer the JSON-RPC request eventually; use a notification
46//! instead when no response is expected at all. Once the SDK routes a response
47//! for the request, automatic cancellation is disarmed: the peer has already
48//! answered, even if caller code has not yet consumed the handle with
49//! [`block_task`], [`on_receiving_result`], or [`forward_response_to`], and even
50//! if a dispatch handler claimed the response.
51//!
52//! # Handling cancellation of incoming requests
53//!
54//! For incoming requests, get the request-local cancellation marker from the
55//! [`Responder`]. This keeps cancellation handling next to the request work it
56//! controls:
57//!
58//! ```
59//! # use agent_client_protocol::{ConnectionTo, Error, Responder, UntypedRole};
60//! # use agent_client_protocol_test::{MyRequest, MyResponse};
61//! # async fn example(request: MyRequest, responder: Responder<MyResponse>, cx: ConnectionTo<UntypedRole>) -> Result<(), Error> {
62//! # async fn run_request(_request: MyRequest) -> Result<MyResponse, Error> { todo!() }
63//! let cancellation = responder.cancellation();
64//!
65//! cx.spawn(async move {
66//!     let response = cancellation.run_until_cancelled(run_request(request)).await;
67//!     responder.respond_with_result(response)
68//! })?;
69//! # Ok(())
70//! # }
71//! ```
72//!
73//! [`run_until_cancelled`] is the simple path for handlers that should stop
74//! work and reply with the standard cancellation error as soon as cancellation
75//! is requested; it drops the work future when cancellation wins. If the
76//! handler needs cleanup, partial results, or custom cancellation behavior,
77//! use [`cancelled`][`RequestCancellation::cancelled`] or
78//! [`is_cancelled`][`RequestCancellation::is_cancelled`] directly inside the
79//! request work instead.
80//!
81//! Cancellation markers are only updated when the connection can process the
82//! incoming `$/cancel_request` notification. Long-running handlers should
83//! return quickly and move work into [`ConnectionTo::spawn`], [`SentRequest`]
84//! callbacks, or another task; see the [ordering](super::ordering) chapter.
85//!
86//! # Proxies
87//!
88//! When proxying with [`forward_response_to`], the SDK observes the upstream
89//! [`Responder`] cancellation marker and forwards cancellation to the
90//! downstream request automatically. The downstream response (normal data or a
91//! cancellation error) is still forwarded back upstream.
92//!
93//! Because cancellation propagates per hop this way, the raw notification is
94//! never tunneled across hops: [`ConnectionTo::send_proxied_message_to`] drops
95//! `$/cancel_request` notifications rather than forwarding a `requestId` that
96//! was allocated on a different connection and would be meaningless to the
97//! next peer.
98//!
99//! ## Custom methods on proxies
100//!
101//! A proxy that intercepts a method with its own handler decides what
102//! cancellation means for it. The SDK always records the cancellation on the
103//! request's [`Responder`] marker before the handler chain runs; what happens
104//! next is up to the handler that owns the request:
105//!
106//! - **Handle locally**: react to [`Responder::cancellation`] like any
107//!   request handler (ignore it, finish early, or respond with
108//!   [`Error::request_cancelled`]).
109//! - **Forward and propagate**: use [`forward_response_to`], or, when the
110//!   forwarding needs custom logic (rewriting the request, post-processing
111//!   the result), register the upstream marker explicitly with
112//!   [`forward_cancellation_from`] before consuming the handle:
113//!
114//! ```
115//! # use agent_client_protocol::{ConnectionTo, Error, Responder, UntypedRole};
116//! # use agent_client_protocol_test::{MyRequest, MyResponse};
117//! # async fn example(request: MyRequest, responder: Responder<MyResponse>, backend: ConnectionTo<UntypedRole>) -> Result<(), Error> {
118//! backend
119//!     .send_request(request)
120//!     .forward_cancellation_from(responder.cancellation())
121//!     .on_receiving_result(async move |result| {
122//!         // Custom result handling before responding upstream.
123//!         responder.respond_with_result(result)
124//!     })?;
125//! # Ok(())
126//! # }
127//! ```
128//!
129//! - **Absorb**: consume the handle without registering the marker
130//!   ([`on_receiving_result`] or [`block_task`] alone); the upstream marker is
131//!   still set, but nothing is sent downstream and the request runs to
132//!   completion there.
133//! - **Custom routing**: claim the `$/cancel_request` notification itself in a
134//!   handler (user handlers run before the generic forwarding fallbacks) and
135//!   translate it manually when you control the relevant hop-local request IDs.
136//!
137//! # Low-level access
138//!
139//! Register [`CancelRequestNotification`] (or [`ProtocolLevelNotification`])
140//! directly only when you need low-level access to cancellation notifications,
141//! such as custom routing or protocol tracing:
142//!
143//! ```
144//! # use agent_client_protocol::{ConnectionTo, Error, UntypedRole};
145//! use agent_client_protocol::schema::v1::CancelRequestNotification;
146//!
147//! # fn example() {
148//! let builder = UntypedRole.builder().on_receive_notification(
149//!     async |cancel: CancelRequestNotification, _cx: ConnectionTo<UntypedRole>| {
150//!         // Mark the matching in-flight operation cancelled.
151//!         let _request_id = cancel.request_id;
152//!         Ok(())
153//!     },
154//!     agent_client_protocol::on_receive_notification!(),
155//! );
156//! # let _ = builder;
157//! # }
158//! ```
159//!
160//! Such a handler observes cancellation notifications but does not replace
161//! the built-in handling: the SDK updates the [`Responder`] cancellation
162//! markers for every incoming `$/cancel_request` before the handler chain
163//! runs, even when a handler claims the notification.
164//!
165//! If you are implementing custom routing and already know the JSON-RPC request
166//! ID on the peer connection you are targeting, use
167//! [`ConnectionTo::send_cancel_request_to`]. Most code should use
168//! [`SentRequest::cancel`] instead, because the request handle already knows the
169//! correct peer, request ID, and proxy wrapping.
170//!
171//! [`block_task`]: crate::SentRequest::block_task
172//! [`on_receiving_result`]: crate::SentRequest::on_receiving_result
173//! [`forward_response_to`]: crate::SentRequest::forward_response_to
174//! [`run_until_cancelled`]: crate::RequestCancellation::run_until_cancelled
175//! [`RequestCancellation`]: crate::RequestCancellation
176//! [`RequestCancellation::cancelled`]: crate::RequestCancellation::cancelled
177//! [`RequestCancellation::is_cancelled`]: crate::RequestCancellation::is_cancelled
178//! [`ConnectionTo::send_request`]: crate::ConnectionTo::send_request
179//! [`ConnectionTo::send_request_to`]: crate::ConnectionTo::send_request_to
180//! [`ConnectionTo::send_proxied_message_to`]: crate::ConnectionTo::send_proxied_message_to
181//! [`ConnectionTo::spawn`]: crate::ConnectionTo::spawn
182//! [`SentRequest`]: crate::SentRequest
183//! [`SentRequest::cancel`]: crate::SentRequest::cancel
184//! [`SentRequest::detach`]: crate::SentRequest::detach
185//! [`forward_cancellation_from`]: crate::SentRequest::forward_cancellation_from
186//! [`ConnectionTo::send_cancel_request_to`]: crate::ConnectionTo::send_cancel_request_to
187//! [`Responder::cancellation`]: crate::Responder::cancellation
188//! [`Responder`]: crate::Responder
189//! [`Error::request_cancelled`]: crate::Error::request_cancelled
190//! [`CancelRequestNotification`]: crate::schema::v1::CancelRequestNotification
191//! [`ProtocolLevelNotification`]: crate::schema::v1::ProtocolLevelNotification