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 5 hook positions, covering the full path from the outer reply process down to the raw model API call:
Position
Type
Description
on_reply
Onion
Wraps a complete reply, covering all ReAct rounds, tool executions, and the final output
on_reasoning
Onion
Wraps a single ReAct round’s reasoning step (input assembly → model call → stream decoding)
on_acting
Onion
Wraps a single tool call execution
on_model_call
Onion
Wraps the underlying ChatModel API call — the closest to the model
on_system_prompt
Transformer
Fires every time the system prompt is assembled; multiple middlewares chain in sequence, each transforming the previous one’s output
The two 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.
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_reply
ReAct loop (per round)
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.
AgentScope packages a set of hooks into a class — a single middleware class can implement any subset of the 5 hook positions at the same time. Pass instances to Agent(middlewares=[...]) to equip them:
from agentscope import Agentfrom agentscope.middleware import TracingMiddlewareagent = Agent( name="assistant", system_prompt="You are a helpful assistant.", model=model, toolkit=toolkit, middlewares=[TracingMiddleware()],)
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.
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:
from agentscope import Agentfrom agentscope.middleware import TracingMiddlewareagent = Agent( name="assistant", system_prompt="You are a helpful assistant.", model=model, toolkit=toolkit, middlewares=[TracingMiddleware()],)
Each reply produces a nested span tree. The key attributes captured at each level are:
Agent Reply Span
Model Call Span
Tool Execution Span
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
From on_model_call:
Model name, provider, input/output token counts
Request and response message content
Wraps streaming responses, writing attributes onto the final chunk
From on_acting:
Tool name, call ID, input arguments
Tool execution result
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.
Subclass MiddlewareBase and implement only the hooks you need — leave the rest alone.The example below covers 4 positions 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: