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).

Streaming honours the same Stop

start(stream=True) and iter_stream() short-circuit a pending tool batch on Stop, just like chat() — the same InterruptController / cancel_token seam covers the streaming path end-to-end.
Fixed in PraisonAI PR #5081 (issue #5073). Before this PR, cancel_token was dropped at the ToolCallExecutor.execute_batch() boundary on the streaming path — Stop halted the model between iterations but shell / MCP / file tools already dispatched in the current batch ran to completion, and the streaming loop still spent one more completion request on the cancelled turn. Since #5081, the token is forwarded from the public streaming entry point (start(stream=True) / iter_stream()) through get_response_stream into execute_batch, and three additional guards break the streaming and fallback loops the moment the token is signalled.

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