Skip to content

Conversation

@nerzhulart
Copy link

@nerzhulart nerzhulart commented Oct 21, 2025

This proposal covers per-JsonRPC request cancellation mechanism. The idea is taken from LSP.

The reason of adding per-request cancellation is to have cancellation on the JsonRPC level for any request without having specific cancellation methods for each feature/case.
Also, it fits well on async abilities of many languages (Kotlin, C#, probably JS/TS or Rust)

The description from the added cancellation.mdx

ACP uses JSON-RPC 2.0 for making requests and getting responses.

The JSON-RPC specification doesn't define any standard mechanism for request cancellation and keeps it up to the implementation.

- The agent and the client **MUST** implement [$/cancelRequest](./schema#%24%2Fcancelrequest) notification method to support per-request cancellation.

- When a cancellation request is received from the remote side, the corresponding request activity and all nested activities (including outgoing requests) **MUST** be cancelled, then one of the responses **MUST** be sent back:

  * an error response with the Cancelled [error code `-32800`](./schema#param-code)
  * a valid response with the appropriate data (e.g., a partial result or a valid result with the Cancelled marker)

- The calling side **MAY** implement graceful cancellation processing by waiting for the cancelled response (error code `-32800`) from the remote side.

- The request **MAY** be also cancelled from inside the request handler activity (e.g. when a heavy action is being performed in IDE by a request and the user cancels it explicitly). In this case the response with the [error code `-32800`](./schema#param-code) and the appropriate message **MUST** be sent back and the cancellation **SHOULD** be propagated on the calling side.

- Cancellation **MAY** also be done explicitly on a per-feature basis to cover the bigger scope of things to cancel (e.g., cancellation of a [prompt turn](./prompt-turn#cancellation))

Now, I'm not sure if to strictly require each side to implement the $/cancelRequest methods. If to make it optional we have to write if's in code to support both cancellation approaches: per-request and per-feature (like session/cancel). And, also we have to negotiate this in capabilities.

@nerzhulart nerzhulart force-pushed the n500/cancellationPerRequest branch from 3af7d1f to c0de9bb Compare October 21, 2025 18:40
@nerzhulart nerzhulart force-pushed the n500/cancellationPerRequest branch from c0de9bb to 82b5a5f Compare October 21, 2025 18:54
Copy link
Member

@benbrandt benbrandt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like this a lot! I have a few questions, but I think it is a pretty strong start.

I'm also realizing we need to make it clearer how to update the schema, as right now it is derived from the Rust types (and you pointed out in your updates some holes we have in the schema that I should add around the basic JSON RPC stuff)

But I can also help with the Rust side, I think more important is we hash out some of these details anyway.


The JSON-RPC specification doesn't define any standard mechanism for request cancellation and keeps it up to the implementation.

- The agent and the client **MUST** implement [$/cancelRequest](./schema#%24%2Fcancelrequest) notification method to support per-request cancellation.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately I think we may need to make this a capability given the number of in-progress agents/clients.

However, I think we could make this a "MUST" for a v2 of the protocol, which I think would make sense.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately I think we may need to make this a capability given the number of in-progress agents/clients.

However, I think we could make this a "MUST" for a v2 of the protocol, which I think would make sense.

Ok, no problem at all, so I'll describe it as a capability

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For v1 at least 😄 I'll separately start a document we can collaborate on for v2 breaking change candidates


- When a cancellation request is received from the remote side, the corresponding request activity and all nested activities (including outgoing requests) **MUST** be cancelled, then one of the responses **MUST** be sent back:
- an error response with the Cancelled [error code `-32800`](./schema#param-code)
- a valid response with the appropriate data (e.g., a partial result or a valid result with the Cancelled marker)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this also allow for the opposite side to also do some sort of graceful cancellation? Like we do for prompts where the agent can still send a session/update for currently in-progress items before responding with the cancelled stop reason?

Obviously the client would need to handle this as you pointed out in the line below, I'm just curious what the boundaries of both sides would be.

Copy link
Author

@nerzhulart nerzhulart Oct 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this also allow for the opposite side to also do some sort of graceful cancellation? Like we do for prompts where the agent can still send a session/update for currently in-progress items before responding with the cancelled stop reason?

Yes, the 2nd item is exactly to cover this. So if the handler handles a cancellation manually it may send a non-error response and the calling side will receive it. The 1st item covers the default case, when a handler execution is cancelled and not processed, in this case the default JsonRpcErrorCode.Cancelled should be sent


- The request **MAY** be also cancelled from inside the request handler activity (e.g. when a heavy action is being performed in IDE by a request and the user cancels it explicitly). In this case the response with the [error code `-32800`](./schema#param-code) and the appropriate message **MUST** be sent back and the cancellation **SHOULD** be propagated on the calling side.

- Cancellation **MAY** also be done explicitly on a per-feature basis to cover the bigger scope of things to cancel (e.g., cancellation of a [prompt turn](./prompt-turn#cancellation))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In your mind, do you think we should move to a world where prompt cancellation would also fall under this cancellation paradigm?

I think it might fit, I'm just curious in your mind where you see the boundary of specialized cancellation vs generic

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In your mind, do you think we should move to a world where prompt cancellation would also fall under this cancellation paradigm?

I think it might fit, I'm just curious in your mind where you see the boundary of specialized cancellation vs generic

TBH when I explored the prompt semantic I realized that it would be more handy (at least for us) to have prompt request only for the prompt start, not for the whole execution lifetime. It can make it more flexible.

Client   Agent
->> [request] startPrompt(PromptStartRequest(initialMessage, additonalArgs...))
<<- [response] PromptStartResponse(promptId=xxx)
<<- [update] PromptUpdate(promptId=xxx, agentChunk)
<<- [update] PromptUpdate(promptId=xxx, tool_call)
....
<<- [update] PromptUpdate(promptId=xxx, PrompFinishEvent.END_TURN/CANCELLED/ETC...)

In this case you can bind all the updates to a particular prompt, because now it's not handy to maintaint something like current prompt, in interprocess/intermachine communication it may race.


The Agent Client Protocol uses JSON RPC 2.0 for communication.

### <span class="font-mono">JsonRpcRequest</span>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call, we should expose this and make sure it is added to the schema

The ID of the request to cancel. Must match the id from a previously sent
request.
</ResponseField>
<ResponseField name="method" type={"string"} required>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need a payload here? Or just a request id?

Copy link
Author

@nerzhulart nerzhulart Oct 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need a payload here? Or just a request id?

Maybe to add a message? I can add arbitrary payload as well, but how to interpret it when received?


The JSON-RPC specification doesn't define any standard mechanism for request cancellation and keeps it up to the implementation.

- The agent and the client **MUST** implement [$/cancelRequest](./schema#%24%2Fcancelrequest) notification method to support per-request cancellation.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a common pattern to do a $ for a wildcard?
What do you think of other options like just cancel_request or request/cancel?
I'm open here, since we don't have one of this class yet, just curious the reasoning.

Copy link
Author

@nerzhulart nerzhulart Oct 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a common pattern to do a $ for a wildcard?
What do you think of other options like just cancel_request or request/cancel?
I'm open here, since we don't have one of this class yet, just curious the reasoning.

I decided to reuse the same name as in LSP. In my inderstanding the $ char means that the method belongs to the JsonRpc level (that is lower than ACP semantic level) unlike other methods that are explicitly bound to ACP entities. I don't mind to change it if you don't like it

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All good. Borrowing from LSP whenever possible makes sense to me!

@@ -1,4 +1,7 @@
{
"jsonRpcMethods": {
"cancel_request": "$/cancelRequest"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting question: I wonder if we need to have agent/client methods to handle this.
Obviously not at the protocol level, but we do need to somehow route this to the agent/client

Which means the agent/client will need to keep track of which request body was attached to each request id. If we require the cancellation to pass more params, we are close to just defining individual cancellation requests...

🤔 interesting question. It might be fine to require on the SDK level that this is handled nicely for the implementer. But I think all the more reason to put this behind a capability for now, so we can also potentially roll this out initially as "unstable" and work out some of the wrinkles

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which means the agent/client will need to keep track of which request body was attached to each request id. If we require the cancellation to pass more params, we are close to just defining individual cancellation requests...

I'm not sure about implementations in Rust or TS, but in Kotlin I maintatin a map in incoming requests, the map is [requestId->requestJob]. So while a request is being executed and cancel notification is received we know what exactly to cancel. BUT, it works exactly on JsonRpc level and doesn't touch semantic entities of ACP protocol, it's agnostic to what exactly should be cancelled

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think we can definitely do that, but for the graceful cancellation I wonder if there is a way to route it... It would be possible though

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants