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

