diff --git a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientExtensions.cs index fcca066848e..699b98c2e02 100644 --- a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientExtensions.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using Microsoft.Agents.AI; using Microsoft.Extensions.AI; using Microsoft.Extensions.DependencyInjection; @@ -134,16 +133,6 @@ internal static IChatClient WithDefaultAgentMiddleware(this IChatClient chatClie // so FICC emits execute_tool spans on the agent source. chatBuilder.Use(innerClient => new DeferredOpenTelemetryChatClient(innerClient)); - var agentChatClient = chatBuilder.Build(services); - - if (options?.ChatOptions?.Tools is { Count: > 0 }) - { - // When tools are provided in the constructor, set the tools for the whole lifecycle of the chat client - var functionService = agentChatClient.GetService(); - Debug.Assert(functionService is not null, "FunctionInvokingChatClient should be registered in the chat client."); - functionService!.AdditionalTools = options.ChatOptions.Tools; - } - - return agentChatClient; + return chatBuilder.Build(services); } } diff --git a/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicBetaServiceExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicBetaServiceExtensionsTests.cs index 45d027fc261..2e49e588cd1 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicBetaServiceExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicBetaServiceExtensionsTests.cs @@ -210,11 +210,11 @@ public void CreateAIAgent_WithTools_AssignsToolsCorrectly() // When tools are provided, ChatOptions is created but instructions remain null Assert.Null(agent.Instructions); - // Verify that tools are registered in the FunctionInvokingChatClient - var functionInvokingClient = agent.GetService(); - Assert.NotNull(functionInvokingClient); - Assert.NotNull(functionInvokingClient.AdditionalTools); - Assert.Contains(functionInvokingClient.AdditionalTools, t => t is AIFunction func && func.Name == "TestFunction"); + // Verify that tools are registered on the agent's ChatOptions, which are merged into the + // invocation-scoped ChatOptions on every call (see ChatClientAgent.CreateConfiguredChatOptions). + var agentTools = agent.GetService()?.Tools; + Assert.NotNull(agentTools); + Assert.Contains(agentTools, t => t is AIFunction func && func.Name == "TestFunction"); } /// @@ -310,11 +310,11 @@ public void CreateAIAgent_WithToolsAndInstructions_AssignsBothCorrectly() Assert.Equal("Test Agent", agent.Name); Assert.Equal("Test instructions", agent.Instructions); - // Verify that tools are registered in the FunctionInvokingChatClient - var functionInvokingClient = agent.GetService(); - Assert.NotNull(functionInvokingClient); - Assert.NotNull(functionInvokingClient.AdditionalTools); - Assert.Contains(functionInvokingClient.AdditionalTools, t => t is AIFunction func && func.Name == "TestFunction"); + // Verify that tools are registered on the agent's ChatOptions, which are merged into the + // invocation-scoped ChatOptions on every call (see ChatClientAgent.CreateConfiguredChatOptions). + var agentTools = agent.GetService()?.Tools; + Assert.NotNull(agentTools); + Assert.Contains(agentTools, t => t is AIFunction func && func.Name == "TestFunction"); } /// @@ -339,7 +339,7 @@ public void CreateAIAgent_WithEmptyTools_DoesNotAssignTools() // With empty tools and no instructions, agent instructions remain null Assert.Null(agent.Instructions); - // Verify that FunctionInvokingChatClient has no additional tools assigned + // Verify that the FunctionInvokingChatClient is not used as a fallback tool store. var functionInvokingClient = agent.GetService(); Assert.NotNull(functionInvokingClient); Assert.True(functionInvokingClient.AdditionalTools is null or { Count: 0 }); diff --git a/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicClientExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicClientExtensionsTests.cs index abbbe6111fa..876d71936e2 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicClientExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicClientExtensionsTests.cs @@ -284,11 +284,11 @@ public void CreateAIAgent_WithTools_AssignsToolsCorrectly() // When tools are provided, ChatOptions is created but instructions remain null Assert.Null(agent.Instructions); - // Verify that tools are registered in the FunctionInvokingChatClient - var functionInvokingClient = agent.GetService(); - Assert.NotNull(functionInvokingClient); - Assert.NotNull(functionInvokingClient.AdditionalTools); - Assert.Contains(functionInvokingClient.AdditionalTools, t => t is AIFunction func && func.Name == "TestFunction"); + // Verify that tools are registered on the agent's ChatOptions, which are merged into the + // invocation-scoped ChatOptions on every call (see ChatClientAgent.CreateConfiguredChatOptions). + var agentTools = agent.GetService()?.Tools; + Assert.NotNull(agentTools); + Assert.Contains(agentTools, t => t is AIFunction func && func.Name == "TestFunction"); } /// @@ -384,11 +384,11 @@ public void CreateAIAgent_WithToolsAndInstructions_AssignsBothCorrectly() Assert.Equal("Test Agent", agent.Name); Assert.Equal("Test instructions", agent.Instructions); - // Verify that tools are registered in the FunctionInvokingChatClient - var functionInvokingClient = agent.GetService(); - Assert.NotNull(functionInvokingClient); - Assert.NotNull(functionInvokingClient.AdditionalTools); - Assert.Contains(functionInvokingClient.AdditionalTools, t => t is AIFunction func && func.Name == "TestFunction"); + // Verify that tools are registered on the agent's ChatOptions, which are merged into the + // invocation-scoped ChatOptions on every call (see ChatClientAgent.CreateConfiguredChatOptions). + var agentTools = agent.GetService()?.Tools; + Assert.NotNull(agentTools); + Assert.Contains(agentTools, t => t is AIFunction func && func.Name == "TestFunction"); } /// @@ -413,7 +413,7 @@ public void CreateAIAgent_WithEmptyTools_DoesNotAssignTools() // With empty tools and no instructions, agent instructions remain null Assert.Null(agent.Instructions); - // Verify that FunctionInvokingChatClient has no additional tools assigned + // Verify that the FunctionInvokingChatClient is not used as a fallback tool store. var functionInvokingClient = agent.GetService(); Assert.NotNull(functionInvokingClient); Assert.True(functionInvokingClient.AdditionalTools is null or { Count: 0 }); diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientExtensionsTests.cs index 04937c23e8e..f6fc856e0b8 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientExtensionsTests.cs @@ -109,6 +109,32 @@ public void CreateAIAgent_WithExistingFunctionInvokingChatClient_ConfiguresConcu Assert.True(functionInvokingClient.AllowConcurrentInvocation); } + [Fact] + public void CreateAIAgent_SharedChatClient_DoesNotLeakToolsBetweenAgents() + { + // Arrange: a single pre-decorated IChatClient shared by two independently-constructed agents, + // one "privileged" and one "public", each with their own distinct tool. + var chatClientMock = new Mock(); + var sharedChatClient = chatClientMock.Object.AsBuilder().UseFunctionInvocation().Build(); + + AITool publicTool = AIFunctionFactory.Create(() => "public", name: "public_read"); + AITool privilegedTool = AIFunctionFactory.Create(() => "privileged", name: "privileged_write"); + + // Act: construct the low-privilege agent first, then the privileged agent on the same shared client. + var publicAgent = sharedChatClient.AsAIAgent(tools: [publicTool]); + var privilegedAgent = sharedChatClient.AsAIAgent(tools: [privilegedTool]); + + // Assert: neither agent mutated the shared FunctionInvokingChatClient's AdditionalTools, so + // constructing the privileged agent cannot overwrite/leak tools into the public agent's execution scope. + var functionInvokingClient = sharedChatClient.GetService(); + Assert.NotNull(functionInvokingClient); + Assert.True(functionInvokingClient.AdditionalTools is null or { Count: 0 }); + + // Each agent's own configured tools remain scoped to itself. + Assert.Equal([publicTool], publicAgent.ChatOptions!.Tools); + Assert.Equal([privilegedTool], privilegedAgent.ChatOptions!.Tools); + } + [Fact] public void CreateAIAgent_WithNullClient_Throws() {