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

# Concurrency

> Limit parallel agent runs and bound tool execution time

Concurrency controls let you limit parallel agent execution and set timeouts for tool calls to prevent resource exhaustion.

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

agent = Agent(
    name="worker",
    instructions="Respect concurrency limits across tool calls",
)

agent.start("Process these jobs without overloading APIs")
```

The user runs parallel work; concurrency controls cap simultaneous operations and tool timeouts.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    subgraph "Concurrency Control Flow"
        User[👤 User] --> Registry[🛂 ConcurrencyRegistry]
        Registry --> Agent[🧠 Agent]
        Agent --> ToolPool[🔧 Tool Pool<br/>2-thread executor]
        ToolPool --> Timeout{⏱️ Timeout?}
        Timeout -->|Yes| Error[❌ Timeout Result]
        Timeout -->|No| Success[✅ Tool Result]
    end
    
    classDef user fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef registry fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef pool fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef result fill:#10B981,stroke:#7C90A0,color:#fff
    classDef error fill:#8B0000,stroke:#7C90A0,color:#fff
    
    classDef agent fill:#8B0000,color:#fff
    classDef tool fill:#189AB4,color:#fff
    class User user
    class Registry registry
    class Agent agent
    class ToolPool pool
    class Success result
    class Error error
```

## Quick Start

<Steps>
  <Step title="Limit parallel runs of an agent">
    Control how many instances of the same agent can run concurrently:

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

    registry = ConcurrencyRegistry()
    registry.set_limit("researcher", 2)  # at most 2 concurrent runs

    agent = Agent(name="researcher", instructions="Research topics")

    # Sync context
    registry.acquire_sync("researcher")
    try:
        agent.start("Research Mars exploration")
    finally:
        registry.release("researcher")
    ```
  </Step>

  <Step title="Same, async">
    Use async context for better resource utilization:

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    await registry.acquire("researcher")
    try:
        await agent.astart("Research Mars exploration")
    finally:
        registry.release("researcher")
    ```
  </Step>

  <Step title="Bound tool time with ToolConfig">
    Prevent slow tools from blocking agent execution:

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

    agent = Agent(
        name="Assistant",
        instructions="Use tools to help users",
        tools=["get_weather"],
        tool_config=ToolConfig(timeout=30),  # seconds; slow tools return a timeout dict
    )
    agent.start("What's the weather in Tokyo?")
    ```
  </Step>
</Steps>

***

## How It Works

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant User
    participant Registry as ConcurrencyRegistry
    participant Agent
    participant Executor as Tool Executor
    
    User->>Registry: acquire_sync("researcher")
    Registry->>Registry: Check semaphore limit
    Registry->>Agent: Grant execution slot
    Agent->>Executor: submit(tool, timeout=30s)
    
    alt Tool completes in time
        Executor->>Agent: Return result
    else Tool times out
        Executor->>Agent: {"error": "Tool timed out after 30s", "timeout": True}
    end
    
    Agent->>User: Response
    User->>Registry: release("researcher")
```

### Retune the limit safely at any time

Call `set_limit()` at any time — even while agents already hold permits — and the new cap takes effect immediately without letting phantom permits leak.

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

reg = ConcurrencyRegistry()
reg.set_limit("worker", 3)

# ... three "worker" runs are already in flight ...

reg.set_limit("worker", 5)  # widen mid-flight — the same limiter is resized in place
reg.set_limit("worker", 2)  # shrink again — in-flight holders stay on the same counter
```

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    subgraph "set_limit() while running"
        H1[🧠 Holder 1] --> Lim[🛂 Limiter]
        H2[🧠 Holder 2] --> Lim
        H3[🧠 Holder 3] --> Lim
        Retune[⚙️ set_limit new] -->|resize in place| Lim
        Lim -->|same object| Same[✅ Holders continue]
    end

    classDef holder fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef limiter fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef config fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef result fill:#10B981,stroke:#7C90A0,color:#fff

    class H1,H2,H3 holder
    class Lim limiter
    class Retune config
    class Same result
```

