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

# Tool Call Executor Timeout

> Cap each tool call in milliseconds and short-circuit pending calls with a cancel token

The tool-call executor caps each tool call with a millisecond timeout and lets you cancel pending calls, returning a typed result the model can reason about instead of hanging the turn.

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

def fetch_url(url: str) -> str:
    ...  # a tool that may hang

Agent(
    instructions="Look things up.",
    tools=[fetch_url],
    llm={"model": "gpt-4o", "tool_timeout_ms": 5000},  # 5-second per-tool cap
).start("Look up https://example.com")
```

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    subgraph "Tool Call Executor"
        Call[📞 Tool Call] --> Exec{⏱️ Timeout?}
        Exec -->|within budget| Body[▶️ Run Tool]
        Body --> OK[✅ ToolResult ok]
        Exec -->|timeout_ms exceeded| Timeout[⏰ ToolTimeoutError]
        Exec -->|orphan pool full| Capacity[🚧 ToolCapacityError]
        Exec -->|cancel_token set| Cancel[🛑 ToolCancelledError]
        Timeout --> Struct[📦 structured_error kind=timeout]
        Capacity --> Struct3[📦 structured_error kind=capacity]
        Cancel --> Struct2[📦 structured_error kind=cancelled]
    end

    classDef input fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef process fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef output fill:#10B981,stroke:#7C90A0,color:#fff
    classDef fail fill:#8B0000,stroke:#7C90A0,color:#fff

    class Call input
    class Exec,Body process
    class OK output
    class Timeout,Capacity,Cancel,Struct,Struct2,Struct3 fail
```

<Warning>
  `tool_timeout_ms` is in **milliseconds**. The wrapper-level `tool_timeout` (YAML / CLI) is in **seconds**. `tool_timeout_ms: 5000` = 5 seconds. See [Tool Config](/docs/configuration/tool-config) for the layers.
</Warning>

## Quick Start

<Steps>
  <Step title="Cap every tool call">
    Set `tool_timeout_ms` inside the `llm=` dict. A slow tool now returns a typed timeout result instead of freezing the turn.

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

    def fetch_url(url: str) -> str:
        ...  # a tool that may hang

    agent = Agent(
        instructions="Fetch pages and summarise them.",
        tools=[fetch_url],
        llm={"model": "gpt-4o", "tool_timeout_ms": 5000},  # 5s per tool
    )

    agent.start("Summarise https://example.com")
    ```
  </Step>

  <Step title="Cancel from another thread">
    Signal a `threading.Event`-shaped cancel token to abort pending tool calls — useful in a REPL or gateway where a user hits stop.

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    import threading
    from praisonaiagents.tools import (
        SequentialToolCallExecutor, ToolCall, ToolCancelledError,
    )

    cancel = threading.Event()

    def execute_tool(name, args, tool_call_id):
        return {"ok": True}

    executor = SequentialToolCallExecutor()
    cancel.set()  # e.g. fired from a signal handler / stop button

    results = executor.execute_batch(
        [ToolCall("fetch_url", {"url": "https://example.com"}, "call_1")],
        execute_tool,
        cancel_token=cancel,
    )

    print(results[0].error_kind)         # "cancelled"
    print(results[0].structured_error)   # {"error": True, "kind": "cancelled", ...}
    ```
  </Step>
</Steps>

***

## How It Works

The executor wraps each tool call, branching to a typed result on timeout or cancellation.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant User
    participant Agent
    participant LLM
    participant Executor
    participant Tool

    User->>Agent: Request
    Agent->>LLM: Prompt + tools
    LLM->>Executor: Tool call
    Executor->>Tool: Run (dedicated worker)
    alt within timeout_ms
        Tool-->>Executor: Value
        Executor-->>LLM: ToolResult ok
    else timeout_ms exceeded
        Executor-->>LLM: ToolResult error_kind=timeout
    else cancel_token set
        Executor-->>LLM: ToolResult error_kind=cancelled
    end
    LLM-->>Agent: Next step
    Agent-->>User: Response
```

| Condition | Behaviour |
| - | - |
| `timeout_ms is None` or `<= 0` | Tool runs directly with zero wrapping overhead. |
| `timeout_ms > 0` | Tool runs on a dedicated **daemon** worker thread (`tool-timeout`); on expiry the worker is abandoned (daemon, so it never blocks interpreter exit) and a typed timeout result is returned. Healthy concurrent timed calls are not capped; only workers that outlive their deadline count against a shared `_TIMEOUT_POOL_MAX = 32` orphan cap. |
| `cancel_token` signalled | Tool never runs; returns a typed cancelled result. |

<Warning>
  A timed-out **sync** tool's worker is abandoned, not killed — Python threads cannot be force-killed. It runs on a **daemon** thread, so a hung tool never blocks interpreter exit, but it may keep running in the background. Do not rely on the timeout for security-critical bounds.
</Warning>

<Note>
  **After a wall-clock timeout:** the orphaned daemon worker keeps running, the agent-level retry loop is **skipped unless the tool is idempotent**. Re-driving a mutating tool while its body is still executing would duplicate side effects, so only tools marked idempotent are retried — unknown tools default to non-idempotent (safe). See [Tool Timeouts → Retry-on-timeout is opt-in](/docs/features/tool-timeout#retry-on-timeout-is-opt-in-for-mutating-tools).

  **Two orphan caps, two layers.** The agent-level cap `_MAX_ORPHANED_TOOL_EXECUTORS = 4` (`agent/tool_execution.py`, PR [#5357](https://github.com/MervinPraison/PraisonAI/pull/5357)) refuses the next call with `{"tool_pool_exhausted": True}` once four executors are orphaned and no worker is free. The call-executor-level cap `_TIMEOUT_POOL_MAX = 32` (PR [#5589](https://github.com/MervinPraison/PraisonAI/pull/5589)) is distinct: a shared `BoundedSemaphore` caps only **abandoned** workers (those still running past their deadline). A slot is charged **lazily** at expiry — not up front — so healthy concurrency is never capped; the slot is released when the worker finally exits. The two layers compose: a refused call surfaces as `tool_pool_exhausted: True` at the agent layer and as `error_kind="capacity"` at the executor layer.
</Note>

A timed call lands in one of three terminal states: the worker finished (`success`), the deadline hit and a slot was charged lazily (`timeout`), or the orphan pool is saturated and the call never ran (`capacity`).

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    Call[📞 Timed tool call<br/>timeout_ms > 0]:::input --> Slot{🚧 Orphan pool<br/>saturated?}
    Slot -->|Yes — 32 abandoned workers| Cap[🚧 capacity<br/>call refused, never ran]:::fail
    Slot -->|No| Run[▶️ Run on daemon worker]:::process
    Run --> Fin{⏱️ Finished before<br/>deadline?}
    Fin -->|Yes| OK[✅ success]:::ok
    Fin -->|No| TO[⏰ timeout<br/>worker abandoned, slot charged lazily]:::fail

    classDef input fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef process fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef ok fill:#10B981,stroke:#7C90A0,color:#fff
    classDef fail fill:#8B0000,stroke:#7C90A0,color:#fff
```

***

## Configuration Options

| Option | Type | Default | Description |
| - | - | - | - |
| `tool_timeout_ms` (in `llm=` dict / `extra_settings`) | `int` | `None` | Per-tool timeout in **milliseconds**. `None` or `<= 0` disables it. |
| `cancel_token` (kwarg to `execute_batch`) | `threading.Event`-like | `None` | Any object with `is_set()`, `is_cancelled`, or `cancelled` (attribute or callable). When signalled, pending tool calls short-circuit. |

The cancel token is duck-typed — it supports both `threading.Event` and `InterruptController`-shaped tokens without importing a concrete type.

***

## Structured Error Envelope

`ToolResult.structured_error` returns a discriminated JSON envelope so the model can tell a timeout from a genuine bug, instead of an opaque `"Error executing tool: ..."` string.

```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
{
  "error": true,
  "kind": "timeout",
  "type": "ToolTimeoutError",
  "message": "Tool 'fetch_url' timed out after 5000ms",
  "tool": "fetch_url"
}
```

| Field | Type | Values |
| - | - | - |
| `error` | `bool` | Always `true` when present |
| `kind` | `str` | `"timeout"` \| `"cancelled"` \| `"capacity"` \| `"error"` |
| `type` | `str` | Exception class name (`ToolTimeoutError`, `ToolCancelledError`, `ToolCapacityError`, or the tool's own exception) |
| `message` | `str` | `str(exc)` |
| `tool` | `str` | Tool function name |
| `tool_pool_exhausted` | `bool` | `True` when the tool worker pool is saturated by hung earlier tool calls; the healthy tool was refused rather than queued behind them. |

A `capacity` kind carries `ToolResult.result == {"error": "capacity", "tool": "<name>"}`, `error_kind == "capacity"`, and `ToolResult.error` is a `ToolCapacityError` ("too many hung tool workers; refusing to abandon another"):

```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
{
  "error": true,
  "kind": "capacity",
  "type": "ToolCapacityError",
  "message": "too many hung tool workers; refusing to abandon another",
  "tool": "fetch_url"
}
```

The `kind` discriminant lets the model retry a `timeout`, abort on a `cancelled` or `capacity`, and report a plain `error` differently.

<Warning>
  **Seeing `error_kind="capacity"`?** Different from `tool_pool_exhausted` (agent-level, PR [#5357](https://github.com/MervinPraison/PraisonAI/pull/5357)): this means the **call-executor's** orphan pool (`_TIMEOUT_POOL_MAX = 32`) is saturated by hung workers — the new call never ran. Fix the hung tools; don't raise the timeout.
</Warning>

<Warning>
  **Seeing `tool_pool_exhausted`?** One or more of your earlier tool calls exceeded `tool_timeout` and are still running in background threads. Fix the hung tool — do **not** raise `tool_timeout` in response. The result means "your hung tools have starved the executor; investigate them", not "reduce your tool timeout".
</Warning>

***

## Exceptions

| Exception | Module | When raised |
| - | - | - |
| `ToolTimeoutError` | `praisonaiagents.tools` | Sync worker exceeded `timeout_ms` |
| `ToolCancelledError` | `praisonaiagents.tools` | `cancel_token` was signalled before / between calls |
| `ToolCapacityError` | `praisonaiagents.tools` (exposed via `tools.call_executor`) | Too many tool workers are already orphaned (timed out). Distinct from `ToolTimeoutError`: the new call never ran because the process is already saturated with abandoned workers (`_TIMEOUT_POOL_MAX = 32`). |

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents.tools import ToolTimeoutError, ToolCancelledError, ToolCapacityError
```

These are surfaced as `ToolResult.error` with the matching `error_kind` — they are not thrown to your calling code.

<Warning>
  There are **two** `ToolTimeoutError` classes in PraisonAI. `praisonaiagents.tools.ToolTimeoutError` (this page, milliseconds, executor layer) is distinct from `praisonai.agents_generator.ToolTimeoutError` (wrapper layer, seconds, YAML/CLI). Import the one for the layer you are catching at. See [Tool Config](/docs/configuration/tool-config).
</Warning>

***

## Common Patterns

Bound a whole batch with one timeout — most users' entry point:

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

def fetch_url(url: str) -> str:
    ...

Agent(
    instructions="Summarise pages.",
    tools=[fetch_url],
    llm={"model": "gpt-4o", "tool_timeout_ms": 3000},  # 3s cap on every tool call
).start("Summarise https://example.com")
```

Wire up graceful cancellation for a REPL or long-running gateway:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
import threading
from praisonaiagents.tools import SequentialToolCallExecutor, ToolCall

cancel = threading.Event()

def stop():
    cancel.set()  # call from a signal handler or a UI stop button

def execute_tool(name, args, tool_call_id):
    return {"ok": True}

executor = SequentialToolCallExecutor()
results = executor.execute_batch(
    [ToolCall("fetch_url", {"url": "https://example.com"}, "call_1")],
    execute_tool,
    timeout_ms=5000,
    cancel_token=cancel,
)
```

***

## Best Practices

<AccordionGroup>
  <Accordion title="Pick the right timeout layer">
    Use millisecond `tool_timeout_ms` for **user-facing latency budgets** (a chat turn should not hang). Use wrapper-level `tool_timeout` (seconds) for **hard operational cutoffs** in YAML/CLI crews.
  </Accordion>

  <Accordion title="Background work may continue">
    A timed-out sync tool's daemon worker is abandoned, not killed. Assume the tool may still be running. Never use the timeout to enforce security-critical bounds.
  </Accordion>

  <Accordion title="Reserve cancel tokens for interactive loops">
    Use `cancel_token` for interactive or streaming loops (bot, CLI, gateway) where a user can hit stop. Background jobs should rely on `tool_timeout_ms` alone.
  </Accordion>

  <Accordion title="Let the model see structured_error">
    Pass `ToolResult.structured_error` to the LLM instead of flattening it to a string. The discriminated `kind` is what lets the agent choose retry vs. abort vs. report.
  </Accordion>
</AccordionGroup>

***

## Related

<CardGroup cols={2}>
  <Card title="Async Tool Safety" icon="shield" href="/docs/features/async-tool-safety">
    Wrapper-level timeouts and safe async tool execution.
  </Card>

  <Card title="Tool Config" icon="wrench" href="/docs/configuration/tool-config">
    Configure timeouts across all three enforcement layers.
  </Card>

  <Card title="Concurrency" icon="bolt" href="/docs/features/concurrency">
    Parallel tool execution and per-agent limits.
  </Card>
</CardGroup>


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