Middleware
Middleware in AG-UI provides a powerful way to transform, filter, and augment the event streams that flow through agents. It enables you to add cross-cutting concerns like logging, authentication, rate limiting, and event filtering without modifying the core agent logic. Examples below assume the relevant RxJS operators/utilities (map, tap, catchError, switchMap, timer, etc.) are imported.
What is Middleware?
Middleware sits between the agent execution and the event consumer, allowing you to:- Transform events – Modify or enhance events as they flow through the pipeline
- Filter events – Selectively allow or block certain events
- Add metadata – Inject additional context or tracking information
- Handle errors – Implement custom error recovery strategies
- Monitor execution – Add logging, metrics, or debugging capabilities
How Middleware Works
Middleware forms a chain where each middleware wraps the next, creating layers of functionality. When an agent runs, the event stream flows through each middleware in sequence.agent.use(...) is applied in runAgent(). connectAgent() currently calls connect() directly and does not run middleware.
Function-Based Middleware
For simple transformations, you can use function-based middleware. This is the most concise way to add middleware:Class-Based Middleware
For more complex scenarios requiring state or configuration, use class-based middleware:runNext(input, next)normalizes chunk events into fullTEXT_MESSAGE_*/TOOL_CALL_*sequences.runNextWithState(input, next)also provides accumulatedmessagesandstateafter each event.
Built-in Middleware
AG-UI provides several built-in middleware components for common use cases:FilterToolCallsMiddleware
Filter tool calls based on allowed or disallowed lists:FilterToolCallsMiddleware filters emitted TOOL_CALL_* events. It does not block tool execution in the upstream model/runtime.
Middleware Patterns
Common patterns include logging, auth viaforwardedProps, and rate limiting. See the JS middleware reference for concrete implementations.
Combining Middleware
You can combine multiple middleware to create sophisticated processing pipelines:Execution Order
Middleware executes in the order it’s added, with each middleware wrapping the next:- First middleware receives the original input
- It can modify the input before passing to the next middleware
- Each middleware processes events from the next in the chain
- The final middleware calls the actual agent
Protocol Version on RUN_STARTED
Each run’s RUN_STARTED.protocolVersion names the version the run’s stream
speaks, not who relays it
(Versioning). For middleware
that means one rule, decided by who produced the run:
- Forwarding a run. When the
RUN_STARTEDcomes from the next agent in the chain, pass it through with itsprotocolVersionuntouched. Don’t add one, overwrite it, or remove it. If the downstream agent predates the field, the missing declaration is the truthful one. - Synthesizing a run. When the middleware answers the request itself and
emits its own
RUN_STARTED(no downstream agent runs), the middleware is the run’s producer. Declare the version it was built against with the SDK’sPROTOCOL_VERSIONconstant. Don’t mirror the client’sRunAgentInput.protocolVersionand don’t omit the field. - Translating a run. A middleware that rewrites the stream into a different protocol version declares the version it emits.
threadId and runId, like any other
producer:
Best Practices
- Keep middleware focused – Each middleware should have a single responsibility
- Handle errors gracefully – Use RxJS error handling operators
- Avoid blocking operations – Use async patterns for I/O operations
- Document side effects – Clearly indicate if middleware modifies state
- Test middleware independently – Write unit tests for each middleware
- Consider performance – Be mindful of processing overhead in the event stream