Skip to main content

Overview

Agent middleware is the mechanism for injecting custom logic — logging, tracing, input rewriting, access control — into key points of the agent execution pipeline, without modifying the agent or model code. AgentScope exposes 6 hook positions plus a tool-provider hook, covering the full path from the outer reply process down to the raw model API call: The three types differ as follows:
  • Onion — middleware wraps the next handler, allowing logic before/after next_handler() and observation of the intermediate event stream.
  • Transformer — middlewares form a pipeline; the previous one’s output feeds into the next one. There is no “inner layer” concept.
  • Tool source — not a hook on the runtime path. Agent.__init__ does not call list_tools(); you opt in explicitly by collecting the tools from your middlewares and passing them into the toolkit yourself.
The diagram below shows how these hooks nest within the agent lifecycle. on_system_prompt is embedded inside on_reasoning because it fires when the reasoning step assembles the system prompt; on_compress_context sits at the top of each ReAct round, before reasoning:
on_reply
ReAct loop (per round)
on_compress_context (context compression decision)
on_reasoning
on_system_prompt (system prompt assembly)
on_model_call (model API call)
on_acting (once per tool call)
on_acting currently wraps only tool execution inside the agent runtime; tools dispatched outside the agent via external execution are not tracked by on_acting.

Equip Middleware

AgentScope packages a set of hooks into a class — a single middleware class can implement any subset of the 6 hook positions (plus the optional list_tools tool-provider hook) at the same time. Pass instances to Agent(middlewares=[...]) to equip them:
At construction time the agent scans each middleware instance, checks which hooks it actually implements, and routes it into the matching position-specific execution lists. Unimplemented positions are skipped automatically with no call overhead.

Built-in Middleware

TracingMiddleware

TracingMiddleware wires the full agent lifecycle to OpenTelemetry tracing. It instruments on_reply, on_model_call, and on_acting, producing hierarchical spans. Before using it, register a TracerProvider and an OTLP exporter in the process:
Then attach TracingMiddleware to the agent:
Each reply produces a nested span tree. The key attributes captured at each level are:
From on_reply:
  • Agent name, session ID, reply ID
  • Input messages and the final output message
  • HITL pending tool calls
  • External execution pending tool calls
When no TracerProvider is configured, every hook short-circuits directly to next_handler() — no spans are created, no attributes are computed — making the overhead negligible.
When the agent receives an ExternalExecutionResultEvent (a tool executed outside the agent), TracingMiddleware synthesizes a compensating span for each external execution result, preserving full observability for tools run by external systems.

Add Additional Spans

To trace custom operations within the agent lifecycle, use the standard OpenTelemetry Python SDK directly. Obtain a tracer scoped to AgentScope and wrap any target code in a span:
These custom spans are emitted alongside AgentScope’s built-in spans and delivered to the same OTLP collector configured in the TracerProvider.

TTSMiddleware

TTSMiddleware intercepts the agent’s text output and synthesizes speech audio, injecting DataBlockStartEvent / DataBlockDeltaEvent / DataBlockEndEvent into the event stream alongside the text. It hooks into on_reply to observe every TextBlockDeltaEvent and TextBlockEndEvent.
The middleware automatically adapts to the TTS model’s mode: The output event stream differs by mode: Non-realtime — audio follows after the text block completes:
Realtime — audio chunks arrive interleaved with text as synthesis runs concurrently:
Each DataBlockDeltaEvent.data carries an incremental base64-encoded audio chunk; the full audio is the concatenation of every delta’s decoded bytes, keyed by block_id.

Custom Middleware

Subclass MiddlewareBase and implement only the hooks you need — leave the rest alone. The example below covers every position in a single middleware. Each onion hook receives an input_kwargs dict carrying the fields that flow into the wrapped layer; forward it with next_handler(**input_kwargs), or pass keyword arguments to override specific fields:

Execution Order

Onion hooks (on_reply, on_reasoning, on_acting, on_model_call) — the first middleware in the list is the outermost layer:
For streaming / event-yielding hooks, the inner middleware sees each yielded event first:
Transformer hooks (on_system_prompt) — middlewares chain left to right:
The overall execution order of all hooks within a single reply follows the agent lifecycle:
list_tools is not part of the per-reply execution path and is not invoked automatically by the agent — it is a convenience interface so a middleware can advertise its own tools. The caller assembling the toolkit decides whether to collect them.

Practical Examples

Timing middleware

The middleware below records the elapsed time of every model call:

Rate-limiting middleware

The middleware below enforces a minimum interval between two model calls:

Dynamic system prompt middleware

The middleware below injects real-time context into the system prompt:

Model fallback middleware

The middleware below switches to a fallback model when the primary one fails: