Skip to content

Tool middleware#

Tool middleware lets you wrap one function tool with async hooks that run immediately around its execution—the same idea as an agent's on_tool_execution() Middleware, but attached at tool definition time with plain callables (no BaseMiddleware subclass).

Use this pattern when behavior is specific to a single tool. For policies that apply to every tool on an agent, register BaseMiddleware with on_tool_execution() instead.

Why use it#

Tool-scoped hooks are optional. They help when:

  • Colocation — Validation, redaction, or metrics for one tool live next to that implementation instead of in shared agent middleware.
  • Clear contracts — Libraries can ship a tool with hooks that always run (normalize arguments, scrub secrets) without requiring consumers to register matching agent middleware.
  • Simpler agents — You avoid a large on_tool_execution full of if event.name == ... when only a few tools need special handling.

Use agent middleware=[...] when the policy is global or shared across most tools. Use middleware=[...] on @tool, @agent.tool, or @toolkit.tool when the behavior belongs to that tool only.

Typical cases#

Scenario Reason to use tool-scoped hooks
Normalize or validate arguments for one function Only that tool’s schema needs the transform; keeps agent middleware small.
Redact or reshape results before they return to the model Per-tool privacy or formatting (for example strip internal IDs).
Light auditing or metrics for a sensitive action The hook is bundled with the tool so it is hard to forget at agent setup.
Retry or fallback tied to one integration Failure handling stays next to the API client without naming the tool in global middleware.
Approve or reject before the tool body runs Gate dangerous or irreversible tools on policy, session flags, or a human decision without scattering checks inside the implementation.

API#

The public type alias is ToolMiddleware in autogen.beta.middleware. A hook is an async callable with the same parameters as BaseMiddleware.on_tool_execution, except the first argument is the inner ToolExecution (the next step in the chain), not self.

  • Pass middleware=[hook, ...] to @tool, Agent.tool, or Toolkit.tool.
  • Multiple hooks use the same nesting as agent tool middleware: the first entry in the list is the outermost layer around the tool body.
  • If the agent also registers BaseMiddleware with on_tool_execution, agent middleware runs outside tool-scoped hooks (it sees the full execution, including hooks).
from typing import Annotated

from autogen.beta import Agent, Context, tool, Variable
from autogen.beta.config import OpenAIConfig
from autogen.beta.events import ToolCallEvent, ToolResultEvent
from autogen.beta.middleware import ToolExecution

async def add_request_id(
    call_next: ToolExecution,
    event: ToolCallEvent,
    context: Context,
) -> ToolResultEvent:
    context.variables.setdefault("request_id", "unknown")
    return await call_next(event, context)

@tool(middleware=[add_request_id])
def search(query: str, request_id: Annotated[str, Variable()]) -> str:
    """Runs a search. request_id may be injected by middleware."""
    return f"results-for-{query}-{request_id}"

agent = Agent("assistant", config=OpenAIConfig("gpt-4o-mini"))

Note

Tool-scoped hooks are plain callables. They do not use the Middleware(...) factory or BaseMiddleware.