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

# Reliability Guarantees

> Safe defaults that keep long tool-using and multi-task runs going

<Note>
  This page covers three **default, automatic** correctness guarantees for tool-using and multi-task runs: malformed tool-call JSON recovery, tool-pair-safe window trimming, and same-batch async dependency ordering. For **task/workflow-level** reliability knobs (retry jitter, `workflow_timeout`, failure policies), see [Reliability](/docs/features/reliability).
</Note>

Long tool-using conversations and mixed async task batches used to fail in three subtle ways. Those failure classes are now handled automatically — no configuration, no new imports, no API changes.

<Note>
  **Behaviour changes (PraisonAI PR #4245).** Two silent defects are now fixed automatically by upgrading:

  * Tool-call parse failure is surfaced as a retryable `role="tool"` error across **all** execution paths (previously a wrong action could be dispatched and reported to the model as success on some paths).
  * `temperature` set in `Agent(llm={...})` now reaches the request (previously silently overridden by `1.0`). See [LLM Configuration](/docs/configuration/llm-config#core-llm-configuration).
</Note>

<Note>
  **Behaviour changes (PraisonAI PR #5121).** Two additional `chat_mixin.py` execution paths now surface the same retryable `role="tool"` error on a malformed tool-call `arguments` JSON, instead of silently dispatching with `{}` and reporting success:

  * Async OpenAI-native tool-completion path (`_achat_completion`)
  * Sync OpenAI-native streaming path (`_start_stream_impl`)

  `TypeError` on a non-text arguments value is now treated the same as malformed JSON (mirrors `llm.py`'s parser).
</Note>

<Note>
  **Behaviour changes (PraisonAI PR #4808).** Three silent defects at the LLM message layer are fixed automatically by upgrading:

  * A tool returning `[{"error": "..."}]` is now surfaced as an error on every path (previously the async secondary path dumped it as data).
  * The error sentence sent back to the model is the same on every path.
  * A tool returning a non-JSON-serializable value (`set`, `datetime`, `bytes`, custom class) no longer raises `TypeError` and kills the turn.
</Note>

<Note>
  **Behaviour changes (PraisonAI PR #5233).** Two silent data-correctness defects are fixed automatically by upgrading:

  * **Streaming tool serialization:** a streaming tool call that succeeds but returns a non-JSON-serializable value (`datetime`, `set`, `bytes`, DataFrame, custom class) is no longer mislabelled `"Error executing function: ..."` to the model. It now stringifies into `{"result": "..."}` — parity with the sync/async loops' `except (TypeError, ValueError)` fallback. Real tool execution failures still surface as `{"error": str(e)}` on the streaming path.
  * **Concurrent token/cost attribution on a shared LLM:** two `Agent`s sharing one `LLM` instance (`Agent(llm=shared_llm)`) executed concurrently via `PraisonAIAgents.arun_all_tasks` (which dispatches with `asyncio.gather`) no longer swap each other's `current_agent_name` mid-await. `LLM.current_agent_name` is now backed by a per-instance `contextvars.ContextVar` — each gathered task keeps its own value across the `await`, so tokens are attributed to the agent that actually spent them. See [Thread-Safe Agent State — Concurrent token/cost attribution on a shared LLM](/docs/features/thread-safety#concurrent-tokencost-attribution-on-a-shared-llm-praisonai-5233).
</Note>

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

researcher = Agent(name="Researcher", instructions="Research the topic")
writer = Agent(name="Writer", instructions="Write based on the research")

research = Task(
    name="research",
    description="Research quantum computing",
    agent=researcher,
    async_execution=True,
)

article = Task(
    name="article",
    description="Write an article using the research",
    agent=writer,
    async_execution=True,
    context=[research],
)

asyncio.run(
    PraisonAIAgents(agents=[researcher, writer], tasks=[research, article]).astart()
)
```

The `article` task depends on `research`. Even when both run async in the same batch, the dependency is awaited first — `article` never sees an empty result.

<Note>
  The dependency-ordering guarantee applies on both entry points. Use `astart()` (shown here) if you want the tasks to actually run in parallel — the sync `.start()` runs them sequentially and logs a warning ([PraisonAI PR #5227](https://github.com/MervinPraison/PraisonAI/pull/5227)).
</Note>

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    subgraph "Reliability Guarantees"
        A[Malformed tool JSON] --> B[Reported to model as role=tool error]
        C[Window trim] --> D[Tool-call ↔ tool-result pair preserved]
        E[Async batch dependency] --> F[Dependency flushed before dependent]
        G[Async turn failure] --> H[chat_history rolled back to pre-turn snapshot]
        I[Tool returns non-serializable value] --> J[Fed to model as str value on every path incl. streaming, turn continues]
        K[Tool returns list-of-errors] --> L[Surfaced as role=tool error on every path]
    end

    classDef failure fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef recovery fill:#189AB4,stroke:#7C90A0,color:#fff

    class A,C,E,G,I,K failure
    class B,D,F,H,J,L recovery
```

**Async turn failure** → `achat()`/`astart()` roll back the pre-turn `chat_history` snapshot, matching the sync path. (Since PR #4739 — see [Async Agents](/docs/features/async#failed-turns-leave-chat-history-clean).)

## Quick Start

<Steps>
  <Step title="Tool-using agent survives a malformed tool call">
    If the model emits a tool call whose `arguments` JSON is truncated or malformed, the parse failure is reported back to the model as a `role="tool"` error and the run continues. Nothing to enable.

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

    def search_web(query: str) -> str:
        """Search the web for a query."""
        return f"Results for {query}"

    agent = Agent(
        name="Researcher",
        instructions="Answer using the search tool",
        tools=[search_web],
    )

    agent.start("Find recent papers on quantum error correction")
    ```
  </Step>

  <Step title="A Session that survives long tool-using conversations">
    A windowed `Session` never emits a transcript that starts on an orphaned `role="tool"` message, so strict providers keep accepting it after many tool turns.

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

    session = Session(session_id="research_chat", user_id="user_1")
    agent = session.Agent(name="Assistant", role="Research helper")

    agent.start("Summarise today's findings")
    session.save_state({"topic": "quantum computing"})
    ```
  </Step>

  <Step title="Dependent async tasks keep their context">
    Two `async_execution=True` tasks in the same batch, where the second depends on the first via `context=[...]`, run in the correct order automatically. The ordering guarantee applies on both entry points; use `astart()` if you want the tasks to actually run in parallel.

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

    researcher = Agent(name="Researcher", instructions="Research the topic")
    writer = Agent(name="Writer", instructions="Write based on the research")

    research = Task(
        name="research",
        description="Research quantum computing",
        agent=researcher,
        async_execution=True,
    )

    article = Task(
        name="article",
        description="Write an article using the research",
        agent=writer,
        async_execution=True,
        context=[research],
    )

    asyncio.run(
        PraisonAIAgents(agents=[researcher, writer], tasks=[research, article]).astart()
    )
    ```

    <Note>
      The sync `.start()` / `.run()` preserves the same dependency ordering but runs `async_execution=True` tasks sequentially and logs a warning — switch to `astart()` / `arun()` for parallel fan-out ([PraisonAI PR #5227](https://github.com/MervinPraison/PraisonAI/pull/5227)).
    </Note>
  </Step>

  <Step title="A tool returning a non-serializable value doesn't crash the turn">
    A tool returning a `datetime`, `set`, `bytes`, or custom object is fed to the model as `str(value)` on every path — the run continues instead of raising `TypeError`.

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

    def current_time() -> datetime:
        """Return the current time."""
        return datetime.now()  # not JSON-serializable

    agent = Agent(
        name="Clock",
        instructions="Tell the user the current time using the tool.",
        tools=[current_time],
    )

    agent.start("What time is it?")
    ```
  </Step>
</Steps>

***

## How It Works

### Malformed tool-call JSON

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

    User->>Agent: Request
    Agent->>LLM: Tool schema
    LLM-->>Agent: Tool call with truncated / invalid JSON arguments
    Note over Agent: Sentinel returned (NOT {})
    Agent->>LLM: role=tool error — "arguments could not be parsed; tool was NOT executed"
    LLM-->>Agent: Corrected tool call with valid JSON
    Agent-->>User: Answer
```

All seven execution paths — five in `llm.py` (sync sequential, async sequential, streaming batch, non-streaming batch, and async Responses-API loop) plus two in `chat_mixin.py` (async OpenAI-native tool-completion, sync OpenAI-native streaming) — now surface a `role="tool"` error message when a tool call's `arguments` JSON cannot be parsed, and skip the dispatch instead of calling the tool with wrong arguments.

The exact message the model sees:

```
Error: arguments for tool '<function_name>' could not be parsed (the argument string
was invalid or truncated). The tool was NOT executed. Please re-emit the tool call
with complete, valid JSON arguments.
```

A parse failure returns a distinct internal sentinel that is kept separate from `{}`. Legitimately empty arguments (`{}`) still dispatch the tool as normal — only genuinely unparseable JSON surfaces as an error.

| Execution path | Coverage |
| - | - |
| Sync sequential (`llm.py`) | Reports `role="tool"` error, skips dispatch |
| Async sequential (`llm.py`) | Reports `role="tool"` error, skips dispatch |
| Streaming batch (`llm.py::get_response_stream`) | Reports `role="tool"` error, skips dispatch |
| Non-streaming batch (`llm.py::get_response`) | Reports `role="tool"` error, skips dispatch |
| Async Responses-API loop (`llm.py::get_response_async`) | Reports `role="tool"` error, skips dispatch |
| **Async OpenAI-native tool-completion (`chat_mixin.py::_achat_completion`)** | **Reports `role="tool"` error, skips dispatch (PR #5121)** |
| **Sync OpenAI-native streaming (`chat_mixin.py::_start_stream_impl`)** | **Reports `role="tool"` error, skips dispatch (PR #5121)** |

The sentinel `_TOOL_ARGUMENTS_PARSE_FAILED` and the helpers `tool_arguments_parse_failed` / `tool_parse_error_message` are defined in `agent/tool_execution.py` and reused by `llm/llm.py` via a shared import, so the sentinel is a single object identity across both dispatch layers (an `is` check only works against the same object).

### Uniform tool-result message

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

    Agent->>Formatter: Tool result (error, list-error, or non-serializable value)
    Note over Formatter: One formatter for every non-Ollama path
    Formatter-->>Agent: Same error sentence / str(value) fallback
    Agent->>LLM: role=tool message
    LLM-->>Agent: Answer
```

Every non-Ollama path now routes tool results through one formatter, so the same tool value produces the same message regardless of which internal branch handled it. Previously five inline copies had drifted apart:

| copy in `llm.py` | error sentence used | list-of-errors handled | `json.dumps` guarded |
| - | - | - | - |
| sync sequential | `"...inform the user."` (shorter) | yes | no |
| sync batch (non-Ollama) | `"...could not be completed"` (fuller) | yes | no |
| async secondary | `"...inform the user."` (shorter) | **no — dumped as data** | no |
| async batch (non-Ollama) | `"...could not be completed"` (fuller) | yes | no |
| streaming | `"...could not be completed"` (fuller) | yes | **yes** |

The uniform behaviour is:

* A tool returning `[{"error": "..."}]` is **always** surfaced as an error to the model.
* The error sentence is the same everywhere: `Error: <msg>. Please inform the user that the operation could not be completed.`
* A non-JSON-serializable value (`set`, `datetime`, `bytes`, DataFrame, custom class) falls back to a stringified result instead of raising `TypeError` — on **every** path, including streaming (since PR #5233).

| Execution path | Coverage |
| - | - |
| Sync sequential | Same message; list-error surfaced; non-serializable safe |
| Sync batch (non-Ollama) | Same message; list-error surfaced; non-serializable safe |
| Async sequential | Same message; **list-error now surfaced** (previously dumped as data); non-serializable safe |
| Async batch (non-Ollama) | Same message; list-error surfaced; non-serializable safe |
| Streaming | Non-serializable safe (PR #5233); real execution failures still surfaced as `{"error": ...}` |
| Ollama (`OllamaAdapter`) | Intentional divergence: `role: "user"`, natural-language shape |

The Ollama path keeps its natural-language `role: "user"` shape — the one genuine divergence, unchanged.

Before PR #5233 the streaming path did **not** have this parity. `chat_completion_with_tools_stream` wrapped tool *execution* and JSON *serialization* in one `try` with a single generic `except Exception`, so a tool that **succeeded** but returned a `datetime`, `set`, `bytes`, DataFrame, or custom object was reported to the model as `"Error executing function: <TypeError…>"`. PR #5233 splits execution and serialization into two `try` blocks: the serialization block uses the same `except (TypeError, ValueError)` fallback the sync/async loops already used and stringifies into `{"result": "..."}`. A tool that genuinely raises still surfaces as `{"error": str(e)}` — the split does not swallow real failures.

### Tool-pair-safe window trim

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant Store
    participant Transcript

    Store->>Transcript: Trim to active window
    Note over Transcript: Naive slice could start on role=tool
    Store->>Transcript: Preserve tool-call ↔ tool-result pairs
    Transcript-->>Store: Retained tail never starts on an orphaned tool message
```

Windowed retention in `DefaultSessionStore` routes its slice through a tool-pair-safe guard, and the `TruncateOptimizer` / `SlidingWindowOptimizer` context strategies skip any leading orphaned `role="tool"` messages after trimming. Strict providers (OpenAI, Anthropic) reject a transcript that opens on a tool result whose originating assistant `tool_calls` message was trimmed away — that shape is no longer emitted.

### Same-batch async dependency ordering

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant Runner
    participant TaskA as Async Task A
    participant TaskB as Async Task B (depends on A)

    Runner->>TaskA: Queue in batch
    Note over Runner: TaskB depends on A and A is still pending
    Runner->>TaskA: Flush batch first
    TaskA-->>Runner: Result
    Runner->>TaskB: Queue with A's context filled in
    TaskB-->>Runner: Correct output
```

`arun_all_tasks` tracks pending `(task_id, coroutine)` pairs. Before queuing an async task whose dependency is still pending in the current batch, it flushes the batch so the dependency finishes first. If the just-flushed dependency ended up `failed`, the failure cascades to the dependent instead of running it with missing upstream context. Both dependency edges are covered: `task.context` and workflow `previous_tasks` (from `next_tasks`).

### Sub-agent transcripts can't clobber a user's session

`session.close()` on a session with `session.Agent(...)` sub-agents cannot overwrite an unrelated user's session, even when a sub-agent's name would sanitise to the same filename. Sub-agent transcripts are stored inside the parent's own record under `metadata["agent_histories"]`, so the old collision (PraisonAI issue #4120) is unrepresentable (PraisonAI PR #4126). See [Session Store](/docs/features/session-store) for the storage layout.

***

## Crash-safe persistence

File-backed state is written atomically (PR [#5357](https://github.com/MervinPraison/PraisonAI/pull/5357)): the value is serialised first, written to a temp file in the same directory, `fsync`-ed, then swapped in with `os.replace`. A crash, OOM kill, or a non-serialisable value cannot truncate or corrupt the previous good file — the last-known-good state always survives.

| State | Guarantee |
| - | - |
| Permission rules & approvals | Atomic write — a downgraded `deny` rule after restart is impossible |
| Workflow checkpoints | Atomic write — a crash mid-write cannot force a restart from step 0 |
| Plans & todos | Atomic write |
| Cost reports | Atomic write |
| Async JSON storage (`BaseJSONStorage.save_async`) | Serialise-first, then temp file + `os.replace` |

<Note>
  No new imports or flags — the guarantee applies automatically to the built-in file-backed stores. See [Permissions](/docs/features/permissions#crash-safe-rule-persistence), [Workflow Checkpoint Resume](/docs/features/workflow-checkpoint-resume), and [JSON Persistence](/docs/features/persistence-json).
</Note>

***

## Configuration Options

No configuration required — these are default, automatic behaviours. There are no new flags, no new config options, and no import changes. Upgrading is enough.

***

## Best Practices

<AccordionGroup>
  <Accordion title="Prefer role=tool error surfaces over try/except around agent.start()">
    You do not need to wrap `agent.start(...)` in `try/except` to guard against malformed tool-call output. The parse failure is already reported back to the model as a `role="tool"` error, giving it a turn to self-correct. This guarantee applies uniformly across sync, async, streaming, and Responses-API code paths.
  </Accordion>

  <Accordion title="Set a Session retention window without worrying about tool-message boundaries">
    Bounded retention preserves tool-call ↔ tool-result pairs automatically. The retained tail never starts on an orphaned tool message, so you can cap history freely without provider-side "invalid conversation" errors.
  </Accordion>

  <Accordion title="Give dependent async tasks their context=[...]">
    Declare dependencies with `context=[task]` (or a workflow `next_tasks` edge). Batch ordering is preserved for you — the dependency is flushed before the dependent runs, so the dependent always sees real upstream output.
  </Accordion>

  <Accordion title="You don't need to make tool return values JSON-serializable for reliability">
    A tool returning `datetime`, `set`, `bytes`, or a custom object is fed to the model as `str(value)` and the turn continues. Serialization is still recommended for readability, but it is no longer a correctness requirement.
  </Accordion>
</AccordionGroup>

***

## Related

<CardGroup cols={2}>
  <Card title="Reliability" icon="shield-halved" href="/docs/features/reliability">
    Retry jitter, workflow timeouts, and task failure policies
  </Card>

  <Card title="Tasks" icon="list-checks" href="/docs/concepts/tasks">
    Task definitions, dependencies, and async execution
  </Card>

  <Card title="Tools" icon="wrench" href="/docs/concepts/tools">
    Define and call tools from agents
  </Card>

  <Card title="Sessions" icon="database" href="/docs/concepts/session-management">
    Persistent state and windowed conversation history
  </Card>
</CardGroup>


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