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.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). temperatureset inAgent(llm={...})now reaches the request (previously silently overridden by1.0). See LLM Configuration.
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).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 raisesTypeErrorand kills the turn.
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
Agents sharing oneLLMinstance (Agent(llm=shared_llm)) executed concurrently viaPraisonAIAgents.arun_all_tasks(which dispatches withasyncio.gather) no longer swap each other’scurrent_agent_namemid-await.LLM.current_agent_nameis now backed by a per-instancecontextvars.ContextVar— each gathered task keeps its own value across theawait, so tokens are attributed to the agent that actually spent them. See Thread-Safe Agent State — Concurrent token/cost attribution on a shared LLM.
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.
Async turn failure → achat()/astart() roll back the pre-turn chat_history snapshot, matching the sync path. (Since PR #4739 — see Async Agents.)
Quick Start
1
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.2
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.3
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.4
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.How It Works
Malformed tool-call JSON
All seven execution paths — five inllm.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:
{}. Legitimately empty arguments ({}) still dispatch the tool as normal — only genuinely unparseable JSON surfaces as an error.
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
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:
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 raisingTypeError— on every path, including streaming (since PR #5233).
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
Windowed retention inDefaultSessionStore 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
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 for the storage layout.
Crash-safe persistence
File-backed state is written atomically (PR #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.
No new imports or flags — the guarantee applies automatically to the built-in file-backed stores. See Permissions, Workflow Checkpoint Resume, and JSON Persistence.
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
Prefer role=tool error surfaces over try/except around agent.start()
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.Set a Session retention window without worrying about tool-message boundaries
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.
Give dependent async tasks their context=[...]
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.You don't need to make tool return values JSON-serializable for reliability
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.Related
Reliability
Retry jitter, workflow timeouts, and task failure policies
Tasks
Task definitions, dependencies, and async execution
Tools
Define and call tools from agents
Sessions
Persistent state and windowed conversation history

