Skip to main content
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.
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")
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 for the layers.

Quick Start

1

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

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.
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", ...}

How It Works

The executor wraps each tool call, branching to a typed result on timeout or cancellation.
ConditionBehaviour
timeout_ms is None or <= 0Tool runs directly with zero wrapping overhead.
timeout_ms > 0Tool runs on a dedicated ThreadPoolExecutor(max_workers=1); on expiry the worker is abandoned and a typed timeout result is returned.
cancel_token signalledTool never runs; returns a typed cancelled result.
A timed-out sync tool’s worker is abandoned, not killed — Python threads cannot be force-killed. The tool may keep running in the background. Do not rely on the timeout for security-critical bounds.

Configuration Options

OptionTypeDefaultDescription
tool_timeout_ms (in llm= dict / extra_settings)intNonePer-tool timeout in milliseconds. None or <= 0 disables it.
cancel_token (kwarg to execute_batch)threading.Event-likeNoneAny 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.
{
  "error": true,
  "kind": "timeout",
  "type": "ToolTimeoutError",
  "message": "Tool 'fetch_url' timed out after 5000ms",
  "tool": "fetch_url"
}
FieldTypeValues
errorboolAlways true when present
kindstr"timeout" | "cancelled" | "error"
typestrException class name (ToolTimeoutError, ToolCancelledError, or the tool’s own exception)
messagestrstr(exc)
toolstrTool function name
The kind discriminant lets the model retry a timeout, abort on a cancelled, and report a plain error differently.

Exceptions

ExceptionModuleWhen raised
ToolTimeoutErrorpraisonaiagents.toolsSync worker exceeded timeout_ms
ToolCancelledErrorpraisonaiagents.toolscancel_token was signalled before / between calls
from praisonaiagents.tools import ToolTimeoutError, ToolCancelledError
These are surfaced as ToolResult.error with the matching error_kind — they are not thrown to your calling code.
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.

Common Patterns

Bound a whole batch with one timeout — most users’ entry point:
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:
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

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

Async Tool Safety

Wrapper-level timeouts and safe async tool execution.

Tool Config

Configure timeouts across all three enforcement layers.

Concurrency

Parallel tool execution and per-agent limits.