> ## Documentation Index
> Fetch the complete documentation index at: https://praison.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Cooperative Tool Cancellation

> Stop a running shell/HTTP/MCP tool promptly on /stop or Ctrl-C — not just the next one

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.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    Stop[⛔ /stop or Ctrl-C]:::agent --> Ctl[🎛️ InterruptController]:::config
    Ctl --> Ev[⚡ cancel_event set]:::process
    Ev --> Body[🛠️ Running tool body]:::process
    Body --> Kill[💀 Process group killed]:::warn
    Kill --> Out[✅ interrupted outcome]:::result

    classDef agent fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef process fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef config fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef warn fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef result fill:#10B981,stroke:#7C90A0,color:#fff
```

> [!NOTE]
>
> **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](https://praison.ai/docs/features/tool-timeout.md).

## 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`.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent
from praisonaiagents.agent import InterruptController

controller = InterruptController()

agent = Agent(
    name="Runner",
    instructions="Run shell commands when asked.",
    tools=["shell"],
    interrupt_controller=controller,
)

# From another thread / the CLI /stop / Ctrl-C handler:
controller.request("user_stop")
# A running `shell` tool body is aborted within ~100ms and the
# tool 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.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent
from praisonaiagents.tools import tool, AgentState, Injected

@tool
def slow_download(url: str, state: Injected[AgentState]) -> str:
    cancel = state.cancel_event
    for chunk in stream(url):                 # your streaming source
        if cancel is not None and cancel.is_set():
            return "interrupted"
        process(chunk)                          # your per-chunk work
    return "done"

agent = Agent(name="Downloader", tools=[slow_download])
```

***

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

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant User
    participant Agent
    participant Controller as InterruptController
    participant Tool as Tool body
    participant Proc as Subprocess

    User->>Controller: request("user_stop")
    Controller->>Controller: event.set()
    Note over Tool,Proc: tool polls cancel_event every ~100ms
    Tool->>Controller: cancel_event.is_set()?
    Controller-->>Tool: True
    Tool->>Proc: kill process group
    Tool-->>Agent: interrupted: True
    Agent-->>User: run stops promptly
```

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. Overlapping turns on the same Agent each live in their own `ContextVar` scope, so clearing one turn's flag never clears another's.

***

## Reading the outcome

The built-in `shell` tool returns a discriminated `interrupted` payload — distinct from a `timeout`.

| Field | Value | Meaning |
| - | - | - |
| `stdout` | `''` | No output captured on abort |
| `stderr` | `'Command interrupted by user'` | Human-readable reason |
| `exit_code` | `-1` | Process did not exit normally |
| `success` | `False` | The call did not complete |
| `interrupted` | `True` | Aborted by cooperative interrupt (not a timeout) |
| `execution_time` | `<float>` | Seconds until the abort |

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.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    Q{What do you<br/>need to stop?}:::agent
    Q -->|The NEXT tool call| A["timeout_ms / cancel_token<br/>on the executor"]:::config
    Q -->|A tool ALREADY running| B["Agent.interrupt_controller<br/>+ cancel_event"]:::process

    classDef agent fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef process fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef config fill:#6366F1,stroke:#7C90A0,color:#fff
```

* **Stop the *next* tool** → `cancel_token` / `timeout_ms` on the executor. See [Tool Timeouts & Cancellation](https://praison.ai/docs/features/tool-timeout.md).
* **Stop the tool that is *already running*** → `Agent.interrupt_controller` + `cancel_event` (this page).

***

## Cancel during LLM retry backoff

Stop interrupts the wait between LLM retries, so a 429 backoff no longer runs its full sleep.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    A[🤖 LLM call] -->|429| B[⏳ Backoff sleep]
    B -->|every ~200ms| C{🛑 Stop set?}
    C -->|No| B
    C -->|No, time up| A
    C -->|Yes| D[✅ InterruptedError]

    classDef agent fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef wait fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef process fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef result fill:#10B981,stroke:#7C90A0,color:#fff

    class A agent
    class B wait
    class C process
    class D result
```

