Skip to main content
Async tool calls in agent.achat() now route through the same safety mechanisms as sync execution, including approval checks, timeouts, and Loop Guard’s doom-loop / no-progress protection (PR #4005). The sync-only execute_tool_with_circuit_breaker wrapper is not reused on the async path (it uses time.sleep); circuit-breaker state tracked elsewhere still applies.
The user runs tools via achat(); approvals, circuit breakers, and timeouts apply the same way as synchronous execution.

Quick Start

1

Async Tool with Approval

2

Task-Scoped Tools


How It Works

Architecture: The unified async dispatcher now calls execute_tool_async for tool invocations, so long-running tools no longer block the event loop.

Safety Mechanisms

Approval Checks

User approval prompts now appear during async tool execution:

Circuit Breaker Protection

Tool failure rates are tracked across async calls:

Loop Guard on the async path

Every async tool call in agent.achat() now runs through Loop Guard the same way sync calls always did (PR #4005). Repeated identical calls, repeated timeouts, and repeated exceptions all accumulate toward WARN → BLOCK → HALT on the current turn.
Async-specific details:
  • Async tool timeouts (asyncio.wait_for firing after tool_timeout seconds) are recorded as failures and count toward the BLOCK/HALT thresholds. Previously they early-returned and bypassed the streak.
  • Async tool exceptions are also recorded as failures — a tool that keeps raising escalates the same way a tool that keeps returning {"error": …} does.
  • A Loop Guard HALT propagates as ToolExecutionError(is_retryable=False). The async retry wrapper honours both loop_blocked (in the returned dict) and the exception, so no custom retry policy can re-run a terminally blocked call.
See Loop Guard for the full threshold table, tool classification, and configuration options.

Retry parity on the async path

Raised exceptions on agent.achat(...) / agent.execute_tool_async(...) are now retried under ToolConfig(retry_policy=RetryPolicy(...)), matching the sync path (PR #4141) — programming errors (ValueError, TypeError, AttributeError) and ToolExecutionError(is_retryable=False) (including Loop Guard HALT) stay terminal. See Tool Retry Policy for the full policy reference.

Timeout Controls

Async tool execution respects timeout settings:

Wrapper-Level Timeout (YAML / framework: praisonai)

Two ToolTimeoutError classes exist in PraisonAI. This section covers praisonai.agents_generator.ToolTimeoutError — the wrapper-level version raised on YAML/CLI tool_timeout (seconds). The SDK tool-call executor has its own praisonaiagents.tools.ToolTimeoutError (milliseconds, llm={"tool_timeout_ms": N}), surfaced as a ToolResult.error with error_kind="timeout". See Tool Call Executor Timeout.
When running through YAML or the CLI, the wrapper wraps every tool with a timeout-enforcing shim before handing the agent to the SDK. This provides defense-in-depth timeout enforcement even for pathological tools. On timeout the wrapper raises ToolTimeoutError (a TimeoutError subclass) instead of returning a JSON dict. This preserves each tool’s declared return-type contract — a typed return value is never silently downgraded to a string. Framework adapters catch it and translate it per framework. The wrapper handles sync and async tools differently:
  • Sync tools run in an instance-owned ThreadPoolExecutor (see _get_tool_timeout_executor in agents_generator.py); on timeout the future is best-effort cancelled. A call that already started cannot be interrupted, so background_work_may_continue is True.
  • Async tools are wrapped with asyncio.wait_for(...), which cancels the underlying task cleanly, so background_work_may_continue is False.
In Python, configure with tool_config=ToolConfig(timeout=…). Exception raised on wrapper-level timeout:
To catch the executor-level version instead, use from praisonaiagents.tools import ToolTimeoutError — see Tool Call Executor Timeout. ToolTimeoutError carries three attributes:
Once half the pool’s workers are permanently leaked to stuck sync tools, the pool is automatically recycled: leaked threads continue until their syscall returns, but new tool calls get a fresh pool instead of queueing behind them.
See Tool Configuration for full details and Concurrency for shape comparison.

Task-Scoped Tools

The tools_override parameter allows tasks to provide their own tool set for async execution:
For users, this manifests as task-specific tools being available during async chat:

Trace Events

Async tool execution now emits the same trace events as sync execution:

Migration Notes

No code changes required - async tool safety is automatically enabled: Before: Async calls bypassed safety mechanisms
After: Async calls use full safety pipeline

Best Practices

When using approval in async environments, ensure your event loop can handle user input:
Set appropriate timeouts for async tool execution:
Circuit breaker state affects all calls - monitor in async workflows:
Design task tools to be self-contained since they override agent tools:

Safe Defaults

On a fresh interactive session, the runtime now routes dangerous tools through ConsoleBackend automatically — no approval= kwarg needed. Off-TTY (pipes, CI) keeps deny-by-default. See Tool Approval → Default Behaviour for the precedence ladder and bypass flags.

Approval

Tool approval configuration

Tool Circuit Breaker

Tool failure protection

Tool Approval

Safe-by-default behaviour, bypass flags, and risk levels

Loop Guard

Always-on per-turn tool-call guardrails, now on both sync and async paths

Footnotes

  1. The async column was broken before PR #4141 — a raised exception was retried zero times regardless of RetryPolicy. See Tool Retry Policy → Sync / async parity.