This chapter documents the $/cancel_request protocol-level notification and
how the SDK implements it.
For API usage (cancelling a SentRequest, observing cancellation from a
Responder, or retaining a RequestCancellationHandle alongside a callback),
see the concepts::cancellation chapter in the
agent-client-protocol rustdoc.
Either side of a connection may send $/cancel_request to ask the peer to
cancel one outstanding JSON-RPC request, identified by its ID:
{
"jsonrpc": "2.0",
"method": "$/cancel_request",
"params": {
"requestId": "70b9f1c9-c2a3-4bd2-b6b9-65a06d96b675"
}
}requestId is the JSON-RPC id of the request to cancel, as allocated by the
sender of that request (a string, number, or null).
Cancellation is cooperative. After receiving $/cancel_request, the peer
may:
- ignore it and respond to the request normally,
- finish early with whatever data it has, or
- respond to the original request with the standard cancellation error,
code
-32800("Request cancelled").
The requesting side always receives a response to the original request;
cancellation only changes which response that is. A $/cancel_request for
an unknown or already-completed request ID is silently ignored. A
$/cancel_request with malformed params (for example, a requestId that is
not a string, number, or null) is logged and ignored without a reply, like any
other malformed notification.
The SDK makes sure a request it received is always answered, even when the
handler never responds. If a Responder is dropped without a response having
been sent through it, the SDK sends an error response for the request:
-32800(Request cancelled) if the peer had cancelled the request with$/cancel_request, as ACP requires for a cancelled request whose method defines no cancellation result;- otherwise
-32603(Internal error), with data naming the method.
If the handler that dropped the responder returns an error, that error is the
response instead. No automatic response is sent when a response was already
sent or handed off (for example with forward_response_to), when the
responder was moved into another task or wrapper that still owns it, or when
the connection is shutting down or has terminated. Notifications have no
responder and never get a reply.
The automatic error is an ordinary error response. A dropped initialize
responder fails initialization like any other initialize error: a first
initialize leaves the connection uninitialized, so the client can retry, and
a failed repeat keeps the previous initialization.
Protocol-level ($/-prefixed) notifications are optional by design. The SDK
ignores unhandled notifications instead of rejecting them with a
method-not-found error. A peer that sends $/cancel_request to a component
that does not support cancellation therefore loses nothing: the request simply
runs to completion.
Dropping an unconsumed SentRequest asks the peer to cancel it. Use
SentRequest::detach() for requests whose eventual response should be ignored,
but which should continue running on the peer. The peer is still expected to
answer the JSON-RPC request eventually; use a notification instead when no
response is expected at all.
Before consuming a PreparedRequest or SentRequest, call
cancellation_handle() to retain a cloneable RequestCancellationHandle.
Its cancel() method requests cancellation independently of response consumption:
an ordered callback or response future still receives the eventual result.
The handle shares once-only cancellation and settlement state with
SentRequest::cancel, forwarded cancellation, and automatic request-drop
cancellation. It cancels over the original request's connection with that
request's ID. Dropping the handle
neither sends cancellation nor disables automatic request-drop cancellation.
To cancel when a caller is abandoned, its application-owned guard or abandonment
handler must explicitly call cancel() and handle any immediate error.
Calling it before a prepared request is published does nothing and is not remembered for later publication. A call racing publication may also do nothing; hand control to independent cancellers after the publishing method returns. Once the SDK routes a response or fails the request, new cancellation calls do nothing—even if the application has not consumed the result yet. An attempt begun before settlement may still enqueue afterward.
Detachment is separate from settlement: detach() discards the response and
suppresses automatic cancellation on drop, but does not revoke retained explicit
handles. They can still cancel the detached request while it remains pending.
Ok(()) means the call encountered no immediate error, not that a notification
was sent or the peer stopped work. Cancellation is cooperative; it does not
guarantee transmission, peer cooperation, or a successful response. A handle does
not abort local callback work or keep the connection driver/response consumer alive.
$/cancel_request targets one pending JSON-RPC request, not the lifetime of the
session work it may start. In ACP v2, session/prompt succeeds once the user
message is inserted and returns its messageId. After insertion the Agent must
return success, not -32800, even if cancellation races the response. Stopping
active session work uses session/cancel. See the
v2 cancellation specification
and prompt lifecycle.
Cancellation propagates hop by hop rather than end to end. Request IDs are
allocated per connection, so a $/cancel_request only ever refers to a
request on the connection it is sent over:
- The client sends
$/cancel_requestfor a request it made to its direct peer (for example, a proxy). - A proxy that forwarded the request downstream (the SDK does this with
forward_response_to) reacts by sending its own$/cancel_requestfor the downstream request, using the downstream connection's request ID. - The downstream response — normal data or the cancellation error — flows back up the chain as the response to each hop's request.
Because the notification is hop-scoped, it is never tunneled across hops:
generic forwarding helpers (send_proxied_message_to in the SDK, and the
conductor's internal routing) drop a raw $/cancel_request instead of
forwarding a request ID that means nothing on the next connection. The
cancellation still reaches the next hop, re-issued by forward_response_to
with that hop's own request ID.
Proxies that intercept methods with custom handlers stay in control: the
request's cancellation marker is their decision point, and handlers see the
raw notification before any generic forwarding fallback. A custom handler can
handle the cancellation locally, propagate it to a forwarded request
(forward_response_to, or forward_cancellation_from when the forwarding
needs custom logic), absorb it, or claim the notification and route it itself.
See the concepts::cancellation chapter in the
agent-client-protocol rustdoc for
the full decision matrix.
When the notification targets a request that was wrapped in a
proxy/successor envelope (see the Protocol Reference), the
$/cancel_request is sent plain, not wrapped, and requestId is the JSON-RPC
id of the proxy/successor request on that connection. Receivers, including
the conductor, also accept a $/cancel_request wrapped in proxy/successor,
which earlier SDK releases sent, and treat it the same way.
The conductor translates cancellations between hops. Since request IDs are
reallocated at every hop, a raw $/cancel_request cannot match anything
beyond the hop it was sent over; the conductor re-issues cancellation with the
next hop's request ID instead.
- Protocol Reference - The
proxy/successorenvelope protocol - agent-client-protocol rustdoc - SDK API for sending, observing, and forwarding cancellations (see
concepts::cancellation)