The question this answers
A user cancels a long agent run. What stops, what finishes anyway, and what is already irreversible?
A twelve-minute agent run that has issued a refund, sent two notification emails, has a database write in flight, and has four more tool calls queued — when the user presses stop.
The run's task tree, the tool-call queue, the in-flight calls' connections, and every external system the run has already touched.
After cancellation, no *new* side effect is initiated, every already-initiated effect reaches a known state, and the user is told which effects are permanent.
Synchronization exists to preserve this sentence. If a schedule can make it false, the code is wrong no matter which primitive it uses.
Four categories, and only two of them stop
Every unit of work in a cancelled run falls into exactly one of four buckets, and the useful discipline is to enumerate them explicitly rather than treating cancellation as one thing.
Not yet started — queued tool calls, planned steps. These stop cleanly by dropping them from the queue, and this is the only category where cancellation is free and complete. In flight — the model call streaming tokens, the HTTP request already sent. The model stream can genuinely be aborted, which stops the token bill. A tool call already dispatched is a different matter: you can stop *waiting* for it, but the request has left and the server will process it. Abandoning the wait is not cancelling the work — see Cancellation and Orphaned Tasks.
Committed but reversible — a database row written, a draft created, a file uploaded. These can be undone by explicit compensating action, and someone has to write that action. Committed and irreversible — the refund issued, the email sent, the message posted, the webhook delivered. Nothing undoes these. The refund can be followed by a charge, which is a *different* transaction with its own record, not an undo. The email cannot be recalled. This is the category that must be reported to the user rather than reasoned away.
| Category | Example | What cancellation achieves | What is required |
|---|---|---|---|
| Not yet started | Four queued tool calls; the next planning step | Complete — drop them from the queue | A queue you can actually drain, and a check before each dispatch |
| In flight, no side effect | Model call streaming tokens; a read-only search | Complete — abort the stream, stop the token bill | An abort signal plumbed to the provider client |
| In flight, with a side effect | POST /refund already sent; a database UPDATE in progress | Partial — you stop waiting; the server still processes it | Wait for the outcome anyway, or reconcile later. Never assume it did not happen. |
| Committed, reversible | Draft created; row inserted; file uploaded | Nothing automatically — requires a compensating action | A written, tested undo path per effect — Saga Pattern |
| Committed, irreversible | Refund issued; email sent; webhook delivered; message posted | Nothing. Ever. | Tell the user exactly what happened. This is the honest part. |
The most dangerous case: in-flight with a side effect
The unknown-outcome problem from What Changes When the Shared State Is on Another Machine arrives here in its sharpest form. You sent POST /refund. You then cancelled and stopped waiting for the response. Did the refund happen? You do not know, and you have thrown away the one channel that would have told you.
There are only three defensible responses. Wait for it anyway: treat in-flight side-effecting calls as uncancellable and let them complete before finishing the cancellation, which makes cancellation slower but leaves no unknown state. Record the intent before dispatching and reconcile afterwards: write "refund r-4471 attempted" durably before the call, so a later job can query the provider and determine what actually happened. Make it idempotent with a key so a subsequent retry — by the reconciler or by the user — cannot double it, which is Idempotency Keys: The Mechanism doing the load-bearing work again.
What is not defensible is dropping the wait and saying nothing. That leaves a side effect in an unknown state with no record, and the first anyone learns of it is a customer statement. The schedule below shows both halves: the clean stop for queued work, and the residue that the stop does not touch.
| # | User | Agent runtime | Tool calls | External systems | State |
|---|---|---|---|---|---|
| 1 | · | · | · | earlier in the run: refund r-4471 issued, 2 emails sent | permanent effects=3 queued=4 in flight=1 |
| 2 | · | · | db.updateOrder(o-88) dispatched — awaiting response | · | in flight=1 queued=4 |
| 3 | presses Stop | · | · | · | cancelled=true queued=4 in flight=1 |
| 4 | · | set cancelled flag; abort the model stream | · | · | queued=4 in flight=1 token spend=stopped |
| 5 | · | drain the tool queue: 4 calls dropped, never dispatched | · | · | queued=0 in flight=1 |
| 6 | · | · | db.updateOrder(o-88) is still in flight — the request left the process | · | in flight=1 |
| 7 | · | WRONG: abandon the wait, mark the run cancelled | · | · | o-88 state=UNKNOWN ✕ Every initiated effect reaches a known state. The order may or may not be updated and nothing recorded which. |
| 8 | · | RIGHT: await the in-flight side-effecting call before finishing | · | · | o-88 state=updated in flight=0 |
| 9 | · | report: 1 refund and 2 emails are permanent; order o-88 was updated; 4 steps not run | · | · | cancelled=complete permanent effects=4 |
Designing so cancellation means something
The most effective interventions happen before cancellation is requested. Order the plan so irreversible effects come last: do all the reads, all the reversible writes, and all the human-visible previews first, and put the refund and the emails at the end. Then cancelling at minute three of twelve costs nothing permanent, whereas the same run with the refund at step two has already spent the irreversible budget.
Gate irreversible effects behind approval, so the last irreversible action is a human decision rather than an agent one — Approval Gates and Risk Classes and In-the-Loop vs On-the-Loop and Escalation. Make effects reversible where you can: a draft rather than a send, a scheduled email with a delay window rather than an immediate one, a pending refund rather than a captured one. A five-minute delay converts an irreversible effect into a reversible one, and it is astonishing how often that is acceptable.
Structurally: cancellation must propagate down the task tree, which is the argument for Structured Concurrency — a run whose sub-tasks have a parent can be cancelled as a unit, while a run of detached spawns cannot be cancelled at all because nobody knows what is outstanding. And every cancellable operation needs a cancellation *check* between steps, because a cancellation signal nobody reads is not cancellation — Cancellation Propagation and the cancelled-but-running failure shape.
Key points
- Cancellation is a request to stop future work. It is never an undo of work already done, and any design that assumes otherwise is wrong.
- Four categories: not started (stops cleanly), in flight without effects (stops cleanly), in flight with effects (you stop waiting, the server does not stop), and committed (nothing stops it).
- Abandoning the wait on a side-effecting call leaves the effect in an unknown state — the worst outcome, because there is no record of the uncertainty.
- Three defensible responses to in-flight effects: wait for them anyway, record intent before dispatch and reconcile later, or make them idempotent so retry is safe.
- Order the plan so irreversible effects come last, and cancellation at minute three costs nothing permanent instead of everything.
- A five-minute send delay or a pending-rather-than-captured refund converts an irreversible effect into a reversible one, and is often acceptable.
- Cancellation must propagate down a task tree, which requires the tree to exist — detached spawns cannot be cancelled because nobody knows they are outstanding.
- The cancellation report is the deliverable: the user must be told exactly which effects survived.
The loop, answered
Every field is required, which is why no lesson here can recommend concurrency without naming the interleaving that breaks it, the complexity it adds, and the simpler thing to consider first.
- • A cancellation signal is set on the run, and every step checks it before dispatching the next unit of work — a signal nobody reads is not cancellation.
- • The model stream is aborted through the provider client, which stops token generation and the associated spend immediately.
- • The queue of undispatched tool calls is drained and discarded; each dropped call is recorded so the report can list what was not done.
- • In-flight calls are classified by effect: read-only calls are abandoned, side-effecting calls are awaited or recorded for reconciliation.
- • The signal propagates to child tasks and sub-agents through the task tree, and each child performs the same sequence for its own work.
- • A report is produced listing permanent effects, reversible effects that were compensated, unknown outcomes, and steps not run.
- • Cancel arrives while four calls are queued and one is in flight: the four are dropped, the model stream aborts, and the in-flight call completes on the server regardless of whether anyone is listening.
- • Cancel arrives one millisecond after
POST /refundis dispatched. Whether the refund happens is decided entirely by the server; the client's cancellation has no influence at all. - • Cancel arrives, the wait is abandoned, and the order-update request succeeds server-side. The run is marked cancelled, the order is updated, and no record connects the two — discovered later as an inconsistency with no explanation.
- • A detached sub-agent was spawned with no parent link. The parent run is cancelled; the sub-agent continues, makes three more tool calls, and posts a result to a run that no longer exists — Orphaned Tasks.
- • Cancel arrives during an approval gate at minute ten: nothing irreversible has happened, drafts are discarded, and the residue is exactly zero. Same signal, entirely different outcome, because of plan ordering.
- • Cancellation guarantees no *new* work is initiated after the signal is observed — and only at the points where the signal is actually checked.
- • Aborting a model stream guarantees the token spend stops. It does not guarantee the provider has not already billed for generated tokens.
- • Dropping a queued call guarantees that call never happens. This is the only complete guarantee cancellation offers.
- • Nothing guarantees an in-flight request does not take effect. The correct model is that it probably will.
- • A compensating action guarantees reversal only if it exists, is correct, and itself succeeds — compensation is code with its own failure modes, not an escape hatch.
- • No mechanism makes a sent email unsent. Stating this plainly to users is part of the design.
- • Waiting for in-flight side-effecting calls before completing cancellation makes cancellation slower, and users experience a stop button that does not stop immediately.
- • Compensating actions consume the same rate limits and connection pools as the original work, so mass cancellation produces a load spike of undo operations.
- • Reconciliation jobs query external systems for outcomes, adding sustained background load proportional to cancellation rate.
- • Cancellation propagation through a deep task tree takes time, during which children may still be dispatching work.
- • Cancelled-but-running: the signal is set, nothing checks it, and the run continues to completion while the UI reports it stopped.
- • Unknown outcome: an in-flight side effect abandoned with no record, discovered later as unexplained state.
- • Orphaned sub-agents that outlive the cancelled parent and continue spending money and making calls.
- • Partial compensation: three of five reversible effects undone, leaving state that matches no coherent point in the run.
- • Silent irreversibility: the run reports "cancelled" with no mention of the refund and two emails that are permanent.
- • Cancellation during compensation, leaving the undo half-applied — which is why compensating actions must themselves be idempotent and retryable.
- • Long runs where the user may change their mind, which is most interactive agent work.
- • Cost control: aborting a model stream stops the largest single spend term immediately — Budgets, Limits and Termination.
- • Runaway detection, where an automatic cancellation on a budget or loop-count breach prevents an expensive failure from continuing.
- • Any design where irreversible effects can be deferred behind an approval gate, which makes cancellation genuinely complete for most of the run.
- • When cancellation is implemented as "stop waiting" and sold as "stop", which produces unknown state and misleads the user.
- • When compensation logic is added for effects that could simply have been deferred — undo code is harder than reordering the plan.
- • When cancellation propagation is added without a task tree, producing a partial stop that is harder to reason about than no cancellation.
- • Time from cancellation signal to actual quiescence, and how much work is dispatched in that window.
- • Count of unknown-outcome effects per cancellation — the number that should be zero and rarely is.
- • Orphaned task count after cancellation: tasks spawned minus tasks that observed the signal.
- • Irreversible effects per cancelled run, which directly measures whether plan ordering is working.
- • Compensation success rate, since a failed undo is a silent inconsistency.
- • Token spend after the cancellation signal, which reveals whether the stream abort is actually plumbed through.
- • Every tool needs an effect classification and, for reversible effects, a written and tested compensating action.
- • Cancellation checks must be placed at every step boundary, and missing one produces a run that ignores the signal.
- • A task tree with parent links is required for propagation, which constrains how sub-agents may be spawned.
- • Reconciliation infrastructure for unknown outcomes is a background system with its own reliability requirements.
- • The user-facing report — what happened, what did not, what is permanent — is a real surface that must be built and kept accurate.
- • Structure the run so nothing irreversible happens without approval, which makes cancellation complete for the entire pre-approval phase — Approval Gates and Risk Classes.
- • Defer irreversible effects with a delay window, converting them into reversible ones for a period.
- • Make every side effect idempotent and let the run be re-run rather than cancelled, which sidesteps mid-run state entirely — Idempotency Keys: The Mechanism.
- • Bound the run with a budget or deadline instead of relying on manual cancellation — Timeouts, Deadlines vs Timeouts and Budgets, Limits and Termination.
- • Structured concurrency so the run is a scope whose children are cancelled with it, rather than a set of detached tasks — Structured Concurrency.
What people believe, and what is true
Cancelling the run undoes what it did.
Cancellation stops future work. A refund issued and two emails sent are permanent, and the honest design tells the user so rather than implying a rollback.
Aborting the request cancels the tool call.
It cancels your wait. The request has left; the server will process it. The outcome is now unknown unless you wait, record, or reconcile.
We set a cancelled flag, so the run stops.
It stops at the points that check the flag. A loop with no check runs to completion while the UI reports it stopped — the cancelled-but-running failure.