.NET: Pass IServiceProvider to ChatClientAgent in AddAIAgent overloads (#7737)

All four AddAIAgent overloads in AgentHostingServiceCollectionExtensions
created a ChatClientAgent without forwarding the IServiceProvider, so the
FunctionInvokingChatClient in the agent's pipeline had no service provider
and tools could not resolve their dependencies at invocation time.

Fixes #4453

Co-authored-by: Max Montes Soza <max-montes@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
westey
2026-08-18 18:19:01 +00:00
committed by GitHub
parent e33e78127f
commit f330457042
2 changed files with 140 additions and 4 deletions
@@ -30,7 +30,7 @@ public static class AgentHostingServiceCollectionExtensions
{
var chatClient = sp.GetRequiredService<IChatClient>();
var tools = sp.GetKeyedServices<AITool>(name).ToList();
return new ChatClientAgent(chatClient, instructions, key, tools: tools);
return new ChatClientAgent(chatClient, instructions, key, tools: tools, services: sp);
}, lifetime);
}
@@ -51,7 +51,7 @@ public static class AgentHostingServiceCollectionExtensions
return services.AddAIAgent(name, (sp, key) =>
{
var tools = sp.GetKeyedServices<AITool>(name).ToList();
return new ChatClientAgent(chatClient, instructions, key, tools: tools);
return new ChatClientAgent(chatClient, instructions, key, tools: tools, services: sp);
}, lifetime);
}
@@ -73,7 +73,7 @@ public static class AgentHostingServiceCollectionExtensions
{
var chatClient = chatClientServiceKey is null ? sp.GetRequiredService<IChatClient>() : sp.GetRequiredKeyedService<IChatClient>(chatClientServiceKey);
var tools = sp.GetKeyedServices<AITool>(name).ToList();
return new ChatClientAgent(chatClient, instructions, key, tools: tools);
return new ChatClientAgent(chatClient, instructions, key, tools: tools, services: sp);
}, lifetime);
}
@@ -96,7 +96,7 @@ public static class AgentHostingServiceCollectionExtensions
{
var chatClient = chatClientServiceKey is null ? sp.GetRequiredService<IChatClient>() : sp.GetRequiredKeyedService<IChatClient>(chatClientServiceKey);
var tools = sp.GetKeyedServices<AITool>(name).ToList();
return new ChatClientAgent(chatClient, instructions: instructions, name: key, description: description, tools: tools);
return new ChatClientAgent(chatClient, instructions: instructions, name: key, description: description, tools: tools, services: sp);
}, lifetime);
}
@@ -1,7 +1,11 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Moq;
@@ -293,4 +297,136 @@ public class AgentHostingServiceCollectionExtensionsTests
Assert.Equal(lifetime, descriptor.Lifetime);
Assert.Equal(lifetime, result.Lifetime);
}
/// <summary>
/// Verifies end-to-end that a tool invoked by an agent registered via <c>AddAIAgent</c> receives the
/// application's <see cref="IServiceProvider"/> in its <see cref="AIFunctionArguments.Services"/>, and can
/// therefore resolve its dependencies at invocation time.
/// Regression test for https://github.com/microsoft/agent-framework/issues/4453.
/// </summary>
[Theory]
[InlineData(AddAIAgentOverload.Instructions)]
[InlineData(AddAIAgentOverload.ChatClientInstance)]
[InlineData(AddAIAgentOverload.ChatClientServiceKey)]
[InlineData(AddAIAgentOverload.DescriptionAndChatClientServiceKey)]
public async Task AddAIAgent_ToolInvocationCanResolveServicesFromDIAsync(AddAIAgentOverload overload)
{
// Arrange
var tool = new ServiceCapturingAIFunction();
var services = new ServiceCollection();
services.AddSingleton<IMarkerService, MarkerService>();
RegisterAgent(services, overload).WithAITool(tool);
var serviceProvider = services.BuildServiceProvider();
var agent = serviceProvider.GetRequiredKeyedService<AIAgent>(AgentName);
// Act
var response = await agent.RunAsync("call the tool");
// Assert
Assert.Equal("done", response.Text);
Assert.True(tool.WasInvoked);
Assert.NotNull(tool.ResolvedMarkerService);
}
private const string AgentName = "test-agent";
private const string ChatClientServiceKey = "test-chat-client";
/// <summary>
/// Identifies which <c>AddAIAgent</c> overload a test exercises.
/// </summary>
public enum AddAIAgentOverload
{
/// <summary>The overload taking only a name and instructions.</summary>
Instructions,
/// <summary>The overload taking an <see cref="IChatClient"/> instance.</summary>
ChatClientInstance,
/// <summary>The overload taking a chat client service key.</summary>
ChatClientServiceKey,
/// <summary>The overload taking a description and a chat client service key.</summary>
DescriptionAndChatClientServiceKey,
}
private static IHostedAgentBuilder RegisterAgent(IServiceCollection services, AddAIAgentOverload overload)
{
switch (overload)
{
case AddAIAgentOverload.Instructions:
services.AddSingleton<IChatClient>(new ToolCallingChatClient());
return services.AddAIAgent(AgentName, "Test instructions");
case AddAIAgentOverload.ChatClientInstance:
return services.AddAIAgent(AgentName, "Test instructions", new ToolCallingChatClient());
case AddAIAgentOverload.ChatClientServiceKey:
services.AddKeyedSingleton<IChatClient>(ChatClientServiceKey, new ToolCallingChatClient());
return services.AddAIAgent(AgentName, "Test instructions", (object?)ChatClientServiceKey);
case AddAIAgentOverload.DescriptionAndChatClientServiceKey:
services.AddKeyedSingleton<IChatClient>(ChatClientServiceKey, new ToolCallingChatClient());
return services.AddAIAgent(AgentName, "Test instructions", "A test agent", ChatClientServiceKey);
default:
throw new ArgumentOutOfRangeException(nameof(overload));
}
}
/// <summary>
/// Marker service used to verify that the application's service provider is reachable from tool invocations.
/// </summary>
private interface IMarkerService;
[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Instantiated via dependency injection.")]
private sealed class MarkerService : IMarkerService;
/// <summary>
/// An <see cref="AIFunction"/> that records whether it could resolve <see cref="IMarkerService"/> from the
/// <see cref="AIFunctionArguments.Services"/> supplied at invocation time.
/// </summary>
private sealed class ServiceCapturingAIFunction : AIFunction
{
public bool WasInvoked { get; private set; }
public IMarkerService? ResolvedMarkerService { get; private set; }
public override string Name => "TestTool";
public override string Description => "A test tool.";
protected override ValueTask<object?> InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken)
{
this.WasInvoked = true;
this.ResolvedMarkerService = arguments.Services?.GetService<IMarkerService>();
return new ValueTask<object?>("tool result");
}
}
/// <summary>
/// A chat client that requests the test tool on the first call and returns a final answer afterwards.
/// </summary>
private sealed class ToolCallingChatClient : IChatClient
{
private int _callCount;
public Task<ChatResponse> GetResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
{
var content = Interlocked.Increment(ref this._callCount) == 1
? new FunctionCallContent(callId: "call-1", name: "TestTool", arguments: null)
: (AIContent)new TextContent("done");
return Task.FromResult(new ChatResponse(new ChatMessage(ChatRole.Assistant, [content])));
}
public IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
public object? GetService(Type serviceType, object? serviceKey = null) => null;
public void Dispose()
{
}
}
}