Skip to main content
Slow tools can stream progress or hand back a “resolve later” handle so a single long-running call never freezes the whole turn.

Quick Start

1

Stream progress from a tool

Add an on_progress=None parameter. The executor detects it and wires updates through automatically — no other changes needed.
2

Defer a long-running job

Return defer(...) and the model sees the note immediately — no blocking on a 10-minute render.
Later, call resolve_deferred(job_id, video_url) from the render-complete callback to deliver the result — see Resolving a Deferred Result.

How It Works

Deferred tools behave the same on the LiteLLM path and the native OpenAI-SDK path. The run loop registers your handle on the shared resolver and re-injects the resolved value into the agent’s durable chat_history — no per-provider wiring needed. Under an Agent, injection goes through the Agent’s thread-safe _append_to_chat_history (wired automatically by _wire_deferred_history_callback, which fires on lazy LLM construction, direct agent.llm_instance = ... assignment, and agent.switch_model(...)). As of PR #5589, a standalone LLM keeps no history — LLM.chat_history is removed and the fallback is a no-op.
The executor inspects each tool’s signature, forwards progress it emits, and records a deferred handle without blocking.

Which return type do I choose?

Pick the simplest option that fits how long your tool runs.

Configuration Options

ToolProgress describes a single incremental update a tool emits while working. DeferredToolResult is a handle a tool returns when it kicks off background work. The defer() factory builds a DeferredToolResult; handle_id defaults to a generated uuid.uuid4().hex when omitted. The enriched ToolResult carries these extra fields alongside result. The praisonaiagents.tools public deferred-resolver API delivers a background result back into the conversation once the work finishes. Native OpenAI-SDK parity is available as of the PraisonAI release that includes the #3967 fix and later.

Resolving a Deferred Result

Call resolve_deferred(handle_id, value) when the background job finishes — the value is re-injected into the same conversation as a tool response.
Since PraisonAI PR #4739, Agent-driven deferred results now reach agent.chat_history on every path — lazy _ensure_llm_instance() construction, direct agent.llm_instance = ... assignment, and agent.switch_model(...) model swaps. Before #4739 the resolved value landed on LLM.chat_history, a list the Agent run loop never replays, so the next turn never saw it. No API change — existing deferred code benefits automatically.As of PR #5589, LLM.chat_history is removed entirely — the LLM layer is stateless with respect to conversation history. For an Agent, the resolved value still lands on agent.chat_history via the Agent’s thread-safe _agent_history_append callback; for a raw standalone LLM, the fallback is now a no-op.
A tool returns defer(...) immediately; a background thread calls resolve_deferred(...) with the same handle_id when it is done.
The run loop registers the handle automatically on both the LiteLLM path and the native OpenAI-SDK path, so tool authors only call resolve_deferred(...) from the completion callback — no LLM wiring needed. Inspect agent.chat_history to confirm the resolved value landed on the durable transcript:
The two-turn cycle: turn 1 the tool returns defer(...); a background thread calls resolve_deferred(...) while the user is idle; turn 2 the user asks a follow-up and the agent replies using the resolved value now present in agent.chat_history.
Resolving a value before the run loop registers is safe — the value is buffered and delivered on the next register() call. Nothing is silently dropped. See the regression test test_early_resolution_buffering for provenance.

Deferred results under switch_model()

Swapping the model mid-conversation keeps deferred resolution wired — switch_model(...) re-attaches the Agent’s history callback to the new LLM instance.
For an Agent, inspect agent.chat_history — the deferred value is appended via the Agent’s locked, bounded history callback. For a standalone LLM, no history is kept (llm.chat_history no longer exists as of PR #5589); if your test asserts on it, update it to assert on the returned value or on an Agent-owned history list instead.
Test migration (PR #5589). tests/unit/llm/test_tool_loop_step_limit.py now asserts on the returned value instead of llm.chat_history[-1]["content"]. If any of your structured-output tests assert on llm.chat_history, migrate them the same way — assert on the returned value, or on an Agent-owned history list.

When does the resolver fire?

The resolver handles the job finishing after registration, before registration (buffered), or being cancelled. The same sequence applies whether the agent runs on the LiteLLM loop or the native OpenAI-SDK loop.

Common Patterns

Async tool with progress

An async def tool is awaited natively — no asyncio.run wrapper needed.

Deferred job resolved by handle

Return defer(...) now, then resolve the job later by its handle_id.

Structured error inspection

Read structured_error to get the error type and message instead of a flattened string.

Best Practices

Tools without an on_progress parameter are called the old way. No change is needed unless you want progress — the executor auto-detects the parameter via inspect.signature.
The executor swallows exceptions raised by an on_progress callback and keeps the tool running. A broken UI channel never kills a tool call.
The executor stamps tool_call_id and function_name automatically. Only set them yourself when you relay updates from another tool.
Job queues, video renders, and batch pipelines belong behind a defer() handle so the turn continues while the work runs.
defer() and resolve_deferred() work the same on the LiteLLM loop and the native OpenAI-SDK loop. Background jobs that finish after the current turn returns still land in the agent’s chat_history and reach the next turn.

Tool Progress Streaming

Event/sink-based progress via emit_tool_progress()

Async Tool Safety

Rules for async tools inside sync flows

Structured LLM Errors

How structured errors surface to the model

Custom Tools

Building your own tools