While waiting, the agent checks the cancel token about every 200 ms and raises `InterruptedError("Agent chat cancelled: <reason>")` once it is set. Both retry paths honour it. Without an explicit `cancel_token`, `agent.interrupt_controller` is used.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
import threading
from praisonaiagents import Agent

cancel = threading.Event()
cancel.reason = "user"  # becomes the message in the raised InterruptedError

agent = Agent(name="t", instructions="x")
# cancel.set() during a 429 backoff raises:
# InterruptedError: Agent chat cancelled: user
```

See [Tool Retry & Backoff](https://praison.ai/docs/features/tool-retry-backoff.md) and [OpenAI Client Retries](https://praison.ai/docs/features/openai-client-retries.md) for retry settings.

***

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

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant User
    participant Agent
    participant Stream as get_response_stream
    participant Exec as execute_batch
    participant Tool as Pending tools
    participant Model

    User->>Agent: start(prompt, stream=True)
    Agent->>Stream: forward cancel_token
    Stream->>Model: request (tools=...)
    Model-->>Stream: tool_calls
    User->>Agent: interrupt_controller.request("user_stop")
    Stream->>Exec: execute_batch(..., cancel_token=token)
    Exec-->>Stream: cancelled ToolResult (pending calls skipped)
    Note over Stream,Model: follow-up completion request is NOT issued
    Stream-->>Agent: stream ends
    Agent-->>User: streamed output stops promptly

    classDef user fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef agent fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef stream fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef tool fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef model fill:#10B981,stroke:#7C90A0,color:#fff

    class User user
    class Agent agent
    class Stream,Exec stream
    class Tool tool
    class Model model
```

