From f330457042f1b829ea10c47cd1ffbb891476783d Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Tue, 18 Aug 2026 18:19:01 +0000 Subject: [PATCH] .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 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...AgentHostingServiceCollectionExtensions.cs | 8 +- ...HostingServiceCollectionExtensionsTests.cs | 136 ++++++++++++++++++ 2 files changed, 140 insertions(+), 4 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting/AgentHostingServiceCollectionExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting/AgentHostingServiceCollectionExtensions.cs index 03ec8cdad..40d585302 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting/AgentHostingServiceCollectionExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting/AgentHostingServiceCollectionExtensions.cs @@ -30,7 +30,7 @@ public static class AgentHostingServiceCollectionExtensions { var chatClient = sp.GetRequiredService(); var tools = sp.GetKeyedServices(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(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() : sp.GetRequiredKeyedService(chatClientServiceKey); var tools = sp.GetKeyedServices(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() : sp.GetRequiredKeyedService(chatClientServiceKey); var tools = sp.GetKeyedServices(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); } diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/AgentHostingServiceCollectionExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/AgentHostingServiceCollectionExtensionsTests.cs index 4d0a82993..9229deb9c 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/AgentHostingServiceCollectionExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/AgentHostingServiceCollectionExtensionsTests.cs @@ -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); } + + /// + /// Verifies end-to-end that a tool invoked by an agent registered via AddAIAgent receives the + /// application's in its , and can + /// therefore resolve its dependencies at invocation time. + /// Regression test for https://github.com/microsoft/agent-framework/issues/4453. + /// + [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(); + RegisterAgent(services, overload).WithAITool(tool); + + var serviceProvider = services.BuildServiceProvider(); + var agent = serviceProvider.GetRequiredKeyedService(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"; + + /// + /// Identifies which AddAIAgent overload a test exercises. + /// + public enum AddAIAgentOverload + { + /// The overload taking only a name and instructions. + Instructions, + + /// The overload taking an instance. + ChatClientInstance, + + /// The overload taking a chat client service key. + ChatClientServiceKey, + + /// The overload taking a description and a chat client service key. + DescriptionAndChatClientServiceKey, + } + + private static IHostedAgentBuilder RegisterAgent(IServiceCollection services, AddAIAgentOverload overload) + { + switch (overload) + { + case AddAIAgentOverload.Instructions: + services.AddSingleton(new ToolCallingChatClient()); + return services.AddAIAgent(AgentName, "Test instructions"); + + case AddAIAgentOverload.ChatClientInstance: + return services.AddAIAgent(AgentName, "Test instructions", new ToolCallingChatClient()); + + case AddAIAgentOverload.ChatClientServiceKey: + services.AddKeyedSingleton(ChatClientServiceKey, new ToolCallingChatClient()); + return services.AddAIAgent(AgentName, "Test instructions", (object?)ChatClientServiceKey); + + case AddAIAgentOverload.DescriptionAndChatClientServiceKey: + services.AddKeyedSingleton(ChatClientServiceKey, new ToolCallingChatClient()); + return services.AddAIAgent(AgentName, "Test instructions", "A test agent", ChatClientServiceKey); + + default: + throw new ArgumentOutOfRangeException(nameof(overload)); + } + } + + /// + /// Marker service used to verify that the application's service provider is reachable from tool invocations. + /// + private interface IMarkerService; + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Instantiated via dependency injection.")] + private sealed class MarkerService : IMarkerService; + + /// + /// An that records whether it could resolve from the + /// supplied at invocation time. + /// + 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 InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) + { + this.WasInvoked = true; + this.ResolvedMarkerService = arguments.Services?.GetService(); + return new ValueTask("tool result"); + } + } + + /// + /// A chat client that requests the test tool on the first call and returns a final answer afterwards. + /// + private sealed class ToolCallingChatClient : IChatClient + { + private int _callCount; + + public Task GetResponseAsync(IEnumerable 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 GetStreamingResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) + => throw new NotImplementedException(); + + public object? GetService(Type serviceType, object? serviceKey = null) => null; + + public void Dispose() + { + } + } }