Agent Cancellation: What Stop Can Actually Promise
Design agent cancellation around durable intent, blocked dispatch, and effect reconciliation. Show what stopped, what completed, and what stays uncertain.
Agent cancellation is a request to stop pursuing a goal, not evidence that every consequence has stopped. A model stream can close while a tool keeps running. A worker can acknowledge a signal after an external service has already accepted its write.
The takeaway: close the boundary for new work, then reconcile work already admitted. Report cancellation intent, execution state, and effect outcomes separately. A single green “Cancelled” badge cannot describe all three honestly.
This is a design guide. Its state names, timelines, and acceptance cases are proposed contracts, not results from a completed experiment.
Define the agent cancellation boundary before the button
Suppose an agent prepares a report and emails it. The user presses Stop while the email request is in flight. There are several possible promises: stop generating the report, stop admitting new tool actions, ask the mail service to cancel, or guarantee that no recipient receives the message.
Those are different contracts. The last may be impossible once the receiving service accepts delivery. Choose the promise the system can enforce and put it in the task API as well as the interface.
A useful baseline is: after the cancellation request is durably accepted, no new ordinary-work commitments are admitted for this task; previously committed actions are reconciled and disclosed. This does not promise that all external activity ends at that instant. Define the cutoff using the authoritative state transition, not the user’s click time or a worker’s local clock.
Cancellation also needs scope. Identify the tenant, task incarnation, and affected subtree. Authenticate and authorize the request. Reusing a task identifier must not let a delayed cancellation stop a different task created later. Repeated requests should return the existing cancellation record rather than create competing shutdown procedures.
Model cancellation as intent plus execution state
Keep the cancellation record durable and monotonic for that task incarnation. It should contain who requested it, the accepted revision, its scope, and the reason. Clearing a browser notification or restarting a worker must not erase it.
One possible lifecycle is:
| State | Meaning | Evidence still required |
|---|---|---|
| Running | Ordinary work can be admitted under current authority | Normal completion or cancellation decision |
| Cancellation requested | The durable gate rejects new ordinary work | Worker and child acknowledgements; effect inventory |
| Draining | Owned computation is stopping and admitted effects are being resolved | Authoritative results or an explicit unresolved record |
| Cancelled, settled | Ordinary execution has stopped and admitted effects have known dispositions | Retained receipts and residual-effect summary |
| Stopped, effects unresolved | Execution is stopped or fenced, but an external outcome remains unknown | Continued reconciliation or operator resolution |
These are application states, not protocol constants. “Settled” means accounted for, not undone: the report may already have been emailed. If a worker cannot be proven stopped or prevented from acting, keep that limitation visible rather than claiming execution has ended.
Serialize cancellation acceptance with normal completion. If verified completion wins first, return “already completed.” If cancellation wins first, a late success response must not silently restore the task to Running or overwrite its cancellation history. It can still supply evidence that an admitted effect completed.
The durable record should outlive any one process. Otherwise, a restart can turn a cancelled task back into runnable queue work.
Close dispatch atomically with cancellation intent
Polling a cancellation flag before each tool call is necessary cooperation, but it leaves a race. A worker can check “not cancelled,” pause, and send after cancellation has been accepted.
Define a dispatch commitment: an atomic transition that checks task status, current authority, and the exact effect identity before recording an action for delivery. Cancellation acceptance and this commitment must serialize through the same authority. A transaction that sets a flag in one database cannot automatically gate a dispatcher using unrelated state elsewhere.
If cancellation wins, reject the proposed action. If commitment wins, classify the action as admitted before the cutoff, even if the network request leaves later. The dispatcher must carry a stable effect identifier and exact payload so recovery cannot substitute a different operation.
You may additionally suppress an admitted but unsent action. However, suppression and a dispatcher’s claim to send must compete through a defined atomic transition. A queue deletion or a last-minute flag check does not prove another worker has not already taken the message. Once delivery might have begun, record uncertainty rather than declaring the action never happened.
The agent concurrency-control guide explains revision checks, ownership generations, and fencing at the protected resource. Cancellation needs those boundaries but is not equivalent to lease expiry. Expiry permits recovery by another attempt; cancellation tells every attempt that ordinary pursuit of this goal should stop. A replacement worker must inherit that intent, not restart the original plan.
Propagate cancellation through owned agent work
A parent needs an inventory of what it owns: model requests, tool calls, queued children, running children, and local subprocesses. Record child ownership before making the child runnable, and gate child registration against cancellation. Otherwise, a parent can enumerate children, miss a concurrently created child, and incorrectly announce that its whole subtree stopped.
For each owned activity, send the supported cancellation signal and track its disposition. Remove queued work where the queue contract permits it, but have consumers check durable task state anyway: a delivered or duplicated message can outlive deletion. Prevent cancelled children from scheduling grandchildren or retrying the original work.
The gRPC cancellation guide explains that cancelling an RPC signals lost interest; the library generally cannot interrupt an application handler. Long-running handlers must cooperate, and propagation to outgoing RPCs depends on language support or application code. Closing the caller’s stream is therefore not a universal process-stop mechanism.
The same ownership distinction applies to shared work. If two tasks depend on one shared fetch or computation, cancelling one subscriber should release that subscriber’s interest, not automatically terminate the other task’s dependency. Explicitly detached work needs a separate owner and a clear product contract; “detached” must not become a loophole for continuing cancelled actions.
Use bounded cleanup and escalation. After a grace period, you may terminate owned local processes or revoke their execution capability. That still does not retract a remote request. Preserve the effect ledger outside the process being killed.
An acknowledgement is not proof of an absent effect
Separate three observations: the control service accepted cancellation, the worker observed it and stopped ordinary scheduling, and the effect receiver established the operation’s final disposition. Do not collapse them into one acknowledgement bit.
The MCP 2025-06-18 cancellation specification makes this distinction especially concrete. Cancellation is an optional notification for an in-progress request. A receiver may ignore it when the request is unknown, already completed, or cannot be cancelled. The notification itself is not a durable acknowledgement that an external action did not occur.
That version also says senders should ignore responses arriving after cancellation. Respect the wire contract; do not design correctness around a late response the client may discard. For side-effecting tools, retain an operation identifier and provide an independently authorized status or reconciliation path. A protocol request ID and a durable business-operation ID serve different purposes.
Adapters should document what their acknowledgement establishes. “Signal queued,” “handler stopped,” and “provider confirmed operation cancelled before execution” are materially different evidence. A timeout or disconnected socket establishes none of the stronger claims by itself.
Reconcile in-flight effects without restarting the goal
At cancellation acceptance, inventory admitted effects and track their outcomes independently of task execution. Useful dispositions include suppressed before dispatch, definitively rejected, completed, provider-confirmed cancelled, and unknown. Keep the receipt or authoritative lookup behind every definitive label.
Unknown is not failure, and it is not permission to retry under a new identifier. A retry can create exactly the effect the user was trying to stop. Query by the original operation identity where supported. If the provider cannot establish the outcome, retain that uncertainty and escalate according to the task’s consequence level.
Run reconciliation under separate, narrow authority. A cancelled worker may no longer have permission to act, while a control-plane reconciler still needs read access to operation status. Allowing this necessary bookkeeping should not reopen ordinary tool execution. Record which actions are read-only checks and which are further mutations.
Compensation is a new effect, not time travel. Deleting an uploaded draft, refunding a charge, and recalling a message have different preconditions and limits. Only perform compensation authorized by the task’s policy or a subsequent user decision; protect it with its own identity and outcome record. Stop does not inherently authorize arbitrary inverse actions.
The durable recovery-checkpoint guide covers persisting intent and reconciling uncertain delivery across crashes. Apply the same discipline to cancellation: after restart, recover into draining or reconciliation, not into another attempt at the original goal.
Test cancellation races and report what remains
A cancellation test should inspect durable state and receiver-side effects, not merely check that a coroutine raised an exception. The following are proposed acceptance cases, not a claim that this harness has run them:
- Before admission: cancellation wins the commitment race; no ordinary effect record is admitted afterward.
- After commitment: cancellation arrives before a delayed send; the declared contract either permits and reports delivery or proves atomic suppression.
- Lost response: the receiver commits, then the connection drops; the task retains an unknown outcome until reconciliation finds evidence.
- Child creation race: a child attempts registration during cancellation; it is either rejected or included in the cancellation inventory.
- Worker restart: a queued message is redelivered; the replacement reads durable cancellation and performs no ordinary work.
- Shared dependency: cancelling one subscriber does not stop another subscriber’s authorized work.
- Late completion: verified success races with Stop; the serialized winner determines task status without discarding effect evidence.
- Uncooperative worker: acknowledgement never arrives; escalation preserves the ledger and does not invent proof that remote work stopped.
Use the agent regression-gates approach to retain these races as release checks once implemented. Capture cancellation revision, effect commitments, worker acknowledgements, provider receipts, and unresolved outcomes so a failure can be reconstructed.
Finally, make the interface say what the system knows: “Stop requested; one action is still being checked,” or “Stopped; the email was already sent.” These messages are less tidy than one terminal badge, but they tell the user whether another decision is needed.
A reliable Stop button does not erase history. It prevents new authorized work, contains what is still running, and gives an honest account of what already escaped.