Originally published at Trida AI.
Clicking stop is the easy part. Everything after the click is where cancelling an agent run actually happens, and in most products it doesn't. Someone watches a run head in the wrong direction, clicks stop, the spinner disappears, and the run is filed as cancelled. Meanwhile a tool call that was already sent finishes a minute later and writes anyway. The screen said stopped. The world disagrees.
That gap isn't a UI bug. It's what cancellation defaults to when nobody designed it: a display state. Making stop mean something in the systems the run touched is a design problem, and durable workflow engines have already mapped most of it.
Cancellation is a request the run has to hear
Temporal draws the line clearly in its documentation of cancellation and termination. Cancellation is cooperative: the service delivers the request and the workflow code decides what to do with it, so it can release resources, compensate for steps that finished, and, in most SDKs, pick its own closing status. Termination is the opposite. The execution closes immediately, the code never sees the request, and no cleanup runs. Anything the run was holding stays held: Temporal's own list is reserved inventory, held locks, and partial writes left as written.
Agent runtimes need both words, used honestly. If your stop path kills the process, call it termination and accept what it leaves behind. If you call it cancellation, the run has to receive the request while it can still act on it, which means the signal needs a delivery route to every part of the run: the orchestrator, any child runs it spawned, and the tool call in flight.
Delivery is the part people assume. Temporal delivers an activity cancellation when the activity heartbeats: a long-running activity that doesn't heartbeat can't receive the request at all, and runs to completion, failure, or timeout instead. Translate that to agents and the equivalent is a tool wrapper that never checks in. A forty-minute export job with no progress reporting isn't just hard to watch. It's uncancellable, and no button upstream changes that.
Model calls have their own version of the same limit. OpenAI's API reference says a response can be cancelled only when it was created with the background parameter set to true; a synchronous call has no handle to cancel by. Whether a run can be stopped is often decided at the moment the work starts, not when someone clicks.
The call in flight decides what stop means
Picture a run with four steps. Two have finished. The fourth hasn't started, and stopping that one is easy: it simply never runs. Everything hard about cancellation lives in step three, the call that is in flight when the request arrives.
One run, the moment cancel arrives. The finished steps are history, the unstarted step is easy, and the call in flight is the one that has to report what it actually did.
That call ends in one of three states, and the runtime has to learn which. It stopped before acting. It acted, and the effect landed. Or it timed out in the middle, and nobody yet knows whether the write went through. The third state is the honest problem. Treating an unknown outcome as "cancelled, nothing happened" is how a stopped run still changes a record, sends the message, or creates the draft, and the operator finds out from the other system.
So a cancelled run shouldn't close with a single status word. It closes with an account: what stopped, what landed before the signal arrived, what cleanup removed, and what is still unknown and needs a person to settle it in the target system. If the unknown list isn't empty, the run isn't cleanly cancelled yet, whatever the UI says.
Child runs widen the account. A supervisor that spawns background work needs cancellation to reach that work too, or the parent stops and the children keep going on the old instructions. LangChain's async subagents documentation makes this an explicit supervisor tool: cancelling a task calls the server's cancel on the run, marks the task cancelled, and, where earlier runs are still active, also cancels those. The propagation is a feature someone built, not a default of spawning work.
Cleanup cannot live inside the thing you cancelled
There's a trap in the obvious implementation. The run receives the signal, catches it, and tries to tidy up: delete the temporary file, release the hold, withdraw the draft. But the tidy-up is new work scheduled inside a scope that has just been cancelled, so it gets cancelled too. Temporal's scopes make the fix explicit: cleanup runs in a nonCancellable scope, protected from the very signal that triggered it.
The agent version is the same shape. The component that runs cleanup, a reaper outside the run or a protected section of the runner, must not take instructions from the run it's cleaning up after, and it needs its own small budget of time and tool calls. Cleanup that can be starved by the cancellation itself is decoration.
What the operator sees after the cancel. One item landed before the signal arrived, so it stays, flagged for a person. Nothing on the report is listed as unknown.
Two limits are worth stating plainly. First, cancellation is never instant, and UIs should stop pretending otherwise: there's always a window where the request is travelling and the in-flight call hasn't reported back, and the status during that window is "cancelling," not "cancelled." Second, some tools offer no way to check in and no way to withdraw a request once sent. For those, the honest options are waiting for the outcome before closing the run, or recording the unknown and handing it to a person. A confident green checkmark isn't on the list.
None of this argues against giving people a stop button. It argues for building the plumbing the button implies: a signal that propagates to child runs and tool calls, check-ins from anything long-running, a protected cleanup path, and a closing report that separates what landed from what didn't. A run you can stop cleanly is also, not coincidentally, a run you understand while it's going.
Work with us
If stopping one of your agents today clears the screen while the work carries on underneath, we can help you build the version where the signal reaches the tool calls, the child runs, and the cleanup. Tell us about the deployment through Trida AI's intake.


Top comments (0)