agent·interface

Stopping an Agent Mid-Task Without Corrupting State

Every serious agent surface needs a stop button. Most don't have one that reliably works — and the ones that do show how hard the problem actually is.

The button that doesn't reliably work

Take the most basic case: a person watching an agent go the wrong way, hitting the interrupt key, and expecting the agent to stop. Claude Code's own tracker has a report from mid-2025 describing exactly this failing — pressing Escape or Ctrl+C shows red feedback confirming the keypress registered, but the agent keeps executing the task anyway. The issue was eventually closed, but the failure mode it documents is the point: an agent mid-tool-call doesn't stop just because you told it to. It stops when the process controlling that tool call checks for the signal, and if that check happens after the consequential part of the action, you've "interrupted" nothing.

This is why coding agents distinguish two kinds of stop. A soft interrupt halts the current response but keeps the session, its context, and any completed file edits intact, so you can redirect without losing the run. A hard interrupt (killing the process) is unambiguous but throws away everything, including in-flight work you might have wanted to keep. Most products expose only the hard version and call it done.

What "resume" actually requires

Stopping is the easy half. The harder requirement — the one that separates an interrupt from a kill switch — is resuming from exactly where you stopped, without replaying side effects that already happened.

LangGraph's answer is to make the pause a first-class primitive. Calling interrupt() inside a node pauses the graph and returns control to the caller; a checkpointer persists the graph's state at that point, keyed to a thread ID, so the run can resume later with a Command carrying the human's answer — seconds or days afterward — without re-running completed steps. LangGraph exposes this as three durability settings (exit, async, sync), trading recovery precision against write overhead (LangGraph interrupts docs). That's the general shape every framework converges on: a pause has to be serializable, or it isn't a pause, it's a hang.

Two wire protocols formalize the same idea for interop, not just within one framework:

ProtocolMechanismResume path
A2ATask transitions to input-required stateClient sends a new message with the same taskId and contextId
AG-UIAgent emits an INTERRUPT event, ending the run with an interrupt outcomeClient starts a new run carrying the per-interrupt responses
LangGraphNode calls interrupt(), checkpointer persists stateCaller resumes with a Command, keyed to thread ID

A2A's task lifecycle treats input-required as a non-terminal state precisely so a remote agent can hand a task back to a client mid-execution and get it back later without losing place. AG-UI's interrupt model does the equivalent for the agent-frontend seam: pausing to request approval, structured input, or an out-of-band policy decision, then resuming safely once the human responds. Neither of these existed as cross-vendor conventions before 2025; both are new enough that most integrations still hand-roll the equivalent behavior underneath.

Where the wire format exists but the corruption still happens

MCP has its own cancellation path, and it's a good case study in why having a spec doesn't close the gap. Per the lifecycle spec, when a request doesn't get a response within a timeout, the sender should issue a cancellation notification and stop waiting. That's a reasonable contract. But a live bug in OpenAI's Codex MCP server shows what happens when only half the contract gets implemented: a client sends notifications/cancelled for an in-flight tool call, the server correctly stops doing the work — and then never sends back a response to the original tools/call request. The issue, filed in May 2026 and still open, leaves the client unable to tell "cancelled and cleaning up" from "permanently stuck," with process termination as the only recovery, which throws away every other in-flight thread along with the one that actually needed cancelling.

That's the interrupt problem in miniature: cancellation without a durable, observable end-state isn't cancellation, it's just a different way to hang. A wire format for "stop" is necessary but not sufficient — the implementation on both ends has to actually close the loop.

The other strategy: don't interrupt, checkpoint everything

Some coding agents sidestep the live-interrupt problem by making every step reversible instead. Cursor creates a revertible snapshot after each agent action, so a run that goes wrong doesn't need a precise, in-flight interrupt at all — you let it finish (or kill it outright) and roll back to any earlier snapshot with one click. Devin takes the async version of the same idea further: it's built to run unattended and hand control back at defined checkpoints rather than staying continuously interruptible, on the theory that if you're not watching in real time, a mid-execution stop button matters less than a good set of checkpoints to resume from. Both are real answers to the same problem "resume from where you paused" is trying to solve — they just move the pause point from "whenever the human presses a key" to "wherever the system already checkpoints."

What this means if you're building the interrupt yourself

Three things have to be true simultaneously, or your interrupt is cosmetic:

  • The stop has to actually stop. Check for the cancellation signal between side-effecting steps, not just between whole turns — a soft interrupt that only fires at response boundaries is not fast enough to prevent the harmful call it was meant to prevent.
  • State has to be serializable at the pause point, including what's already been done, so resume doesn't replay committed side effects.
  • Both ends of the wire have to close the loop. A cancellation notification that never gets an acknowledgment is indistinguishable from a hang, which is worse than no cancellation contract at all, because it looks like it should have worked.

None of the current standards get this fully right by default; they give you a place to put the state, not a guarantee that your implementation used it correctly. That's worth checking the next time you wire up an agent that anyone expects to be able to stop.

For where the interrupt/resume pattern sits against A2A, AG-UI, and LangGraph specifically, the tracker entry has the current state.


Tracking this space daily on the agent-interface tracker. Start at the hub if you're new to the term.