Skip to main content
ACP has two separate cancellation mechanisms. This page covers both, plus two things the SDK adds that the protocol itself doesn’t define. For the protocol rules themselves, read the spec directly rather than relying on a restatement here: Cancellation ($/cancel_request, cooperative cancellation, -32800, and the spec’s own cascading-cancellation diagram) and Prompt Turn: Cancellation (session/cancel, the cancelled stop reason).

The two mechanisms

session/cancel (a notification) asks the agent to end the current prompt turn. The turn stays active until the cancelled prompt actually answers: a new prompt sent in between gets -32600 (invalid request), the same as any prompt sent while another is already running on that session. The agent session answers cancelled once session/cancel has been received for the prompt, whatever the handler itself returns or throws; see below. $/cancel_request cancels any single in-flight request, in either direction. Disposing a request’s Mono (directly, via .timeout(...), or the SDK’s own request timeout, 60 seconds by default on both sides) sends $/cancel_request once, after the request was written; the caller’s Mono ends immediately and a late answer from the peer is discarded. A prompt turn is the one exception: its answer isn’t bounded by the request timeout at all, only by promptTimeout if one is set. On a client, stop a prompt turn and still get the agent’s answer (stop reason cancelled, with the turn’s updates) with a CancellationSignal, as of 0.80.0:
AcpSyncClient.prompt(request, stop) takes the same signal, blocking. Calling stop.cancel() after the answer already arrived sends nothing. This replaces the only other way to do this before 0.80.0, contextWrite(RequestCancellation.cancelWhen(trigger)), which no completion menu offered and the sync client couldn’t use at all:
RequestCancellation/contextWrite still exists, for $/cancel_request on any other request type (not a prompt), where a CancellationSignal parameter isn’t offered.

Java SDK policy: the grace period and maxPromptDuration

Neither of these exists in the protocol, and no peer SDK has an equivalent (the closest precedent is an undocumented 1-second graceful-cancel timeout in the Kotlin SDK). They exist for a structural reason: the spec requires every cancelled prompt to be answered cancelled, and a session allows only one active turn, so a handler that never answers would leave its session busy forever with nothing able to recover it.
  • Cancel grace period (PromptTimeouts.cancelGracePeriod, default 60 seconds): if the handler hasn’t answered this long after session/cancel, the SDK cancels the handler’s subscription (interrupting its thread, for a sync handler) and answers cancelled on its behalf.
  • maxPromptDuration (PromptTimeouts.maxPromptDuration, off by default): a hard ceiling on one turn’s length, answered -32800 with data {"maxPromptDuration": "<ISO-8601>"} when it fires (or cancelled, if the prompt was already being cancelled).
Duration.ZERO disables either one. Exactly one answer is ever sent per prompt. Document both of these as SDK policy, not protocol behavior: they’re specific to this implementation.

Noticing a cancellation from inside a @Prompt method

A @Prompt handler finds out about a cancellation the same way regardless of which of the four triggers caused it (session/cancel or session/close for its session, $/cancel_request for its own request, or the agent itself ending it: the cancel grace period or maxPromptDuration passing, or the connection closing):
SyncPromptContext.isCancelled() is a poll: check it between steps of a long-running handler, to stop early rather than finish work nobody will see the result of. For work you can’t poll, such as a subprocess or an outstanding HTTP call that needs aborting, onCancel(Runnable) registers a callback that runs once, on whichever thread delivers the cancellation, so it must be quick and must not block. The async PromptContext.whenCancelled() returns a Mono<Void> that completes (empty) on cancellation, for composing into a Reactor pipeline, for example work.takeUntilOther(context.whenCancelled()) to stop a chain of operators. The handler doesn’t need to choose cancelled itself, or even succeed. Once session/cancel has been received for a prompt, the agent session sends cancelled as the answer no matter what the handler returns (PromptResponse.endTurn() included) or throws, keeping the handler’s own _meta when it returned a response. A handler that fails after a cancel, for whatever reason, no longer needs its own try/catch to avoid leaking -32603: the failure is answered cancelled too. Before session/cancel arrives, a handler’s own failure is still answered as any other failure would be; this only applies once the prompt is already being cancelled. The early-exit pattern above is still worth writing, to stop doing work nobody will see the result of, but the specific stop reason it returns no longer matters. Once isCancelled() is true, it stays true. After $/cancel_request (as opposed to session/cancel), the SDK has already answered and interrupted the handler’s thread, so whatever the handler eventually returns is simply discarded; that case is unaffected by the paragraph above.

The full sequence

Steps labeled “Java SDK policy” (B and C above) are this SDK’s behavior, not the protocol’s; the rest follows the spec directly.

What’s unchanged, and what’s new

The agent side’s incoming notification handlers are not ordered relative to responses, and session/cancel takes effect on the inbound thread before any later message is handled. A behavior change worth knowing if you’re upgrading: a client-side prompt timeout now actually cancels the turn at a Java agent (it used to let the agent keep running after the client gave up). Raise requestTimeout for genuinely long prompts if the old behavior was load-bearing anywhere. Another behavior change, more recent still: a prompt handler used to need to notice session/cancel and explicitly answer cancelled itself; a handler that returned another stop reason (or failed) after a cancel sent that instead, -32603 included. The agent session now sends cancelled regardless of what the handler returns or throws, once session/cancel has been received for that prompt. Code written against the old behavior (an explicit try/catch around the handler body solely to turn a post-cancel failure into cancelled) still works, since catching and returning cancelled by hand is simply redundant now, not wrong.

Concepts

The prompt turn, and why session/cancel doesn’t end it by itself

Migration Guide

The full session/cancel behavior change from 0.18.0