How It Works
Quick Start
1
Register a callback
2
Log tool calls
3
Async callback for dashboards
Callback Types
Register with
register_display_callback(type, fn, is_async=False). Alias: add_display_callback.
Streaming coverage
Since PR #4735,tool_call fires on all three execution paths — not just non-streaming.
Before #4735 the streaming paths ran tools silently — a streaming UI saw no tool activity even though a tool ran and its result reached the model. The callback now fires once per tool (parallel tools each get their own call), in both the success and failure branches.
As of PraisonAI PR #4819, the
llm_end callback (tokens_in, tokens_out, latency_ms) now fires on the Responses API streaming path (sync) too — previously only the non-streaming path emitted it. Token counters are accurate on that path now, not zero. See Token usage on the streaming path.As of PraisonAI PR #4887, the
llm_start and llm_end callbacks also fire on the async tool-loop (achat_completion_with_tools). Every iteration of a multi-step async tool loop emits paired lifecycle events with tokens_in, tokens_out, cost, and latency_ms — previously only the sync tool-loop emitted them, so async workloads produced no LLM spans and no cost figures. See Cost Tracking and Telemetry.Before PR #4735: you built a desktop UI that listens for
tool_call to render a ”🔧 Running search_web…” indicator. It worked in chat() mode. You switched to agent.start(prompt, stream=True) for token streaming and the indicator stopped appearing — no error, no warning, silent regression.After PR #4735 (closes Issue #4716): the same callback fires on the streaming path with the same kwargs. Your indicator works uniformly.tool_call kwargs
A tool_call handler receives these keyword arguments on every path. Accept **kwargs so future additions never break your handler.
elapsed_time on the custom-LLM path is populated only when the model calls a single tool in a batch. For parallel tool calls, per-tool timing is not available from the executor, so elapsed_time is None.success=False on error-shaped results. A durable run converts a raising tool into {"error": "..."} instead of re-raising inline. The callback detects that shape and reports success=False — so a failed tool is never labelled successful, even though it didn’t raise inline.Streaming example
tool_call fires the same way whether you stream or not.
Best-effort by design. A raising
tool_call handler is caught, logged at debug level, and swallowed — it can never break the stream. This is not the same as a tool failure: the real tool result (not your handler’s error text) still reaches the model.Configuration Options
Callbacks receive keyword arguments matching the event (for example
message, response, tool_name, agent_name). Extra kwargs are filtered to the function signature automatically.
Best Practices
Keep handlers lightweight
Keep handlers lightweight
Display callbacks run on the hot path. Log or enqueue work — avoid heavy I/O in sync handlers. The streaming path fires
tool_call synchronously inline, so a slow handler blocks token yielding until it returns.Use async for network and UI
Use async for network and UI
Set
is_async=True when writing to WebSockets, Slack, or dashboards so the agent loop stays responsive.Wrap custom logic in try/except
Wrap custom logic in try/except
A failing callback should not crash the agent. Catch, log, and return.
Compose with hooks for fine control
Compose with hooks for fine control
Use display callbacks for output formatting; use hooks when you need to allow, block, or mutate tool calls.
Related
Display System
TaskOutput, terminal rendering, and global registries
Callbacks
Broader callback patterns for agents and tasks
Streaming
tool_call fires on the streaming path too — tool telemetry for live UIs
