Expand description
Request cancellation with $/cancel_request.
The SDK exposes the ACP $/cancel_request protocol-level notification:
either side may send it to ask the peer to cancel one outstanding JSON-RPC
request by ID.
Cancellation is cooperative. A peer may ignore $/cancel_request, may
finish with normal data, or may respond to the original request with
Error::request_cancelled (-32800). The requesting side always
receives a response to the original request; cancellation only changes
which response that is. Unhandled notifications are ignored by the SDK
so peers that do not support cancellation simply will not act on it.
§Cancelling outgoing requests
To cancel a request sent through ConnectionTo::send_request, keep the
returned SentRequest and call cancel on it:
let request = cx.send_request(MyRequest {});
request.cancel()?;
// The peer still responds to the request: with normal data if it raced
// ahead, or with the standard cancellation error.
let result = request.block_task().await;The SentRequest remembers the peer and any proxy wrapping used for the
original request, so this also works for requests sent through
ConnectionTo::send_request_to.
When another task needs to request cancellation after the request is consumed,
retain a RequestCancellationHandle first. Its drop is inert; an
application-owned guard can explicitly cancel when the caller is abandoned:
struct CancelOnAbandonment(RequestCancellationHandle);
impl Drop for CancelOnAbandonment {
fn drop(&mut self) {
if let Err(error) = self.0.cancel() {
eprintln!("Failed to request cancellation: {}", error.code);
}
}
}
let request = cx.prepare_request(MyRequest {});
let cancellation = request.cancellation_handle();
let (result_sender, result_received) = oneshot::channel();
request.on_receiving_result(async move |result| {
if let Err(result) = result_sender.send(result) {
clean_up_abandoned_result(result);
}
Ok(())
})?;
let _caller_guard = CancelOnAbandonment(cancellation);
let result = result_received.await.map_err(Error::into_internal_error)?;
apply_result(result);Install the guard in the caller’s scope, not the response callback, and only after the publishing method returns: cancellation before publication is not remembered. If the caller is abandoned, the guard requests cancellation; the selected callback remains responsible for the eventual result or cleanup.
Explicit cancellation does not discard the selected callback’s or future’s
response. Cloned handles share the same once-only cancellation state as
SentRequest::cancel and automatic request-drop cancellation, and remember
the original peer and proxy wrapping. Dropping a cancellation handle does
nothing. Cancelling a prepared request before publication is also a no-op; it is not remembered for
later publication. Responses and local failures disarm cancellation before
application callbacks run. Detaching suppresses only automatic cancellation:
retained handles can explicitly cancel a detached request while it is pending.
cancel() returning Ok(()) is not acknowledgment of transmission or peer
cooperation. It may have been a no-op, and an attempt begun before settlement
can still enqueue afterward. Cancellation controls peer request work, not
local callback work, and retaining a handle does not keep the connection
driver or response consumer alive.
In ACP v2, a successful session/prompt response means the user message was
inserted. The request is then complete, even if session work continues:
stopping that work requires session/cancel, not request cancellation.
See the v2 prompt lifecycle.
Dropping a SentRequest before the SDK receives a response also sends
$/cancel_request. This covers abandoned request handles and futures. For a
request whose eventual response should be ignored, but which should continue
running on the peer, call detach instead; the
eventual response is discarded, but no cancellation is sent. The peer is
still expected to answer the JSON-RPC request eventually; use a notification
instead when no response is expected at all. Once the SDK routes a response
for the request, automatic cancellation is disarmed: the peer has already
answered, even if caller code has not yet consumed the handle with
block_task, on_receiving_result, or forward_response_to, and even
if a dispatch handler claimed the response.
§Handling cancellation of incoming requests
For incoming requests, get the request-local cancellation marker from the
Responder. This keeps cancellation handling next to the request work it
controls:
let cancellation = responder.cancellation();
cx.spawn(async move {
let response = cancellation.run_until_cancelled(run_request(request)).await;
responder.respond_with_result(response)
})?;run_until_cancelled is the simple path for handlers that should stop
work and reply with the standard cancellation error as soon as cancellation
is requested; it drops the work future when cancellation wins. If the
handler needs cleanup, partial results, or custom cancellation behavior,
use cancelled or
is_cancelled directly inside the
request work instead.
Cancellation markers are only updated when the connection can process the
incoming $/cancel_request notification. Long-running handlers and ordered
SentRequest callbacks should return quickly and move work into
ConnectionTo::spawn or another task; see the
ordering chapter.
§Proxies
When proxying with forward_response_to, the SDK observes the upstream
Responder cancellation marker and forwards cancellation to the
downstream request automatically. The downstream response (normal data or a
cancellation error) is still forwarded back upstream.
Because cancellation propagates per hop this way, the raw notification is
never tunneled across hops: ConnectionTo::send_proxied_message_to drops
$/cancel_request notifications rather than forwarding a requestId that
was allocated on a different connection and would be meaningless to the
next peer.
§Custom methods on proxies
A proxy that intercepts a method with its own handler decides what
cancellation means for it. The SDK always records the cancellation on the
request’s Responder marker before the handler chain runs; what happens
next is up to the handler that owns the request:
- Handle locally: react to
Responder::cancellationlike any request handler (ignore it, finish early, or respond withError::request_cancelled). - Forward and propagate: use
forward_response_to, or, when the forwarding needs custom logic (rewriting the request, post-processing the result), register the upstream marker explicitly withforward_cancellation_frombefore consuming the handle:
backend
.send_request(request)
.forward_cancellation_from(responder.cancellation())
.on_receiving_result(async move |result| {
// Custom result handling before responding upstream.
responder.respond_with_result(result)
})?;- Absorb: consume the handle without registering the marker
(
on_receiving_resultorblock_taskalone); the upstream marker is still set, but nothing is sent downstream and the request runs to completion there. - Custom routing: claim the
$/cancel_requestnotification itself in a handler (user handlers run before the generic forwarding fallbacks) and translate it manually when you control the relevant hop-local request IDs.
§Low-level access
Register CancelRequestNotification (or ProtocolLevelNotification)
directly only when you need low-level access to cancellation notifications,
such as custom routing or protocol tracing:
use agent_client_protocol::schema::v1::CancelRequestNotification;
let builder = UntypedRole.builder().on_receive_notification(
async |cancel: CancelRequestNotification, _cx: ConnectionTo<UntypedRole>| {
// Mark the matching in-flight operation cancelled.
let _request_id = cancel.request_id;
Ok(())
},
agent_client_protocol::on_receive_notification!(),
);Such a handler observes cancellation notifications but does not replace
the built-in handling: the SDK updates the Responder cancellation
markers for every incoming $/cancel_request before the handler chain
runs, even when a handler claims the notification.
If you are implementing custom routing and already know the JSON-RPC request
ID on the peer connection you are targeting, use
ConnectionTo::send_cancel_request_to. Most code should use
SentRequest::cancel or RequestCancellationHandle::cancel instead,
because they share SDK settlement state and already know the correct peer,
request ID, and proxy wrapping.