Skip to content

Latest commit

 

History

History
170 lines (135 loc) · 8.3 KB

request-cancellation.md

File metadata and controls

170 lines (135 loc) · 8.3 KB

Request Cancellation

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.

The $/cancel_request Notification

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).

Semantics

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.

Unanswered Requests

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.

Interoperability

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.

Retaining Cancellation Control

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.

Request Cancellation Is Not Session Cancellation

$/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.

Proxy Chains

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:

  1. The client sends $/cancel_request for a request it made to its direct peer (for example, a proxy).
  2. A proxy that forwarded the request downstream (the SDK does this with forward_response_to) reacts by sending its own $/cancel_request for the downstream request, using the downstream connection's request ID.
  3. 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.

Related Documentation