The order of function-call middleware registered at the agent level appears inverted at runtime. When two FunctionInvocationDelegatingAgent middlewares (A then B) are registered via AIAgentBuilder.Use, the built agent is logically A(B(inner)). However, the observed runtime invocation order for function calls is B.Pre -> A.Pre -> function -> A.Post -> B.Post, i.e. B appears to be the outer middleware.
Reproduction steps
c#
chatClient = chatClient.AsBuilder()
.Use(getResponseFunc: ChatClientMiddlewareA, getStreamingResponseFunc: null)
.Use(getResponseFunc: ChatClientMiddlewareB, getStreamingResponseFunc: null)
.ConfigureOptions(options =>
{
options.RawRepresentationFactory = (response) =>
{
var opt = new OpenAI.Chat.ChatCompletionOptions();
#pragma warning disable SCME0001 // 类型仅用于评估,在将来的更新中可能会被更改或删除。取消此诊断以继续。
opt.Patch.Set("$thinking.type"u8, "disabled");
#pragma warning restore SCME0001 // 类型仅用于评估,在将来的更新中可能会被更改或删除。取消此诊断以继续。
return opt;
};
})
.Build();
var originalAgent = chatClient
.AsBuilder()
.BuildAIAgent(
instructions: "You are an AI assistant that helps people find information.",
tools: [AIFunctionFactory.Create(GetDateTime, name: nameof(GetDateTime))]);
var middlewareAgent = originalAgent
.AsBuilder()
.Use(FunctionCallMiddlewareA)
.Use(FunctionCallMiddlewareB)
.Build();
var response = await middlewareAgent.RunAsync("Hello, what time is it?", cancellationToken: default);
async Task<ChatResponse> ChatClientMiddlewareA(IEnumerable<ChatMessage> message, ChatOptions? options, IChatClient innerChatClient, CancellationToken cancellationToken)
{
Console.WriteLine("Chat Client Middleware A - Pre-Chat");
var response = await innerChatClient.GetResponseAsync(message, options, cancellationToken);
Console.WriteLine("Chat Client Middleware A - Post-Chat");
return response;
}
async Task<ChatResponse> ChatClientMiddlewareB(IEnumerable<ChatMessage> message, ChatOptions? options, IChatClient innerChatClient, CancellationToken cancellationToken)
{
Console.WriteLine("Chat Client Middleware B - Pre-Chat");
var response = await innerChatClient.GetResponseAsync(message, options, cancellationToken);
Console.WriteLine("Chat Client Middleware B - Post-Chat");
return response;
}
async ValueTask<object?> FunctionCallMiddlewareA(AIAgent agent, FunctionInvocationContext context, Func<FunctionInvocationContext, CancellationToken, ValueTask<object?>> next, CancellationToken cancellationToken)
{
Console.WriteLine($"Function Name: {context!.Function.Name} - Middleware A Pre-Invoke");
var result = await next(context, cancellationToken);
Console.WriteLine($"Function Name: {context!.Function.Name} - Middleware A Post-Invoke");
return result;
}
async ValueTask<object?> FunctionCallMiddlewareB(AIAgent agent, FunctionInvocationContext context, Func<FunctionInvocationContext, CancellationToken, ValueTask<object?>> next, CancellationToken cancellationToken)
{
Console.WriteLine($"Function Name: {context!.Function.Name} - Middleware B Pre-Invoke");
var result = await next(context, cancellationToken);
Console.WriteLine($"Function Name: {context!.Function.Name} - Middleware B Post-Invoke");
return result;
}
[Description("The current datetime offset.")]
static string GetDateTime()
=> DateTimeOffset.Now.ToString();
Description
The order of function-call middleware registered at the agent level appears inverted at runtime. When two FunctionInvocationDelegatingAgent middlewares (A then B) are registered via AIAgentBuilder.Use, the built agent is logically A(B(inner)). However, the observed runtime invocation order for function calls is B.Pre -> A.Pre -> function -> A.Post -> B.Post, i.e. B appears to be the outer middleware.
Reproduction steps
• builder.Use(FunctionCallMiddlewareA) // A
• builder.Use(FunctionCallMiddlewareB) // B
Observed output (example)
• Function Name: GetDateTime - Middleware B Pre-Invoke
• Function Name: GetDateTime - Middleware A Pre-Invoke
• Function Name: GetDateTime - Middleware A Post-Invoke
• Function Name: GetDateTime - Middleware B Post-Invoke
Expected behavior Registering Use(A) then Use(B) should produce A(B(inner)) both at agent-level wrapping and at function-invocation time, so invocation order should be:
• A.Pre -> B.Pre -> function -> B.Post -> A.Post
Code Sample
c# chatClient = chatClient.AsBuilder() .Use(getResponseFunc: ChatClientMiddlewareA, getStreamingResponseFunc: null) .Use(getResponseFunc: ChatClientMiddlewareB, getStreamingResponseFunc: null) .ConfigureOptions(options => { options.RawRepresentationFactory = (response) => { var opt = new OpenAI.Chat.ChatCompletionOptions(); #pragma warning disable SCME0001 // 类型仅用于评估,在将来的更新中可能会被更改或删除。取消此诊断以继续。 opt.Patch.Set("$thinking.type"u8, "disabled"); #pragma warning restore SCME0001 // 类型仅用于评估,在将来的更新中可能会被更改或删除。取消此诊断以继续。 return opt; }; }) .Build(); var originalAgent = chatClient .AsBuilder() .BuildAIAgent( instructions: "You are an AI assistant that helps people find information.", tools: [AIFunctionFactory.Create(GetDateTime, name: nameof(GetDateTime))]); var middlewareAgent = originalAgent .AsBuilder() .Use(FunctionCallMiddlewareA) .Use(FunctionCallMiddlewareB) .Build(); var response = await middlewareAgent.RunAsync("Hello, what time is it?", cancellationToken: default); async Task<ChatResponse> ChatClientMiddlewareA(IEnumerable<ChatMessage> message, ChatOptions? options, IChatClient innerChatClient, CancellationToken cancellationToken) { Console.WriteLine("Chat Client Middleware A - Pre-Chat"); var response = await innerChatClient.GetResponseAsync(message, options, cancellationToken); Console.WriteLine("Chat Client Middleware A - Post-Chat"); return response; } async Task<ChatResponse> ChatClientMiddlewareB(IEnumerable<ChatMessage> message, ChatOptions? options, IChatClient innerChatClient, CancellationToken cancellationToken) { Console.WriteLine("Chat Client Middleware B - Pre-Chat"); var response = await innerChatClient.GetResponseAsync(message, options, cancellationToken); Console.WriteLine("Chat Client Middleware B - Post-Chat"); return response; } async ValueTask<object?> FunctionCallMiddlewareA(AIAgent agent, FunctionInvocationContext context, Func<FunctionInvocationContext, CancellationToken, ValueTask<object?>> next, CancellationToken cancellationToken) { Console.WriteLine($"Function Name: {context!.Function.Name} - Middleware A Pre-Invoke"); var result = await next(context, cancellationToken); Console.WriteLine($"Function Name: {context!.Function.Name} - Middleware A Post-Invoke"); return result; } async ValueTask<object?> FunctionCallMiddlewareB(AIAgent agent, FunctionInvocationContext context, Func<FunctionInvocationContext, CancellationToken, ValueTask<object?>> next, CancellationToken cancellationToken) { Console.WriteLine($"Function Name: {context!.Function.Name} - Middleware B Pre-Invoke"); var result = await next(context, cancellationToken); Console.WriteLine($"Function Name: {context!.Function.Name} - Middleware B Post-Invoke"); return result; } [Description("The current datetime offset.")] static string GetDateTime() => DateTimeOffset.Now.ToString();Error Messages / Stack Traces
Package Versions
MicroSoft.Agents.AI 1.8.0
.NET Version
.Net 10.0
Additional Context
Root cause hypothesis
• FunctionInvocationDelegatingAgent injects its function-middleware by setting ChatClientAgentRunOptions.ChatClientFactory inside AgentRunOptionsWithFunctionMiddleware.
• The ChatClientFactory implementation constructs a temporary chat-client builder and performs builder.Use(originalFactory) then builder.ConfigureOptions(...) (which registers the middleware that wraps functions).
• Because chat-client builders also append factories and Build() applies them in reverse, the order of registrations inside the ChatClientFactory causes the chat-client-level composition to produce the opposite outer/inner relationship.
• In short: agent-level wrapping + per-request chat-client factory registration (which itself registers factories) interact such that registration order ends up inverted at runtime.
Relevant files
• src/Microsoft.Agents.AI/FunctionInvocationDelegatingAgent.cs (method: AgentRunOptionsWithFunctionMiddleware)
• src/Microsoft.Agents.AI/AIAgentBuilder.cs (Use / Build semantics)
• src/Microsoft.Agents.AI/ChatClient/ChatClientAgentRunOptions.cs (ChatClientFactory)