Skip to main content

Cancel-Safe Agents: The Side Effects Your Stop Button Already Shipped

· 11 min read
Tian Pan
Software Engineer

A user clicks Stop because the agent misunderstood the request. The UI flashes "stopped." By the time the spinner disappears, the agent has already sent two emails, scheduled a Tuesday meeting on the user's calendar, opened a draft pull request against the wrong branch, and queued a Slack message that is racing the cancellation signal through the tool layer. The model has obediently stopped generating tokens. The world has not stopped reacting to the tokens it generated thirty seconds ago.

This is the failure mode nobody covered in the agent demo. Cancellation in synchronous code was already a hard problem with a generation of cooperative-cancellation theory behind it: Go contexts, Python's asyncio.cancel, structured concurrency with task groups, the whole grammar of "ask politely, escalate carefully, don't leave resources behind." Agents take that already-hard problem and add a layer on top: the planner does not know that the user revoked authorization between step 4 and step 5, and the tools it kicked off in step 4 do not get a memo when step 5 is cancelled. Stop is a UI affordance. The system underneath stop has to be designed.

Stop Is a Lie Until It Names What Already Happened

Most agent UIs treat cancel as a binary: a button, a spinner that vanishes, a "stopped" toast. The user reads "stopped" and assumes nothing further will happen on their behalf. That assumption is the entire bug.

Inference cancellation is the easy half. The token stream stops cleanly because the model is a function the runtime can interrupt. Tool execution is the hard half. By the time the cancel signal reaches the tool layer, an HTTP request to a calendar API may already be in flight, an email may already be sitting in a queue waiting for a worker, and a database migration may already be halfway through a transaction the agent will never see commit or roll back. A trustworthy stop button has to surface these. The right UI affordance after cancel is not "stopped" — it is "here is what your agent did before you cancelled, here is what was interrupted, here is what is still in flight, here is what you can undo."

Building that UI requires the system underneath to know the answers. It almost never does, because the side-effect inventory is implicit: scattered across tool-call logs, vendor request IDs that may or may not have been written, and exception traces that describe what the agent attempted rather than what landed. The first architectural move is to make the inventory explicit. Every tool call has to be journaled before it is issued, with enough metadata that a post-cancel audit can reconstruct: what was attempted, what reached the upstream system, what came back, and which compensating action — if any — exists.

The Authorization Window Between Step 4 and Step 5

Long-running agents have a subtle authorization problem that synchronous tools never had to deal with: the user can change their mind during the run. They watch the agent take step 1, step 2, step 3, then realize at step 4 that the agent misread the goal. They press cancel. The planner is currently mid-token on step 5's tool call. The tool call lands.

In a well-designed system, the tool's permission to act is not a one-time grant at session start. It is re-checked at the moment of execution. The architectural primitive looks more like an OAuth-style scoped token that can be revoked and that the tool layer presents on every call than like a session-wide flag the planner reads once. Authorization should be evaluated at request time rather than just during initial connection, so an agent that drifts into a goal the user revoked cannot continue acting on the old grant. Concretely: the cancel signal does not just stop the planner — it invalidates the action grant that pending and future tool calls must hold to commit.

This shifts the failure mode from "tool call succeeded after cancel" to "tool call attempted after cancel and was rejected by the auth layer." That second failure mode is recoverable. The first is an incident.

A useful frame: every tool call is a two-phase commit where the second phase is conditional on a still-authorized signal. The tool prepares the side effect (composes the email, locks the calendar slot, opens the database transaction) but does not commit until it re-validates the authorization. Cancellation flips the bit, and prepared-but-uncommitted side effects fall on the floor instead of escaping into the world. This pattern is not free — every tool integration has to be designed with a prepare/commit boundary — but for irreversible actions it is the only way "cancel" can mean what users think it means.

Forward Plans Need Backward Plans

The saga pattern from distributed transactions has been the answer to "what happens when a sequence of side effects has to half-fail" for two decades. Agents are sagas, whether their authors realize it or not. The standard saga discipline applies: when a step has a side effect, define the compensating action that semantically undoes it before you ship that step. Refund the payment. Cancel the calendar invite. Recall the email if the protocol supports recall. Mark the database record reverted with a tombstone row, because the original write is still in the audit log.

The harder, agent-specific point is that compensation cannot always be derived after the fact. A planner that can call any of fifty tools cannot generically know how to undo any of fifty tools. The undo plan has to be authored by the human integrating the tool, not generated by the model. The architectural pattern that works in production is to register, alongside each tool, a paired compensating action and a classification of reversibility:

  • Reversible: the tool exposes a clean inverse (cancel a draft, delete a created record, void a pending charge)
  • Compensable: no inverse, but a semantically-undoing action exists (refund a charge, send a correction email, post a retraction)
  • Irreversible: nothing the system can do undoes it (a webhook fired to an external partner, a published article that has been indexed, a destructive command on a non-versioned filesystem)