| Component | Purpose | Thread Safety |
| - | - | - |
| `ConcurrencyRegistry` | Limits parallel agent runs — a **true global cap across every event loop and thread**. Retuning with `set_limit()` while agents are running is safe — the cap can never be silently exceeded. | ✅ Thread-safe |
| Tool Executor | Runs tools with timeout | ✅ Per-agent pool |
| Plugin APIs | Enable/disable plugins | ✅ Lock-protected |
| `AgentTeam` / `PraisonAIAgents` run guard | Refuses concurrent runs on one instance | ✅ Per-instance re-entrancy lock |

PraisonAI's concurrency controls at a glance:

* **`ConcurrencyRegistry`** caps how many agent runs happen in parallel.
* **Per-agent tool executor** applies `tool_timeout` and recycles a hung worker.
* **`AgentTeam` / `PraisonAIAgents` instances are single-run** — a second concurrent `start()` / `astart()` on the same object raises `RuntimeError`. Batched runs via `start_for_each` / `astart_for_each` are unaffected. See [AgentTeam Batch Runs → Concurrent-run safety](/docs/features/agentteam-batch#concurrent-run-safety).

<Note>
  **Global cap across loops and threads, and safe to retune live.** Each agent's limit is backed by a loop-neutral, in-place-resizable limiter keyed by agent name, so the configured cap holds across **every** event loop and thread — and calling `set_limit()` while agents are already running retunes the limit safely for in-flight holders without ever exceeding the new cap. Sync and async callers share the **same** per-agent permit pool. See [PR #5050](https://github.com/MervinPraison/PraisonAI/pull/5050) (loop-neutral) and [PR #5330](https://github.com/MervinPraison/PraisonAI/pull/5330) (safe retune).
</Note>

### Dynamic reconfiguration safety

Since [PR #5258](https://github.com/MervinPraison/PraisonAI/pull/5258), `set_limit()` and `remove_limit()` are safe to call while permits are still outstanding. Each `acquire()` / `acquire_sync()` now returns the exact semaphore it obtained, and the matching `release()` frees that same instance — so a runtime cap change never redirects a release onto a fresh semaphore and never silently inflates the effective cap.

Before the fix, calling `set_limit("agent", N)` or `remove_limit("agent")` while a permit was outstanding could permanently corrupt the cap: the outstanding release landed on a newer semaphore, which `threading.Semaphore` (unlike `BoundedSemaphore`) accepts without raising. From that point on the configured cap was meaningless and the drift was undetectable from the public API.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant Caller
    participant Registry as ConcurrencyRegistry
    participant S1 as Semaphore (old)
    participant S2 as Semaphore (new)

    Caller->>Registry: acquire("agent")
    Registry->>S1: acquire permit
    Registry-->>Caller: return S1 handle
    Note over Registry,S2: set_limit("agent", N) mid-flight → S2 replaces S1
    Caller->>Registry: release("agent")
    Registry->>S1: release the acquired handle (S1), not S2
    Note over S2: new cap stays intact — no silent inflation

    classDef reg fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef old fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef new fill:#10B981,stroke:#7C90A0,color:#fff

    class Registry reg
    class S1 old
    class S2 new
```

<Note>
  `release(name)` remains backward-compatible: passing only a name still works via a name-lookup fallback. The exact-handle path is used automatically when you release the pair returned by `acquire()` / `acquire_sync()`.
</Note>

***

## Sync vs Async Rule

`acquire_sync()` is safe from any context; in async code prefer `await acquire()` to avoid blocking the loop.

| Context | Method | What happens |
| - | - | - |
| **Sync** | `registry.acquire_sync(name)` | ✅ Works correctly |
| **Async** | `await registry.acquire(name)` | ✅ Works correctly — yields to the loop |
| **Mixed** | `acquire_sync()` in async | ⚠️ Works, but blocks the calling thread — prefer `await acquire()` |

<Warning>
  `acquire_sync()` is safe to call from any context (sync or async). Under the hood it acquires a loop-neutral, in-place-resizable limiter, so it never raises "bound to a different event loop". In an **async** context, however, it blocks the current thread until a permit is available — prefer `await acquire()` there so other tasks on the loop keep running. Sync and async callers share the **same** per-agent permit pool, so the configured limit is a true global cap across every event loop and thread.
</Warning>

**Prefer `await acquire()` in async code:**

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
async def works_but_blocks():
    registry.acquire_sync("agent")  # ⚠️ permitted, but blocks the event loop thread

async def recommended():
    await registry.acquire("agent")  # ✅ yields to the loop
```

***

## Tool Timeout Behavior

When `tool_config=ToolConfig(timeout=...)` is set, tools run in a dedicated executor with these characteristics:

*In YAML the field name is still `tool_timeout:`; in Python use `tool_config=ToolConfig(timeout=…)`.*

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    Call[🛠️ Tool call] --> Which{framework = praisonai<br/>AND plain callable?}
    Which -->|Yes| Core[🧠 Core enforces<br/>Agent.tool_config.timeout]
    Which -->|No, framework tool object<br/>or non-native framework| Wrap[🛡️ Wrapper enforces<br/>per-run ThreadPoolExecutor]
    Core --> CoreTO{Within timeout?}
    Wrap --> WrapTO{Within timeout?}
    CoreTO -->|Yes| OK1[✅ result]
    CoreTO -->|No| Dict["{error: Tool timed out after Ns, timeout: true}"]
    WrapTO -->|Yes| OK2[✅ result]
    WrapTO -->|No| Err[raise ToolTimeoutError]

    classDef call fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef core fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef wrap fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef check fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef ok fill:#10B981,stroke:#7C90A0,color:#fff
    classDef error fill:#8B0000,stroke:#7C90A0,color:#fff

    class Call call
    class Which,CoreTO,WrapTO check
    class Core core
    class Wrap wrap
    class OK1,OK2 ok
    class Dict,Err error
```

### Timeout Return Shape

On timeout, each layer surfaces the timeout differently:

| Layer | Trigger | Behaviour |
| - | - | - |
| SDK Agent executor | `Agent(tool_config=ToolConfig(timeout=30))` in Python — **and now native plain callables under `framework: praisonai`**, which the wrapper forwards to core | `{"error": "Tool timed out after 30s", "timeout": True}` (seconds) |
| Wrapper boundary | Framework tool objects (CrewAI/LangChain `BaseTool`) and non-native frameworks (`crewai`, `autogen`, `ag2`) only. **Native plain callables under `framework: praisonai` now go through the SDK Agent executor row** — see [PR #5652](https://github.com/MervinPraison/PraisonAI/pull/5652). | **Raises `praisonai.agents_generator.ToolTimeoutError`** (a `TimeoutError` subclass) with `tool_name`, `timeout_seconds`, and `background_work_may_continue` (seconds). Framework adapters catch it and translate it per framework. See [Async Tool Safety → Wrapper-Level Timeout](/docs/features/async-tool-safety#wrapper-level-timeout-yaml-framework-praisonai). |
| SDK tool-call executor | `Agent(llm={"tool_timeout_ms": 30000})` | Returns `ToolResult` with `error=praisonaiagents.tools.ToolTimeoutError` and `error_kind="timeout"` (milliseconds). See [Tool Call Executor Timeout](/docs/features/tool-call-executor-timeout). |

<Note>
  On `framework: praisonai`, native plain-callable tools now delegate `tool_timeout` to core — a timeout returns core's structured dict, not the wrapper `ToolTimeoutError`. See [Tool Timeouts → Native tools now delegate tool\_timeout to core](/docs/features/tool-timeout#native-tools-now-delegate-tool_timeout-to-core-pr-5652) for the full story.
</Note>

### Effective Timeout Precedence

When `tool_timeout` values are declared, the wrapper resolves each agent's budget independently:

1. **CLI wins for every agent.** An explicit `--tool-timeout N` on the command line (or `cli_config={"tool_timeout": N}` when embedding) is used verbatim for all agents.
2. **Uniform declared values take the shared-wrap fast path.** When **every** agent under `roles:` and `agents:` declares `tool_timeout` **and** every declared value is identical, a single guard wraps the shared tool dict. If any agent omits the field, the shared wrap is skipped and the per-agent resolver runs instead.
3. **Heterogeneous per-agent values are honoured per agent.** When agents declare different values, each agent's tools carry its own budget (tool objects stay shared; only the guard closure differs). The tightest value no longer collapses onto every agent. Agents that omit `tool_timeout` get **no wrap** (they do not inherit another agent's declared value).
4. **Otherwise, no wrapping.** If nothing declares a timeout, tools run without wrapper-layer enforcement (the SDK executor-layer enforcement still applies if `tool_config=ToolConfig(timeout=…)` is set in Python).

The uniform path is `AgentsGenerator._resolve_uniform_tool_timeout(config)`; heterogeneous budgets use `make_agent_tool_wrap_resolver(config)` / `resolve_agent_tool_timeout(agent_key, config)` — see `praisonai/agents_generator.py`.

The **timeout pool size** (not the per-tool budget above) is per-run configurable via `tool_timeout_workers` on `AgentsGenerator` (or `cli_config["tool_timeout_workers"]`), falling back to `PRAISONAI_TOOL_TIMEOUT_WORKERS` (default `32`). See [Tool Configuration](/docs/configuration/tool-config#wrapper-level-tool-timeout-yaml-cli-defense-in-depth).

<Note>
  As of [PR #5228](https://github.com/MervinPraison/PraisonAI/pull/5228), the generator no longer stamps its internal `_tool_timeout_wrap` / `_agent_tool_wrap_resolver` closures onto a `cli_config` dict you pass in. Those are private per-run closures kept on `AgentsGenerator._run_ctx` and merged into the adapter's `cli_config` kwarg only at dispatch (`_dispatch_cli_config()`). Multi-tenant embedders can safely reuse a single `cli_config` template across runs — it will not come back holding instance-bound lambdas from a prior run.
</Note>

<Warning>
  As of [PR #4477](https://github.com/MervinPraison/PraisonAI/pull/4477), heterogeneous per-agent `tool_timeout` values are honoured per agent instead of collapsing to the tightest value. A slow agent that previously inherited a fast agent's tighter budget (masking a real timeout) may now run longer. Uniform declarations are unaffected. (Earlier, PR #3176 had reversed the collapse from `max()` to `min()`.) [PR #4468](https://github.com/MervinPraison/PraisonAI/pull/4468) follows up by tightening uniform detection: a single declared `tool_timeout` on one agent no longer counts as uniform — if any agent omits the field, undeclared agents get no wrap instead of silently inheriting the lone declared budget.
</Warning>

<Warning>
  YAML boolean values are ignored, not coerced. Because `bool` subclasses `int` in Python, `tool_timeout: yes` or `tool_timeout: true` used to silently become a 1-second cap on every tool. As of PR #2609 the resolver explicitly rejects `bool` values — such entries are treated as "not declared" and fall through to the next precedence level. Use an integer or float (e.g. `tool_timeout: 30`).
</Warning>

### Executor Details

* **One executor per `Agent` instance** (lazy creation)
* **`max_workers=2`** threads per agent
* **Thread name prefix:** `tool-<agent_name>` — useful for log filtering
* **Reused across calls** — no resource leak
* **Recycled on timeout** — a tool that hangs past `tool_timeout` is not reclaimable, so the executor is `shutdown(wait=False)` and the next call gets a fresh worker

<Note>
  **Self-healing after a hang.** The pool has 2 workers by default; before [PR #3960](https://github.com/MervinPraison/PraisonAI/pull/3960), two consecutive hangs would deadlock the pool because `future.cancel()` cannot stop a thread that has already started. Now the executor is recycled on timeout — the next call starts on a fresh worker — so a hung tool cannot progressively degrade throughput toward a deadlock. Recycling is bounded so repeated timeouts cannot leak an unbounded number of stuck threads.
</Note>

**Which timeout to choose:**

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
flowchart TD
    Start[Tool Type] --> IO{Network IO?}
    IO -->|Yes| Net[30-60s]
    IO -->|No| Local{Local computation?}
    Local -->|Yes| CPU[5-10s] 
    Local -->|No| None[No timeout needed]
    
    classDef decision fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef timeout fill:#189AB4,stroke:#7C90A0,color:#fff
    
    class IO,Local decision
    class Net,CPU,None timeout
```

***

## Common Patterns

### Limit FastAPI Route Concurrency

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

registry = ConcurrencyRegistry()
registry.set_limit("chat_agent", 5)

@app.post("/chat")
async def chat_endpoint(message: str):
    await registry.acquire("chat_agent")
    try:
        agent = Agent(name="chat_agent", instructions="Help users")
        response = await agent.astart(message)
        return {"response": response}
    finally:
        registry.release("chat_agent")
```

Calling `registry.set_limit("chat_agent", N)` again on a running server — a config reload or hot re-tune — is now correct even with requests in flight. Each in-flight request releases the exact semaphore it acquired, so the new cap takes effect for subsequent acquires without the old outstanding releases inflating it ([PR #5258](https://github.com/MervinPraison/PraisonAI/pull/5258)).

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
@app.post("/admin/retune")
async def retune(new_limit: int):
    registry.set_limit("chat_agent", new_limit)  # safe with permits outstanding
    return {"chat_agent_limit": new_limit}
```

### Async Context Manager Helper

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from contextlib import asynccontextmanager

@asynccontextmanager
async def throttled_agent(name: str, max_concurrent: int = 3):
    registry = ConcurrencyRegistry()
    registry.set_limit(name, max_concurrent)
    await registry.acquire(name)
    try:
        yield
    finally:
        registry.release(name)

# Usage
async with throttled_agent("researcher", 2):
    agent = Agent(name="researcher", instructions="Research topics")
    result = await agent.astart("Study quantum computing")
```

### Timeout Selection by Tool Type

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

def get_agent_with_timeouts():
    return Agent(
        name="MultiTool Assistant",
        instructions="Help with various tasks",
        tools=[
            "web_search",      # Network IO
            "file_processor",  # Local computation  
            "simple_math"      # Fast operation
        ],
        tool_config=ToolConfig(timeout=45)  # Good balance for mixed workload
    )
```

***

## Best Practices

<AccordionGroup>
  <Accordion title="Always release in finally blocks">
    Prevents deadlocks when exceptions occur:

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    registry.acquire_sync("agent")
    try:
        # Agent work here
        agent.start("task")
    finally:
        registry.release("agent")  # Always runs
    ```
  </Accordion>

  <Accordion title="Prefer async acquire in async code">
    Mixing is no longer an error — `acquire_sync()` in an async context simply blocks its thread. Sync and async callers contend for the **same** per-agent permit pool (see the `test_sync_and_async_share_one_pool` test in [PR #5050](https://github.com/MervinPraison/PraisonAI/pull/5050)). Still prefer `await acquire()` in async code so the event loop keeps running:

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    # ✅ Good - sync context, sync acquire
    def sync_handler():
        registry.acquire_sync("agent")
        try:
            agent.start("task")
        finally:
            registry.release("agent")

    # ✅ Good - async context, async acquire  
    async def async_handler():
        await registry.acquire("agent")  # yields to the loop
        try:
            await agent.astart("task")
        finally:
            registry.release("agent")
    ```
  </Accordion>

  <Accordion title="Set tool_timeout for network tools">
    Any tool that does network IO should have a timeout:

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

    # Tools that need timeouts
    network_tools = ["web_search", "api_call", "download_file"]
    local_tools = ["calculate", "format_text", "parse_json"]

    agent = Agent(
        name="Assistant",
        tools=network_tools + local_tools,
        tool_config=ToolConfig(timeout=30)  # Protects against slow network
    )
    ```
  </Accordion>

  <Accordion title="Use thread names for debugging">
    Filter logs by agent name using the thread prefix:

    ```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    # Filter tool execution logs by agent
    grep "tool-researcher" app.log

    # Or in Python logging
    import logging
    logging.basicConfig(format='%(threadName)s: %(message)s')
    ```
  </Accordion>
</AccordionGroup>

***

## Retries

Tool failures can be automatically retried using the retry policy feature. This works alongside timeouts to handle transient errors:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent, ExecutionConfig
from praisonaiagents.config import ToolConfig
from praisonaiagents.tools.retry import RetryPolicy

agent = Agent(
    name="resilient_agent",
    tools=[web_search, api_tool],
    tool_config=ToolConfig(
        timeout=30,
        retry_policy=RetryPolicy(
            max_attempts=3,
            retry_on={"timeout", "rate_limit", "connection_error"},
            backoff_factor=2.0,
            jitter=True
        )
    ),
    execution=ExecutionConfig(parallel_tool_calls=True),
)
```

<Note>
  `ToolConfig.parallel` is a **deprecated alias** for `ExecutionConfig.parallel_tool_calls`. Enable parallel tool calls with `execution=ExecutionConfig(parallel_tool_calls=True)` alongside `ToolConfig` for timeout and retry. Do not set both spellings to conflicting values \u2014 that raises `TypeError`.
</Note>

For complete retry configuration and error handling strategies, see [Tool Retry Policy](/docs/features/tool-retry-policy).

***

## Parallel tool calls inside one async turn

A single `astart(...)` turn can itself dispatch multiple independent tool calls concurrently when `parallel_tool_calls=True`.

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

agent = Agent(
    instructions="Fetch multiple independent sources and summarise.",
    tools=[fetch_weather, fetch_news, fetch_stocks],
    tool_config=ToolConfig(timeout=15),
    execution=ExecutionConfig(parallel_tool_calls=True),
)

# Before PR #4634 the three tools ran sequentially on astart() even though
# `parallel_tool_calls=True` was set (silently absorbed). Now they run
# concurrently via asyncio.gather inside the single async turn.
asyncio.run(agent.astart("Summarise conditions in New York."))
```

Two situations users conflate compose cleanly: `asyncio.gather(agent.astart(a), agent.astart(b))` runs **two agents** in parallel, while `parallel_tool_calls=True` runs **several tools inside one agent turn** in parallel.

***

## Write-conflict guard for shell-like tools

When `parallel_tool_calls=True` batches two or more tool calls in one turn, PraisonAI runs them sequentially instead of concurrently if any pair could touch the same file. As of PraisonAI [PR #4907](https://github.com/MervinPraison/PraisonAI/pull/4907), the guard also catches shell-like tools whose write target lives in a command string rather than a `path`/`file_path` argument — `execute_command`, `acp_execute_command`, and `execute_code`. Two calls to any of these in the same batch, or one of them alongside any other write, force sequential fallback. Path-only reads (`read_file`, list tools, etc.) still run concurrently.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent, ExecutionConfig
from praisonaiagents.tools.shell_tools import execute_command

agent = Agent(
    instructions="Write two lines to the same report file.",
    tools=[execute_command],
    execution=ExecutionConfig(parallel_tool_calls=True),
)

# Two concurrent `echo … > /tmp/report.txt` calls now run one after the
# other, protecting the file from a write-conflict race.
agent.start("Append 'A' then 'B' to /tmp/report.txt using execute_command.")
```

***

## Shared JSON state under concurrent writes

`BaseJSONStore`'s file lock now tolerates long critical sections without corruption. It judges a lock as stale by the lock file's mtime age and refreshes that mtime with a background heartbeat while held, so a legitimately slow read-modify-write (large JSON write, slow disk) stays exclusive instead of being reclaimed by an impatient waiter and clobbered. See [Thread Safety](/docs/features/thread-safety) for details ([PR #5136](https://github.com/MervinPraison/PraisonAI/pull/5136)).

***

## Related

<CardGroup cols={2}>
  <Card title="Tool Retry Policy" icon="rotate" href="/docs/features/tool-retry-policy">
    Automatically retry failed tool calls with exponential backoff
  </Card>

  <Card title="Tool Configuration" icon="wrench" href="/docs/configuration/tool-config">
    Tool timeout settings and performance tuning
  </Card>

  <Card title="Async Bridge" icon="arrows-left-right" href="/docs/features/async-bridge">
    Safe sync↔async boundary crossing utilities
  </Card>

  <Card title="Thread Safety" icon="lock" href="/docs/features/thread-safety">
    Chat history and state protection mechanisms
  </Card>
</CardGroup>


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