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` protocol-level notification:
4//! either side may send it to ask the peer to cancel one outstanding JSON-RPC
5//! request by ID.
6//!
7//! Cancellation is **cooperative**. A peer may ignore `$/cancel_request`, may
8//! finish with normal data, or may respond to the original request with
9//! [`Error::request_cancelled`] (`-32800`). The requesting side always
10//! receives a response to the original request; cancellation only changes
11//! *which* response that is. Unhandled notifications are ignored by the SDK
12//! so peers that do not support cancellation simply will not act on it.
13//!
14//! # Cancelling outgoing requests
15//!
16//! To cancel a request sent through [`ConnectionTo::send_request`], keep the
17//! returned [`SentRequest`] and call [`cancel`][`SentRequest::cancel`] on it:
18//!
19//! ```
20//! # use agent_client_protocol::{ConnectionTo, Error, UntypedRole};
21//! # use agent_client_protocol_test::MyRequest;
22//! # async fn example(cx: ConnectionTo<UntypedRole>) -> Result<(), Error> {
23//! let request = cx.send_request(MyRequest {});
24//! request.cancel()?;
25//!
26//! // The peer still responds to the request: with normal data if it raced
27//! // ahead, or with the standard cancellation error.
28//! let result = request.block_task().await;
29//! # let _ = result;
30//! # Ok(())
31//! # }
32//! ```
33//!
34//! The [`SentRequest`] remembers the peer and any proxy wrapping used for the
35//! original request, so this also works for requests sent through
36//! [`ConnectionTo::send_request_to`].
37//!
38//! When another task needs to request cancellation after the request is consumed,
39//! retain a [`RequestCancellationHandle`] first. Its drop is inert; an
40//! application-owned guard can explicitly cancel when the caller is abandoned:
41//!
42//! ```
43//! # use agent_client_protocol::{ConnectionTo, Error, RequestCancellationHandle, UntypedRole};
44//! # use agent_client_protocol_test::{MyRequest, MyResponse};
45//! # use futures::channel::oneshot;
46//! # fn apply_result(_result: Result<MyResponse, Error>) {}
47//! # fn clean_up_abandoned_result(_result: Result<MyResponse, Error>) {}
48//! struct CancelOnAbandonment(RequestCancellationHandle);
49//!
50//! impl Drop for CancelOnAbandonment {
51//!     fn drop(&mut self) {
52//!         if let Err(error) = self.0.cancel() {
53//!             eprintln!("Failed to request cancellation: {}", error.code);
54//!         }
55//!     }
56//! }
57//!
58//! # async fn example(cx: ConnectionTo<UntypedRole>) -> Result<(), Error> {
59//! let request = cx.prepare_request(MyRequest {});
60//! let cancellation = request.cancellation_handle();
61//! let (result_sender, result_received) = oneshot::channel();
62//! request.on_receiving_result(async move |result| {
63//!     if let Err(result) = result_sender.send(result) {
64//!         clean_up_abandoned_result(result);
65//!     }
66//!     Ok(())
67//! })?;
68//! let _caller_guard = CancelOnAbandonment(cancellation);
69//! let result = result_received.await.map_err(Error::into_internal_error)?;
70//! apply_result(result);
71//! # Ok(())
72//! # }
73//! ```
74//!
75//! Install the guard in the caller's scope, not the response callback, and only
76//! after the publishing method returns: cancellation before publication is not
77//! remembered. If the caller is abandoned, the guard requests cancellation; the
78//! selected callback remains responsible for the eventual result or cleanup.
79//!
80//! Explicit cancellation does not discard the selected callback's or future's
81//! response. Cloned handles share the same once-only cancellation state as
82//! [`SentRequest::cancel`] and automatic request-drop cancellation, and remember
83//! the original peer and proxy wrapping. Dropping a cancellation handle does
84//! nothing. Cancelling a prepared request before publication is also a no-op; it is not remembered for
85//! later publication. Responses and local failures disarm cancellation before
86//! application callbacks run. Detaching suppresses only automatic cancellation:
87//! retained handles can explicitly cancel a detached request while it is pending.
88//!
89//! `cancel()` returning `Ok(())` is not acknowledgment of transmission or peer
90//! cooperation. It may have been a no-op, and an attempt begun before settlement
91//! can still enqueue afterward. Cancellation controls peer request work, not
92//! local callback work, and retaining a handle does not keep the connection
93//! driver or response consumer alive.
94//!
95//! In ACP v2, a successful `session/prompt` response means the user message was
96//! inserted. The request is then complete, even if session work continues:
97//! stopping that work requires `session/cancel`, not request cancellation.
98//! See the [v2 prompt lifecycle](https://agentclientprotocol.com/protocol/v2/prompt-lifecycle#2-prompt-accepted).
99//!
100//! Dropping a [`SentRequest`] before the SDK receives a response also sends
101//! `$/cancel_request`. This covers abandoned request handles and futures. For a
102//! request whose eventual response should be ignored, but which should continue
103//! running on the peer, call [`detach`][`SentRequest::detach`] instead; the
104//! eventual response is discarded, but no cancellation is sent. The peer is
105//! still expected to answer the JSON-RPC request eventually; use a notification
106//! instead when no response is expected at all. Once the SDK routes a response
107//! for the request, automatic cancellation is disarmed: the peer has already
108//! answered, even if caller code has not yet consumed the handle with
109//! [`block_task`], [`on_receiving_result`], or [`forward_response_to`], and even
110//! if a dispatch handler claimed the response.
111//!
112//! # Handling cancellation of incoming requests
113//!
114//! For incoming requests, get the request-local cancellation marker from the
115//! [`Responder`]. This keeps cancellation handling next to the request work it
116//! controls:
117//!
118//! ```
119//! # use agent_client_protocol::{ConnectionTo, Error, Responder, UntypedRole};
120//! # use agent_client_protocol_test::{MyRequest, MyResponse};
121//! # async fn example(request: MyRequest, responder: Responder<MyResponse>, cx: ConnectionTo<UntypedRole>) -> Result<(), Error> {
122//! # async fn run_request(_request: MyRequest) -> Result<MyResponse, Error> { todo!() }
123//! let cancellation = responder.cancellation();
124//!
125//! cx.spawn(async move {
126//!     let response = cancellation.run_until_cancelled(run_request(request)).await;
127//!     responder.respond_with_result(response)
128//! })?;
129//! # Ok(())
130//! # }
131//! ```
132//!
133//! [`run_until_cancelled`] is the simple path for handlers that should stop
134//! work and reply with the standard cancellation error as soon as cancellation
135//! is requested; it drops the work future when cancellation wins. If the
136//! handler needs cleanup, partial results, or custom cancellation behavior,
137//! use [`cancelled`][`RequestCancellation::cancelled`] or
138//! [`is_cancelled`][`RequestCancellation::is_cancelled`] directly inside the
139//! request work instead.
140//!
141//! Cancellation markers are only updated when the connection can process the
142//! incoming `$/cancel_request` notification. Long-running handlers and ordered
143//! [`SentRequest`] callbacks should return quickly and move work into
144//! [`ConnectionTo::spawn`] or another task; see the
145//! [ordering](super::ordering) chapter.
146//!
147//! # Proxies
148//!
149//! When proxying with [`forward_response_to`], the SDK observes the upstream
150//! [`Responder`] cancellation marker and forwards cancellation to the
151//! downstream request automatically. The downstream response (normal data or a
152//! cancellation error) is still forwarded back upstream.
153//!
154//! Because cancellation propagates per hop this way, the raw notification is
155//! never tunneled across hops: [`ConnectionTo::send_proxied_message_to`] drops
156//! `$/cancel_request` notifications rather than forwarding a `requestId` that
157//! was allocated on a different connection and would be meaningless to the
158//! next peer.
159//!
160//! ## Custom methods on proxies
161//!
162//! A proxy that intercepts a method with its own handler decides what
163//! cancellation means for it. The SDK always records the cancellation on the
164//! request's [`Responder`] marker before the handler chain runs; what happens
165//! next is up to the handler that owns the request:
166//!
167//! - **Handle locally**: react to [`Responder::cancellation`] like any
168//!   request handler (ignore it, finish early, or respond with
169//!   [`Error::request_cancelled`]).
170//! - **Forward and propagate**: use [`forward_response_to`], or, when the
171//!   forwarding needs custom logic (rewriting the request, post-processing
172//!   the result), register the upstream marker explicitly with
173//!   [`forward_cancellation_from`] before consuming the handle:
174//!
175//! ```
176//! # use agent_client_protocol::{ConnectionTo, Error, Responder, UntypedRole};
177//! # use agent_client_protocol_test::{MyRequest, MyResponse};
178//! # async fn example(request: MyRequest, responder: Responder<MyResponse>, backend: ConnectionTo<UntypedRole>) -> Result<(), Error> {
179//! backend
180//!     .send_request(request)
181//!     .forward_cancellation_from(responder.cancellation())
182//!     .on_receiving_result(async move |result| {
183//!         // Custom result handling before responding upstream.
184//!         responder.respond_with_result(result)
185//!     })?;
186//! # Ok(())
187//! # }
188//! ```
189//!
190//! - **Absorb**: consume the handle without registering the marker
191//!   ([`on_receiving_result`] or [`block_task`] alone); the upstream marker is
192//!   still set, but nothing is sent downstream and the request runs to
193//!   completion there.
194//! - **Custom routing**: claim the `$/cancel_request` notification itself in a
195//!   handler (user handlers run before the generic forwarding fallbacks) and
196//!   translate it manually when you control the relevant hop-local request IDs.
197//!
198//! # Low-level access
199//!
200//! Register [`CancelRequestNotification`] (or [`ProtocolLevelNotification`])
201//! directly only when you need low-level access to cancellation notifications,
202//! such as custom routing or protocol tracing:
203//!
204//! ```
205//! # use agent_client_protocol::{ConnectionTo, Error, UntypedRole};
206//! use agent_client_protocol::schema::v1::CancelRequestNotification;
207//!
208//! # fn example() {
209//! let builder = UntypedRole.builder().on_receive_notification(
210//!     async |cancel: CancelRequestNotification, _cx: ConnectionTo<UntypedRole>| {
211//!         // Mark the matching in-flight operation cancelled.
212//!         let _request_id = cancel.request_id;
213//!         Ok(())
214//!     },
215//!     agent_client_protocol::on_receive_notification!(),
216//! );
217//! # let _ = builder;
218//! # }
219//! ```
220//!
221//! Such a handler observes cancellation notifications but does not replace
222//! the built-in handling: the SDK updates the [`Responder`] cancellation
223//! markers for every incoming `$/cancel_request` before the handler chain
224//! runs, even when a handler claims the notification.
225//!
226//! If you are implementing custom routing and already know the JSON-RPC request
227//! ID on the peer connection you are targeting, use
228//! [`ConnectionTo::send_cancel_request_to`]. Most code should use
229//! [`SentRequest::cancel`] or [`RequestCancellationHandle::cancel`] instead,
230//! because they share SDK settlement state and already know the correct peer,
231//! request ID, and proxy wrapping.
232//!
233//! [`block_task`]: crate::SentRequest::block_task
234//! [`on_receiving_result`]: crate::SentRequest::on_receiving_result
235//! [`forward_response_to`]: crate::SentRequest::forward_response_to
236//! [`run_until_cancelled`]: crate::RequestCancellation::run_until_cancelled
237//! [`RequestCancellation`]: crate::RequestCancellation
238//! [`RequestCancellation::cancelled`]: crate::RequestCancellation::cancelled
239//! [`RequestCancellation::is_cancelled`]: crate::RequestCancellation::is_cancelled
240//! [`ConnectionTo::send_request`]: crate::ConnectionTo::send_request
241//! [`ConnectionTo::send_request_to`]: crate::ConnectionTo::send_request_to
242//! [`ConnectionTo::send_proxied_message_to`]: crate::ConnectionTo::send_proxied_message_to
243//! [`ConnectionTo::spawn`]: crate::ConnectionTo::spawn
244//! [`SentRequest`]: crate::SentRequest
245//! [`SentRequest::cancel`]: crate::SentRequest::cancel
246//! [`RequestCancellationHandle`]: crate::RequestCancellationHandle
247//! [`RequestCancellationHandle::cancel`]: crate::RequestCancellationHandle::cancel
248//! [`SentRequest::detach`]: crate::SentRequest::detach
249//! [`forward_cancellation_from`]: crate::SentRequest::forward_cancellation_from
250//! [`ConnectionTo::send_cancel_request_to`]: crate::ConnectionTo::send_cancel_request_to
251//! [`Responder::cancellation`]: crate::Responder::cancellation
252//! [`Responder`]: crate::Responder
253//! [`Error::request_cancelled`]: crate::Error::request_cancelled
254//! [`CancelRequestNotification`]: crate::schema::v1::CancelRequestNotification
255//! [`ProtocolLevelNotification`]: crate::schema::v1::ProtocolLevelNotification