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 Later, call
defer(...) and the model sees the note immediately — no blocking on a 10-minute render.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.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
Callresolve_deferred(handle_id, value) when the background job finishes — the value is re-injected into the same conversation as a tool response.
A tool returns defer(...) immediately; a background thread calls resolve_deferred(...) with the same handle_id when it is done.
resolve_deferred(...) from the completion callback — no LLM wiring needed.
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.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
Anasync def tool is awaited natively — no asyncio.run wrapper needed.
Deferred job resolved by handle
Returndefer(...) now, then resolve the job later by its handle_id.
Structured error inspection
Readstructured_error to get the error type and message instead of a flattened string.
Best Practices
Backward compatible by default
Backward compatible by default
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.Never let the UI break your tool
Never let the UI break your tool
The executor swallows exceptions raised by an
on_progress callback and keeps the tool running. A broken UI channel never kills a tool call.Stamp your own source only when proxying
Stamp your own source only when proxying
The executor stamps
tool_call_id and function_name automatically. Only set them yourself when you relay updates from another tool.Prefer defer() over blocking on external systems
Prefer defer() over blocking on external systems
Job queues, video renders, and batch pipelines belong behind a
defer() handle so the turn continues while the work runs.Same behaviour on both LLM backends
Same behaviour on both LLM backends
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.Related
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

