Microsoft Agent Framework Middleware Created: 23 Feb 2026 Updated: 23 Feb 2026

Agent Middleware

Middleware intercepts agent interactions at various stages of execution without modifying the core agent or function logic. It is the right place for cross-cutting concerns such as logging, security validation, error handling, input sanitisation, and result transformation.

Three Types of Middleware

TypeWhat it interceptsRegistration point
Agent Run MiddlewareEvery RunAsync() call - full input message list and response.agent.AsBuilder().Use(runFunc: ...)
Agent Run Streaming MiddlewareEvery RunStreamingAsync() call - streamed response updates.agent.AsBuilder().Use(runStreamingFunc: ...)
Function Calling MiddlewareEvery function/tool call executed by the agent during a run.agent.AsBuilder().Use(functionCallFunc: ...)
IChatClient MiddlewareEvery raw inference call to the underlying IChatClient.chatClient.AsBuilder().Use(getResponseFunc: ...)

When multiple middleware instances of the same type are registered they form a chain. Each middleware must call the next delegate to pass control to the next in the chain (and ultimately to the agent or chat client itself).

Agent Run Middleware

Inspect or modify the input messages before the agent runs and inspect or transform the AgentResponse after it returns.

async Task<AgentResponse> CustomAgentRunMiddleware(
IEnumerable<ChatMessage> messages,
AgentSession? session,
AgentRunOptions? options,
AIAgent innerAgent,
CancellationToken cancellationToken)
{
Console.WriteLine($"[Middleware] Incoming messages: {messages.Count()}");

AgentResponse response = await innerAgent
.RunAsync(messages, session, options, cancellationToken)
.ConfigureAwait(false);

Console.WriteLine($"[Middleware] Response messages: {response.Messages.Count}");

return response;
}

Agent Run Streaming Middleware

Same idea for streaming. Collect updates as they arrive, then inspect the assembled response after all updates have been yielded.

async IAsyncEnumerable<AgentResponseUpdate> CustomAgentRunStreamingMiddleware(
IEnumerable<ChatMessage> messages,
AgentSession? session,
AgentRunOptions? options,
AIAgent innerAgent,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
Console.WriteLine($"[Middleware] Incoming messages: {messages.Count()}");

List<AgentResponseUpdate> updates = [];
await foreach (AgentResponseUpdate update in
innerAgent.RunStreamingAsync(messages, session, options, cancellationToken))
{
updates.Add(update);
yield return update;
}

Console.WriteLine($"[Middleware] Total updates: {updates.Count}");
}

Function Calling Middleware

Intercept each tool/function call the agent makes during a run. Inspect the function name and arguments, modify the result, or set context.Terminate = true to stop the function calling loop early.

async ValueTask<object?> CustomFunctionCallingMiddleware(
AIAgent agent,
FunctionInvocationContext context,
Func<FunctionInvocationContext, CancellationToken, ValueTask<object?>> next,
CancellationToken cancellationToken)
{
Console.WriteLine($"[Middleware] Calling function: {context.Function.Name}");

object? result = await next(context, cancellationToken);

Console.WriteLine($"[Middleware] Function result: {result}");

return result;
}

Warning: Setting context.Terminate = true stops the function call loop but may leave the chat history in an inconsistent state (function call content with no corresponding result content). Only terminate when you are certain the session will not be used for further runs.

Note: Function calling middleware is only supported for agents that use FunctionInvokingChatClient (e.g. ChatClientAgent).

IChatClient Middleware

Intercept the raw request sent to the inference service and the raw ChatResponse that comes back. This sits below the agent layer and runs on every inference call regardless of session or function calling state.

async Task<ChatResponse> CustomChatClientMiddleware(
IEnumerable<ChatMessage> messages,
ChatOptions? options,
IChatClient innerChatClient,
CancellationToken cancellationToken)
{
Console.WriteLine($"[Middleware] Messages sent to LLM: {messages.Count()}");

ChatResponse response = await innerChatClient
.GetResponseAsync(messages, options, cancellationToken);

Console.WriteLine($"[Middleware] Messages received from LLM: {response.Messages.Count}");

return response;
}

Registering Middleware

Agent Run and Function Calling Middleware

Use AsBuilder() on an existing agent, chain Use() calls, then call Build() to get a new middleware-enabled agent. Ideally provide both runFunc and runStreamingFunc together so streaming runs are intercepted correctly.

var middlewareEnabledAgent = originalAgent
.AsBuilder()
.Use(runFunc: CustomAgentRunMiddleware,
runStreamingFunc: CustomAgentRunStreamingMiddleware)
.Use(CustomFunctionCallingMiddleware)
.Build();

IChatClient Middleware - via builder before agent creation

var middlewareEnabledChatClient = chatClient
.AsBuilder()
.Use(getResponseFunc: CustomChatClientMiddleware, getStreamingResponseFunc: null)
.Build();

var agent = new ChatClientAgent(
middlewareEnabledChatClient,
instructions: "You are a helpful assistant.");

IChatClient Middleware - via factory helper

var agent = new OpenAIClient("<your_api_key>")
.GetChatClient("gpt-4o-mini")
.AsAIAgent(
"You are a helpful assistant.",
clientFactory: chatClient => chatClient
.AsBuilder()
.Use(getResponseFunc: CustomChatClientMiddleware, getStreamingResponseFunc: null)
.Build());

Summary

Middleware typeSignature shapeKey parameters
Agent RunTask<AgentResponse> (messages, session, options, innerAgent, ct)Full input + full output
Agent Run StreamingIAsyncEnumerable<AgentResponseUpdate> (messages, session, options, innerAgent, ct)Full input + streamed updates
Function CallingValueTask<object?> (agent, context, next, ct)FunctionInvocationContext with function name, args, Terminate flag
IChatClientTask<ChatResponse> (messages, options, innerChatClient, ct)Raw LLM input and output

Required Packages

PackagePurpose
Microsoft.Agents.AIAIAgent, AgentResponse, FunctionInvocationContext, builder APIs
Microsoft.Extensions.AIIChatClient, ChatResponse, ChatClientBuilder
Microsoft.Extensions.AI.OpenAIAsIChatClient() and OpenAI integration
Share this lesson: