As of PraisonAI #3086 (2026-07-16), tool middleware (
before_tool / after_tool / wrap_tool_call) gates async tool calls as well — the same hooks fire around agent.astart(...) and execute_tool_async(...). No API change; existing hooks work unmodified.Quick Start
1
Add a logging middleware
2
Wrap calls for retry logic
3
Async parity
The same hooks gate async tool calls — write them once and they fire for both Under the hood the synchronous
agent.start(...) and agent.astart(...).wrap_tool_call chain runs in a thread-pool worker and the actual async tool execution is scheduled back on the event loop — so you write hooks the same way for sync and async.How It Works
Execution order for a model call:- All
before_modelhooks run in registration order wrap_model_callmiddleware chain wraps the actual LLM call- The real LLM call executes
- All
after_modelhooks run in reverse registration order
before_tool, wrap_tool_call, and after_tool — including async tool calls:
Configuration Options
Pass middleware as a flat list toAgent(hooks=[...]). Mix decorator types freely.
Middleware API Reference
TypeScript middleware configuration
Hooks Rust Reference
Rust hooks configuration
Common Patterns
Inject a system prompt on every call:Best Practices
Always return the request/response object
Always return the request/response object
Every
before_* and after_* hook must return the (possibly modified) object. Returning None breaks the chain and raises a runtime error.Use wrap_* for retry and circuit-breaking
Use wrap_* for retry and circuit-breaking
wrap_model_call and wrap_tool_call give you full control over whether call_next is called — perfect for retries, timeouts, and feature flags. before_*/after_* hooks cannot short-circuit the call.Keep hooks stateless or use thread-local state
Keep hooks stateless or use thread-local state
Agents can run concurrently. Avoid mutable global state in hooks. If you need per-request state, use
request.context.metadata or Python’s contextvars.Zero overhead when hooks are empty
Zero overhead when hooks are empty
The middleware manager checks for registered hooks before executing any code. When
hooks=[] (the default), the fast path skips all hook processing entirely — for both sync (execute_tool) and async (execute_tool_async) tool calls. The async path only spins up the thread-pool middleware worker when tool-level hooks are actually registered.Related
Hooks
Event-based hooks for agent lifecycle events
Callbacks
UI-focused callbacks for display and streaming events

