Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<FunctionInvokingChatClient>();
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);
Comment thread
westey-m marked this conversation as resolved.
Comment thread
westey-m marked this conversation as resolved.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<FunctionInvokingChatClient>();
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<ChatOptions>()?.Tools;
Assert.NotNull(agentTools);
Assert.Contains(agentTools, t => t is AIFunction func && func.Name == "TestFunction");
}

/// <summary>
Expand Down Expand Up @@ -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<FunctionInvokingChatClient>();
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<ChatOptions>()?.Tools;
Assert.NotNull(agentTools);
Assert.Contains(agentTools, t => t is AIFunction func && func.Name == "TestFunction");
}

/// <summary>
Expand All @@ -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<FunctionInvokingChatClient>();
Assert.NotNull(functionInvokingClient);
Assert.True(functionInvokingClient.AdditionalTools is null or { Count: 0 });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<FunctionInvokingChatClient>();
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<ChatOptions>()?.Tools;
Assert.NotNull(agentTools);
Assert.Contains(agentTools, t => t is AIFunction func && func.Name == "TestFunction");
}

/// <summary>
Expand Down Expand Up @@ -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<FunctionInvokingChatClient>();
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<ChatOptions>()?.Tools;
Assert.NotNull(agentTools);
Assert.Contains(agentTools, t => t is AIFunction func && func.Name == "TestFunction");
}

/// <summary>
Expand All @@ -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<FunctionInvokingChatClient>();
Assert.NotNull(functionInvokingClient);
Assert.True(functionInvokingClient.AdditionalTools is null or { Count: 0 });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<IChatClient>();
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<FunctionInvokingChatClient>();
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()
{
Expand Down
Loading