Irreversible tools should be gated behind a confirmation step that holds the agent until a human approves, or at minimum a "this action cannot be cancelled once started" marker that the cancel UI surfaces honestly. The systems that get this right treat the reversibility class as a property of the tool registration, not a runtime decision the model makes. The model is not in a position to know whether the email it just sent can be unsent.

There is also an honest second-order failure: compensating actions can themselves fail, and the system has to be able to resume them at the failure point and retry. The post-cancel state machine is its own subsystem: a reverse-direction saga that walks the inventory of landed side effects, attempts the registered compensation for each, and reports — to the user, in the same UI that said "stopped" — which compensations succeeded, which are still in flight, and which the system gave up on and a human now owns.

Durable Ledgers Beat Trust in Memory

The reason most agent runtimes cannot answer "what already happened" cleanly is that they trust the model's loop variables to hold the truth. The agent thinks it ran tool A, then tool B; it has a list in its scratchpad. That list is not durable. If the worker crashes, restarts, hits a deploy, or is interrupted by cancellation in a non-cooperative way, the list is gone. Worse, the list is not the ground truth even when it survives — the model believes tool B succeeded because the response parsed, but the upstream system might have committed a different shape than the agent thought it did.

The pattern that has hardened in durable-execution systems over the last two years — Temporal-style workflow journaling, LangGraph checkpointing, DBOS-style transactional event logs — pushes the side-effect inventory into a write-ahead ledger that survives the agent's process. Every intent the planner forms gets written before it is executed. Every result gets written before it influences the next plan. If the runtime restarts, it replays the ledger to reconstruct state. If the user cancels, the ledger is the inventory the post-cancel UI reads from.

The ledger is not glamorous. It is a database table with a row per tool call, columns for intent, request payload, request ID, response, terminal status, and compensating-action status. It is the difference between a system that can answer "what happened" and a system that hopes the model remembered. For any agent that issues real-world side effects, the ledger is not optional; it is the substrate cancellation correctness rests on.

A subtle benefit: the ledger gives the cancel UI something to display. Instead of a vanished trace, the user sees a list — three tool calls completed, one in flight (now being cancelled), two compensations queued. They can decide to undo the calendar invite but keep the email. They can re-prompt the agent with the corrected goal and the agent can read the ledger to know which earlier work it does not need to redo. Cancellation stops being a destructive operation and starts being a state transition.

Test Cancel at Every Boundary, Not Just at the End

Most agent eval suites test the happy path: full prompt in, full response out, judge the answer. Cancellation evals are an order of magnitude rarer, and the ones that exist usually fire cancel after the agent finishes — which proves nothing because there is nothing to cancel. The eval discipline that catches cancel-correctness bugs cancels at every step boundary: between step 1 and step 2, mid-tool-call on step 3, after the tool committed but before the result reached the planner, during the compensation pass after a previous cancel. Each of these is a different code path with a different failure mode.

The asserts are different too. A cancellation eval is not asking "did the agent give the right answer" — there is no answer. It is asking: did the cancel signal halt new tool calls? Did the inventory ledger reflect everything that landed? Did the registered compensations run for compensable side effects? Did the UI display match the ledger? Did the auth layer reject any post-cancel attempts? These are integration tests masquerading as evals, and the team that takes them seriously surfaces an entire class of incident before users do.

A practical heuristic: any agent step that touches a tool with a side effect should have a paired cancel-during-execution test. The test does not have to be expensive — it can mock the tool with a deterministic delay and assert ledger state — but it has to exist. Agents that ship without this discipline will surprise their users in the same way distributed databases surprised their users twenty years ago, and for the same structural reasons: a system that does many things in many places will inevitably be asked to half-stop, and "half-stop" is a feature, not an edge case.

The Real Affordance Is Negotiation, Not Reversion

Stop, in the agent era, is not a synonym for undo. It is the first move in a negotiation: the user says "I want this to end now," the system says "here is what already happened, here is what I can undo, here is what I cannot, what would you like to do," and the user decides. The systems that get this right invest in the durable ledger that makes the inventory honest, the per-tool compensating actions that make undo a registered capability rather than an aspiration, the auth layer that lets revocation actually revoke, and the UX that surfaces all of this in language a non-engineer can act on.

The systems that do not invest in this lose user trust the first time a Slack message goes out after the user pressed cancel. There is no recovery from that failure mode, because the user's mental model — that stop means stop — was the only thing keeping them comfortable handing the agent the keys. Agents are about to be entrusted with more, not less, and the cancel button is the user's lifeline. Build it like one.

References:Let's stay in touch and Follow me for more thoughts and updates