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

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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],
)

PraisonAIAgents(agents=[researcher, writer], tasks=[research, article]).start()
```

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.

```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), 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.

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    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],
    )

    PraisonAIAgents(agents=[researcher, writer], tasks=[research, article]).start()
    ```
  </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 five execution paths (sync sequential, async sequential, streaming batch, non-streaming batch, and async Responses-API loop) 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                                 | Reports `role="tool"` error, skips dispatch |
| Async sequential                                | Reports `role="tool"` error, skips dispatch |
| Streaming batch (`get_response_stream`)         | Reports `role="tool"` error, skips dispatch |
| Non-streaming batch (`get_response`)            | Reports `role="tool"` error, skips dispatch |
| Async Responses-API loop (`get_response_async`) | Reports `role="tool"` error, skips dispatch |

### 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`, custom class) falls back to `str(result)` instead of raising `TypeError`.

| 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                | Unchanged behaviour — already the reference implementation                                   |
| Ollama (`OllamaAdapter`) | Intentional divergence: `role: "user"`, natural-language shape                               |

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

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

***

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