Install
$ agentstack add skill-ag2ai-ag2-skills-ag2-middleware ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
Middleware
When to use
Middleware is for cross-cutting behaviour that should apply consistently across many runs without changing the agent, model client, or tools themselves. Common use cases:
- Logging, tracing, timing
- Retry on transient failures
- Trim history before it reaches the model
- Cap or estimate token usage
- Rewrite tool arguments / results
- Enforce policies before a tool runs
- Audit human-input requests
Four hooks
BaseMiddleware exposes four async hooks. Implement only the ones you need:
| Hook | Wraps | Use for | |---|---|---| | on_turn(call_next, event, context) → ModelResponse | The whole agent turn | Total latency, request/response inspection, turn-level policies | | on_llm_call(call_next, events, context) → ModelResponse | Each LLM API call | Retry, logging, history trim, request mutation, caching | | on_tool_execution(call_next, event, context) → ToolResultType | Each tool invocation | Validate args, redact results, fallback on failure, access control | | on_human_input(call_next, event, context) → HumanMessage | Each context.input() | Audit, rewrite prompts, automated short-circuit, rate limit |
Each instance is created once per turn and can hold per-turn state on self. The same instance can implement multiple hooks.
Built-in middleware
Importable from ag2.middleware:
| Middleware | Purpose | Constructor | |---|---|---| | LoggingMiddleware | Logs turn start/end, each LLM call, each tool execution | no args | | RetryMiddleware | Retries failed LLM calls | max_retries=N, retry_on=ExceptionClass | | HistoryLimiter | Cap event count before LLM call | max_events=N | | TokenLimiter | Char-based token-budget cap before LLM call | max_tokens=N, chars_per_token=4 | | TelemetryMiddleware | OpenTelemetry GenAI spans (see ag2-telemetry) | see telemetry skill |
Registration — agent-level
Apply to every turn:
from ag2 import Agent
from ag2.config import OpenAIConfig
from ag2.middleware import LoggingMiddleware, RetryMiddleware
agent = Agent(
"assistant",
config=OpenAIConfig(model="gpt-4o-mini"),
middleware=[
LoggingMiddleware(),
RetryMiddleware(max_retries=2),
],
)
Registration — call-level
Add temporary middleware for one turn. Both agent.ask(...) and reply.ask(...) accept it:
from ag2.middleware import TokenLimiter
reply = await agent.ask("Summarise the latest messages.", middleware=[LoggingMiddleware()])
next_turn = await reply.ask("Now answer in one paragraph.", middleware=[TokenLimiter(max_tokens=4000)])
Call-level middleware is appended after the agent's middleware list.
Ordering
Middleware runs in registration order, like nested with blocks. Registering [A, B, C] enters A → B → C and unwinds C → B → A:
enter A
enter B
enter C
exit C
exit B
exit A
This matters when you mix logging, mutation, retry. If RetryMiddleware should retry mutated requests, mutation goes inside retry; if you want each retry attempt logged separately, logging goes inside retry.
Writing your own
Subclass BaseMiddleware, implement the hooks you need:
import logging
from collections.abc import Sequence
from ag2 import Agent, Context
from ag2.config import OpenAIConfig
from ag2.events import BaseEvent, ModelResponse, ToolCallEvent
from ag2.middleware import BaseMiddleware, LLMCall, Middleware, ToolExecution
class AuditMiddleware(BaseMiddleware):
def __init__(self, event: BaseEvent, context: Context, logger: logging.Logger) -> None:
super().__init__(event, context)
self.logger = logger
async def on_llm_call(self, call_next: LLMCall, events: Sequence[BaseEvent], context: Context) -> ModelResponse:
self.logger.info("Calling model with %d events", len(events))
response = await call_next(events, context)
self.logger.info("Model returned: %s", response)
return response
async def on_tool_execution(self, call_next: ToolExecution, event: ToolCallEvent, context: Context):
self.logger.info("Executing tool: %s", event.name)
return await call_next(event, context)
agent = Agent(
"assistant",
config=OpenAIConfig(model="gpt-4o-mini"),
middleware=[
Middleware(AuditMiddleware, logger=logging.getLogger("ag2.audit")),
],
)
If your middleware needs constructor args beyond event and context, wrap with Middleware(YourClass, ...) when registering. Zero-config middleware can be passed bare (middleware=[LoggingMiddleware()]).
Tool-scoped vs agent-scoped
For behaviour that applies to one tool only (validation, redaction for that tool's output, approval gates), use tool middleware instead — middleware=[hook] on @tool, @agent.tool, or Toolkit. See ag2-add-custom-tool for the syntax. The approval_required() built-in (see ag2-hitl) is a tool middleware.
Agent middleware runs outside tool middleware: BaseMiddleware.on_tool_execution() sees the full execution including tool-scoped hooks.
Picking the right hook
on_turn→ behaviour about the whole request/response lifecycle.on_llm_call→ behaviour about what goes into / comes out of the model.on_tool_execution→ tool safety / auditing / result shaping across many tools.- Tool-scoped middleware (not
BaseMiddleware) → behaviour for a single tool's definition. on_human_input→ intercept HITL requests/responses.
Going deeper
references/builtin_middleware.md— every built-in's params, common-case recipes, when each fits.website/docs/user-guide/middleware.mdx— full reference, ordering examples, custom-middleware guidelines.website/docs/user-guide/tools/tool_middleware.mdx— per-tool hooks (different mental model — plain async callables, notBaseMiddleware).- For OpenTelemetry instrumentation specifically, see
ag2-telemetry.
Common pitfalls
- Forgetting
Middleware(...)for constructor args —middleware=[AuditMiddleware](no wrapper) only works if the class needs onlyeventandcontext. Otherwise wrap:middleware=[Middleware(AuditMiddleware, logger=...)]. - Mutation order surprises — middleware runs in registration order. If middleware A trims history and middleware B logs it, register
[A, B]so B sees the trimmed view. - Per-call middleware doesn't replace agent middleware — it's appended. Agent middleware still runs.
- One big middleware doing five things — keep hooks focused. Logging + retry + mutation + policy in one class is hard to reason about and order. Split into multiple instances.
on_tool_executionbranching onevent.namefor a single tool — that's a smell; use tool-scoped middleware for one-tool behaviour and reserveon_tool_executionfor cross-cutting policies.- Putting OpenTelemetry instrumentation in custom code — there's a
TelemetryMiddlewarefor that; seeag2-telemetry.
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: ag2ai
- Source: ag2ai/ag2-skills
- License: Apache-2.0
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.