| Coverage | Non-streaming (`chat()`) | Streaming (`start(stream=True)` / `iter_stream()`) |
| - | - | - |
| Cancel before model returns tool\_calls | ✅ | ✅ |
| Pending tool batch short-circuits on Stop | ✅ | ✅ (since PR #5081) |
| Follow-up completion request skipped on Stop | ✅ | ✅ (since PR #5081) |
| Fallback loop (Anthropic / Gemini / Ollama with tools) honours Stop | ✅ | ✅ (since PR #5081) |

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent
from praisonaiagents.agent import InterruptController

controller = InterruptController()

agent = Agent(
    name="Runner",
    instructions="Search, then summarise.",
    tools=["shell", "search"],
    interrupt_controller=controller,
)

# From another thread / a UI Stop button:
# controller.request("user_stop")

for chunk in agent.start("Find the largest file under /var/log and summarise it", stream=True):
    print(chunk, end="", flush=True)
# On Stop mid-turn: pending tools return `error_kind="cancelled"` and the
# stream ends — no further completion request is issued.
```

> [!NOTE]
>
> **Fixed in PraisonAI [PR #5081](https://github.com/MervinPraison/PraisonAI/pull/5081) (issue [#5073](https://github.com/MervinPraison/PraisonAI/issues/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.

***

## Concurrent turns on one Agent

Two threads (or two asyncio tasks) can call `chat()` / `achat()` on the **same `Agent`** with **different** `cancel_token`s, and each turn's tools and approvals see their own token.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant ThreadA as Thread A
    participant ThreadB as Thread B
    participant Agent
    participant ToolA as Tool (turn A)
    participant ToolB as Tool (turn B)

    ThreadA->>Agent: chat("A", cancel_token=tokenA)
    ThreadB->>Agent: chat("B", cancel_token=tokenB)
    Agent->>ToolA: run under tokenA
    Agent->>ToolB: run under tokenB
    Note over ToolA,ToolB: each tool / approval sees its own turn's token
    ThreadB->>Agent: tokenB.request("user_stop")
    Note over ToolA: turn A is unaffected
```

Internally the active turn is bound to a `ContextVar` (`_active_turn_cancel`) that is copied into tool-executor threads and asyncio tasks, so a second `chat()` call does not overwrite the first turn's cancel token. The legacy `self._active_turn_token` is still set for backward compatibility; `_current_turn_liveness()` prefers the ContextVar and falls back.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
import threading
from praisonaiagents import Agent
from praisonaiagents.agent import InterruptController

agent = Agent(name="Shared", instructions="Answer concurrently.")

tokenA, tokenB = InterruptController(), InterruptController()

threading.Thread(target=agent.chat, kwargs={"prompt": "A", "cancel_token": tokenA}).start()
agent.chat("B", cancel_token=tokenB)

tokenB.request("user_stop")   # only turn B is cancelled; A keeps running
```

> [!NOTE]
>
> Available as of [PR #5764](https://github.com/MervinPraison/PraisonAI/pull/5764) (merged 2026-10-12, fixes [#5763](https://github.com/MervinPraison/PraisonAI/issues/5763)). Before this PR, the second concurrent `chat()` overwrote `self._active_turn_token`, so turn A's approvals read turn B's token — cancelling B could mark A's pending approval stale, and cancelling A was effectively a no-op for its own tools.

> [!NOTE]
>
> Since [PR #5885](https://github.com/MervinPraison/PraisonAI/pull/5885), the gateway inspects a registered agent's entry-point signature before the call to choose between passing `cancel_token` and the shared `interrupt_controller` fallback, so an application `TypeError` inside the entry point is no longer replayed. See [Bot Gateway → Custom agent entry-point contract](https://praison.ai/docs/features/bot-gateway.md#custom-agent-entry-point-contract).

***

## Custom `InterruptController` implementations

Exposing the `event` is optional — a controller without it keeps working, giving loop-level cancellation only.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
import threading
from praisonaiagents import Agent

class MyController:
    def __init__(self):
        self._flag = threading.Event()

    def request(self, reason: str = "user") -> None:
        self._flag.set()

    def clear(self) -> None:
        self._flag.clear()

    def is_set(self) -> bool:
        return self._flag.is_set()

    @property
    def reason(self):
        return "user" if self._flag.is_set() else None

    def check(self) -> None:
        if self._flag.is_set():
            raise InterruptedError("cancelled")

    @property
    def event(self) -> threading.Event:   # opt in to in-flight abort
        return self._flag

agent = Agent(name="Runner", tools=["shell"], interrupt_controller=MyController())
```

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.

> [accordion] InterruptController.protect() — keep new messages from superseding a window
>
> `protect()` is a nestable context manager that makes `is_protected()` return `True` while it is open. It does **not** block `request()` — `/stop` still cancels inside the window. Bot gateways in `busy_mode="interrupt"` check `is_protected()` and queue the new message instead. The SDK opens the window automatically around foreground subagents and context compaction; use it yourself only when your own code path should be treated the same way. ([PR #6124](https://github.com/MervinPraison/PraisonAI/pull/6124))
>
> ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
> from praisonaiagents.agent.interrupt import InterruptController, protect_turn
>
> ctrl = InterruptController()
>
> with ctrl.protect():
>     ...  # New user messages are queued by the bot gateway; /stop still works.
>
> # Safe for any controller, including None or a custom one without protect():
> with protect_turn(getattr(agent, "interrupt_controller", None)):
>     ...
> ```
>
> See [Turn Protection Window](https://praison.ai/docs/features/turn-protection-window.md).

***

## Best Practices

> [accordion] 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()`.

> [accordion] 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.

> [accordion] 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.

> [accordion] Safe to reuse one Agent across concurrent turns
>
> One `Agent` can serve many concurrent `chat()` / `achat()` calls, each with its own `cancel_token`. Each turn's tools and approvals read the token that was passed to that call — a stop on one turn never bleeds into another.

> [accordion] 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

> [card] [Tool Timeouts](https://praison.ai/docs/features/tool-timeout.md)
>
> Deadlines and the executor-level cancel token

> [card] [Tool Progress](https://praison.ai/docs/features/tool-progress-streaming.md)
>
> Surface progress from the same slow tools

> [card] [Gateway Abort & Timeout](https://praison.ai/docs/features/gateway-abort-and-timeout.md)
>
> HTTP-facing abort and deadlines

> [card] [Run Outcome](https://praison.ai/docs/features/run-outcome.md)
>
> Read the discriminated outcome from a run


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.