Skip to main content
Cancellation now reaches the tool body that is already running, so a /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 underlying Event; 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-in shell 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 toolcancel_token / timeout_ms on the executor. See Tool Timeouts & Cancellation.
  • Stop the tool that is already runningAgent.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.
The runtime probes with 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

A tool may run standalone with no injected state. Guard with if state.cancel_event is not None: before calling .is_set() or .wait().
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.
Poll roughly every ~100ms — the same cadence the built-in shell tool uses — so /stop feels instant to the user.
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.

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