Skip to main content
Async & Background
Middleware
Intercept and modify model and tool calls with before/after hooks and wrap decorators
Middleware lets your agent intercept every LLM call and every tool call — log them, modify inputs, retry on failure, or block execution entirely.
The user adds hooks or wrap decorators; middleware runs on every model and tool call in the agent loop.
Data types available:
Inject a system prompt on every call:
Block a specific tool:
Cache expensive tool results:
Gate a tool through an external policy engine:
See HOL Guard Tool Policy for a complete fail-closed integration with an external security engine.
As of PraisonAI #4334 (2026-08-25), model middleware (
@before_model / @after_model / @wrap_model_call) now fires on every model call for both sync (agent.start, agent.chat) and async (agent.astart, agent.achat) paths. Prior to this fix only tool hooks were wired — model hooks registered but never ran. If you defined model hooks that “seemed to do nothing” before, they will now execute. Zero-overhead fast path preserved: agents with no model hooks bypass the middleware chain entirely.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
_chat_completion_with_retry/_achat_completion_with_retrybuild aModelRequest(withInvocationContext), pass it throughMiddlewareManager.execute_model_call, and unwrap the returnedModelResponseback to the agent’s expected return shape.- The retry/backoff logic is the final handler of the middleware chain — so if a hook wraps
call_next, it wraps the retry loop. before_model/wrap_model_callmutations torequest.messages,request.temperature, andrequest.toolsare threaded into the retry core.after_model/wrap_model_calledits toresponse.contentwin over the raw provider payload; unchanged content passes the original object through untouched.- The async path runs the sync middleware chain in an executor and bridges back to the async retry core via
asyncio.run_coroutine_threadsafe— hooks are written the same way for sync and async.
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
Short-circuit the model with a cached response: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 model calls (_chat_completion_with_retry / _achat_completion_with_retry) and tool calls (sync execute_tool and async execute_tool_async). Agents with no model hooks call the retry core directly, and the async path only spins up the thread-pool middleware worker when hooks are actually registered.Related
Hooks
Event-based hooks for agent lifecycle events
Callbacks
UI-focused callbacks for display and streaming events


