计划执行与检查节点
executing-plans
按既定实施计划完成工作,保持过程检查和反馈。
ag2-middleware【效率与协作】Intercept the AG2 agent loop with `BaseMiddleware` — wrap full turns (`on_turn`), each LLM call (`on_llm_call`), each tool execution (`on_tool_execution`), or each human-input request (`on_human_input`). Use for retry, logging, history trimming, request mutation, tool auditing, guardrails, or rate limiting. Built-ins: `LoggingMiddleware`, `RetryMiddl…
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
BaseMiddleware \u2014 wrap full turns (on_turn), each LLM call (on_llm_call), each tool execution (on_tool_execution), or each human-input request (on_human_input). Use for retry, logging, history trimming, request mutation, tool auditing, guardrails, or rate limiting. Built-ins: LoggingMiddleware, RetryMiddleware, HistoryLimiter, TokenLimiter, TelemetryMiddleware (see ag2-telemetry). For per-tool hooks see also ag2-add-custom-tool tool-middleware section."
license: Apache-2.0Middleware is for cross-cutting behaviour that should apply consistently across many runs without changing the agent, model client, or tools themselves. Common use cases:
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.
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 |
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),
],
)
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.
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
<LLM call>
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.
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()]).
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.
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.BaseMiddleware) → behaviour for a single tool's definition.on_human_input → intercept HITL requests/responses.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, not BaseMiddleware).ag2-telemetry.Middleware(...) for constructor args — middleware=[AuditMiddleware] (no wrapper) only works if the class needs only event and context. Otherwise wrap: middleware=[Middleware(AuditMiddleware, logger=...)].[A, B] so B sees the trimmed view.on_tool_execution branching on event.name for a single tool — that's a smell; use tool-scoped middleware for one-tool behaviour and reserve on_tool_execution for cross-cutting policies.TelemetryMiddleware for that; see ag2-telemetry.