/stop or Ctrl-C aborts a live subprocess within ~100ms instead of waiting for its own timeout.
This is in-flight cancellation. It stops a tool that is already running. To stop the next tool call (deadline or token), see Tool Timeouts & Cancellation.
Quick Start
1
Just works from an agent
Give the agent an
InterruptController, then request a stop from another thread — a running shell tool body is aborted within ~100ms and its result carries interrupted: True.2
Custom tool opts into cooperative abort
Read the event inside a custom tool via
Injected[AgentState], then poll or wait on it to bail out promptly.How It Works
The controller exposes its underlyingEvent; the tool-execution loop threads it into AgentState.cancel_event, and the running tool polls it every ~100ms.
The event is scoped to the current turn: the controller clears its flag when an interrupted turn ends, so a stale request cannot abort tools launched by the next turn.
Reading the outcome
The built-inshell tool returns a discriminated interrupted payload — distinct from a timeout.
A wall-clock timeout instead returns
stderr='Command timed out after N seconds' with no interrupted key — check result.get('interrupted') to tell them apart.
Choose your cancellation surface
Pick the surface by what you need to stop.- Stop the next tool →
cancel_token/timeout_mson the executor. See Tool Timeouts & Cancellation. - Stop the tool that is already running →
Agent.interrupt_controller+cancel_event(this page).
Custom InterruptController implementations
Exposing the event is optional — a controller without it keeps working, giving loop-level cancellation only.
getattr(controller, 'event', None), so a controller that omits event is fully backward compatible — it just skips in-flight abort and cancels at the loop boundary.
Best Practices
Check the event exists before using it
Check the event exists before using it
A tool may run standalone with no injected state. Guard with
if state.cancel_event is not None: before calling .is_set() or .wait().Prefer wait over busy-polling
Prefer wait over busy-polling
Use
state.cancel_event.wait(timeout=0.1) for idle waits instead of a tight is_set() loop, so you don’t burn CPU while waiting for work or the signal.Keep the poll window short
Keep the poll window short
Poll roughly every ~100ms — the same cadence the built-in
shell tool uses — so /stop feels instant to the user.Do not persist the event across turns
Do not persist the event across turns
The event is scoped to the current turn and cleared when an interrupted turn ends. Read it fresh from
state.cancel_event each call; never cache it on a module or instance.Related
Tool Timeouts
Deadlines and the executor-level cancel token
Tool Progress
Surface progress from the same slow tools
Gateway Abort & Timeout
HTTP-facing abort and deadlines
Run Outcome
Read the discriminated outcome from a run

