Skip to content

Commit c0c1a2f

Browse files
committed
feat(acp): add request cancellation support
1 parent 2b68bcf commit c0c1a2f

11 files changed

Lines changed: 1379 additions & 8 deletions

File tree

md/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
- [Design Overview](./design.md)
88
- [Protocol Reference](./protocol.md)
9+
- [Request Cancellation](./request-cancellation.md)
910
- [Protocol V2](./protocol-v2.md)
1011

1112
# Conductor (agent-client-protocol-conductor)

md/request-cancellation.md

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# Request Cancellation
2+
3+
The SDK exposes the ACP `$/cancel_request` notification behind the
4+
`unstable_cancel_request` feature. The notification is protocol-level: either
5+
side may send it to ask the peer to cancel one outstanding JSON-RPC request by
6+
ID.
7+
8+
Enable the feature when depending on the crate:
9+
10+
```toml
11+
agent-client-protocol = { version = "...", features = ["unstable_cancel_request"] }
12+
```
13+
14+
To cancel a request sent through `ConnectionTo::send_request`, keep the
15+
returned `SentRequest` and call `cancel` on it:
16+
17+
```rust
18+
# use agent_client_protocol::{ConnectionTo, Error, UntypedRole};
19+
# use agent_client_protocol_test::MyRequest;
20+
# async fn example(cx: ConnectionTo<UntypedRole>) -> Result<(), Error> {
21+
let request = cx.send_request(MyRequest {});
22+
request.cancel()?;
23+
# Ok(())
24+
# }
25+
```
26+
27+
The `SentRequest` remembers the peer and any proxy wrapping used for the
28+
original request, so this also works for requests sent through
29+
`ConnectionTo::send_request_to`.
30+
31+
If you already have the JSON-RPC request ID, send the notification directly:
32+
33+
```rust
34+
# use agent_client_protocol::{ConnectionTo, Error, UntypedRole};
35+
# async fn example(cx: ConnectionTo<UntypedRole>) -> Result<(), Error> {
36+
cx.send_cancel_request("request-id".to_string())?;
37+
# Ok(())
38+
# }
39+
```
40+
41+
For incoming requests, get the request-local cancellation marker from the
42+
`Responder`. This keeps cancellation handling next to the request work it
43+
controls:
44+
45+
```rust
46+
# use agent_client_protocol::{ConnectionTo, Error, Responder, UntypedRole};
47+
# use agent_client_protocol_test::{MyRequest, MyResponse};
48+
# async fn example(request: MyRequest, responder: Responder<MyResponse>, cx: ConnectionTo<UntypedRole>) -> Result<(), Error> {
49+
# async fn run_request(_request: MyRequest) -> Result<MyResponse, Error> { todo!() }
50+
let cancellation = responder.cancellation();
51+
52+
cx.spawn(async move {
53+
let response = cancellation.run_until_cancelled(run_request(request)).await;
54+
responder.respond_with_result(response)
55+
})?;
56+
Ok(())
57+
# }
58+
```
59+
60+
`run_until_cancelled` is the simple path for handlers that should stop work and
61+
reply with the standard cancellation error as soon as cancellation is requested.
62+
If the handler needs cleanup, partial results, or custom cancellation behavior,
63+
use `cancellation.cancelled()` or `cancellation.is_cancelled()` directly inside
64+
the request work instead.
65+
66+
Cancellation markers are only updated when the connection can process the
67+
incoming `$/cancel_request` notification. Long-running handlers should return
68+
quickly and move work into `ConnectionTo::spawn`, `SentRequest` callbacks, or
69+
another task.
70+
71+
When proxying with `SentRequest::forward_response_to`, the SDK observes the
72+
upstream `Responder` cancellation marker and forwards cancellation to the
73+
downstream request automatically.
74+
75+
Register `CancelRequestNotification` or `ProtocolLevelNotification` directly
76+
only when you need low-level access to cancellation notifications, such as
77+
custom routing or protocol tracing:
78+
79+
```rust
80+
# use agent_client_protocol::{ConnectionTo, Error, UntypedRole};
81+
use agent_client_protocol::schema::CancelRequestNotification;
82+
83+
# fn builder() -> agent_client_protocol::Builder<UntypedRole> {
84+
UntypedRole.builder()
85+
.on_receive_notification(
86+
async |cancel: CancelRequestNotification, _cx: ConnectionTo<UntypedRole>| {
87+
let request_id = cancel.request_id;
88+
// Mark the matching in-flight operation cancelled.
89+
Ok(())
90+
},
91+
agent_client_protocol::on_receive_notification!(),
92+
)
93+
# }
94+
```
95+
96+
Cancellation is cooperative. A peer may ignore `$/cancel_request`, may finish
97+
with normal data, or may respond to the original request with
98+
`Error::request_cancelled()` (`-32800`). The SDK ignores unhandled `$/...`
99+
notifications so unsupported protocol-level notifications do not produce
100+
method-not-found errors.

src/agent-client-protocol/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## [Unreleased]
44

5+
### Added
6+
7+
- *(unstable)* Add SDK support for protocol-level request cancellation, including `SentRequest::cancel`, request-local cancellation helpers on `Responder`, and forwarded cancellation propagation.
8+
59
## [0.12.1](https://github.com/agentclientprotocol/rust-sdk/compare/v0.12.0...v0.12.1) - 2026-05-17
610

711
### Other

src/agent-client-protocol/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ default = []
1818
unstable = [
1919
"unstable_auth_methods",
2020
"unstable_boolean_config",
21+
"unstable_cancel_request",
2122
"unstable_logout",
2223
"unstable_mcp_over_acp",
2324
"unstable_message_id",
@@ -29,6 +30,7 @@ unstable = [
2930
]
3031
unstable_auth_methods = ["agent-client-protocol-schema/unstable_auth_methods"]
3132
unstable_boolean_config = ["agent-client-protocol-schema/unstable_boolean_config"]
33+
unstable_cancel_request = ["agent-client-protocol-schema/unstable_cancel_request"]
3234
unstable_logout = ["agent-client-protocol-schema/unstable_logout"]
3335
unstable_mcp_over_acp = ["agent-client-protocol-schema/unstable_mcp_over_acp"]
3436
unstable_message_id = ["agent-client-protocol-schema/unstable_message_id"]

0 commit comments

Comments
 (0)