diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Generators/Generation/SourceBuilder.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Generators/Generation/SourceBuilder.cs
index 9a74c88447..23d748e629 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows.Generators/Generation/SourceBuilder.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Generators/Generation/SourceBuilder.cs
@@ -38,6 +38,7 @@ internal static class SourceBuilder
sb.AppendLine("using System.Collections.Generic;");
sb.AppendLine("using Microsoft.Agents.AI.Workflows;");
sb.AppendLine();
+ sb.AppendLine("using RouteBuilder = Microsoft.Agents.AI.Workflows.RouteBuilder;");
// Namespace
if (!string.IsNullOrWhiteSpace(info.Namespace))
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/ConfigurationExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/ConfigurationExtensions.cs
index e18bae72a5..d6e6df5dbd 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows/ConfigurationExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows/ConfigurationExtensions.cs
@@ -3,9 +3,9 @@
namespace Microsoft.Agents.AI.Workflows;
///
-/// Provides extensions methods for creating objects
+/// Provides extension methods for creating objects
///
-public static class ConfigurationExtensions
+internal static class ConfigurationExtensions
{
///
/// Creates a new configuration that treats the subject as its base type, allowing configuration to be applied at
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/Configured.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/Configured.cs
index 3f876926be..b154bd6ca7 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows/Configured.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows/Configured.cs
@@ -8,7 +8,7 @@ namespace Microsoft.Agents.AI.Workflows;
///
/// Provides methods for creating instances.
///
-public static class Configured
+internal static class Configured
{
///
/// Creates a instance from an existing subject instance.
@@ -50,10 +50,10 @@ public static class Configured
/// A representation of a preconfigured, lazy-instantiatable instance of .
///
/// The type of the preconfigured subject.
-/// A factory to intantiate the subject when desired.
+/// A factory to instantiate the subject when desired.
/// The unique identifier for the configured subject.
///
-public class Configured(Func> factoryAsync, string id, object? raw = null)
+internal class Configured(Func> factoryAsync, string id, object? raw = null)
{
///
/// Gets the raw representation of the configured object, if any.
@@ -66,14 +66,14 @@ public class Configured(Func> fact
public string Id => id;
///
- /// Gets the factory function to create an instance of given a .
+ /// Gets the factory function to create an instance of given a .
///
- public Func> FactoryAsync => factoryAsync;
+ public Func> FactoryAsync => factoryAsync;
///
/// The configuration for this configured instance.
///
- public Config Configuration => new(this.Id);
+ public ExecutorConfig Configuration => new(this.Id);
///
/// Gets a "partially" applied factory function that only requires no parameters to create an instance of
@@ -87,11 +87,11 @@ public class Configured(Func> fact
///
/// The type of the preconfigured subject.
/// The type of configuration options for the preconfigured subject.
-/// A factory to intantiate the subject when desired.
+/// A factory to instantiate the subject when desired.
/// The unique identifier for the configured subject.
/// Additional configuration options for the subject.
///
-public class Configured(Func, string, ValueTask> factoryAsync, string id, TOptions? options = default, object? raw = null)
+internal class Configured(Func, string, ValueTask> factoryAsync, string id, TOptions? options = default, object? raw = null)
{
///
/// The raw representation of the configured object, if any.
@@ -109,14 +109,14 @@ public class Configured(Func, string, Value
public TOptions? Options => options;
///
- /// Gets the factory function to create an instance of given a .
+ /// Gets the factory function to create an instance of given a .
///
- public Func, string, ValueTask> FactoryAsync => factoryAsync;
+ public Func, string, ValueTask> FactoryAsync => factoryAsync;
///
/// The configuration for this configured instance.
///
- public Config Configuration => new(this.Id, this.Options);
+ public ExecutorConfig Configuration => new(this.Id, this.Options);
///
/// Gets a "partially" applied factory function that only requires no parameters to create an instance of
@@ -124,11 +124,11 @@ public class Configured(Func, string, Value
///
internal Func> BoundFactoryAsync => (sessionId) => this.CreateValidatingMemoizedFactory()(this.Configuration, sessionId);
- private Func> CreateValidatingMemoizedFactory()
+ private Func> CreateValidatingMemoizedFactory()
{
return FactoryAsync;
- async ValueTask FactoryAsync(Config configuration, string sessionId)
+ async ValueTask FactoryAsync(ExecutorConfig configuration, string sessionId)
{
if (this.Id != configuration.Id)
{
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/ExecutorBindingExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/ExecutorBindingExtensions.cs
index a0170e7757..afca74af5b 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows/ExecutorBindingExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows/ExecutorBindingExtensions.cs
@@ -113,7 +113,7 @@ public static class ExecutorBindingExtensions
/// An id for the executor to be instantiated.
/// An optional parameter specifying the options.
/// An instance that resolves to the result of the factory call when messages get sent to it.
- public static ExecutorBinding BindExecutor(this Func, string, ValueTask> factoryAsync, string id, TOptions? options = null)
+ public static ExecutorBinding BindExecutor(this Func, string, ValueTask> factoryAsync, string id, TOptions? options = null)
where TExecutor : Executor
where TOptions : ExecutorOptions
{
@@ -139,7 +139,7 @@ public static class ExecutorBindingExtensions
/// An instance that resolves to the result of the factory call when messages get sent to it.
[Obsolete("Use BindExecutor() instead")]
[EditorBrowsable(EditorBrowsableState.Never)]
- public static ExecutorBinding ConfigureFactory(this Func, string, ValueTask> factoryAsync, string id, TOptions? options = null)
+ public static ExecutorBinding ConfigureFactory(this Func, string, ValueTask> factoryAsync, string id, TOptions? options = null)
where TExecutor : Executor
where TOptions : ExecutorOptions
=> factoryAsync.BindExecutor(id, options);
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/Config.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/ExecutorConfig.cs
similarity index 88%
rename from dotnet/src/Microsoft.Agents.AI.Workflows/Config.cs
rename to dotnet/src/Microsoft.Agents.AI.Workflows/ExecutorConfig.cs
index 09792d2a64..48bfd12bb9 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows/Config.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows/ExecutorConfig.cs
@@ -6,7 +6,7 @@ namespace Microsoft.Agents.AI.Workflows;
/// Represents a configuration for an object with a string identifier. For example, object.
///
/// A unique identifier for the configurable object.
-public class Config(string id)
+public class ExecutorConfig(string id)
{
///
/// Gets a unique identifier for the configurable object.
@@ -23,7 +23,7 @@ public class Config(string id)
/// The type of options for the configurable object.
/// A unique identifier for the configurable object.
/// The options for the configurable object.
-public class Config(string id, TOptions? options = default) : Config(id)
+public class ExecutorConfig(string id, TOptions? options = default) : ExecutorConfig(id)
{
///
/// Gets the options for the configured object.
diff --git a/dotnet/src/Microsoft.Agents.AI/AIContextProviderDecorators/AIContextProviderChatClient.cs b/dotnet/src/Microsoft.Agents.AI/AIContextProviderDecorators/AIContextProviderChatClient.cs
index 305abe0465..bf93832232 100644
--- a/dotnet/src/Microsoft.Agents.AI/AIContextProviderDecorators/AIContextProviderChatClient.cs
+++ b/dotnet/src/Microsoft.Agents.AI/AIContextProviderDecorators/AIContextProviderChatClient.cs
@@ -161,6 +161,8 @@ internal sealed class AIContextProviderChatClient : DelegatingChatClient
}
// Materialize the accumulated context back into messages and options.
+ // Clone options to avoid mutating the caller's instance across calls.
+ options = options?.Clone();
var enrichedMessages = aiContext.Messages ?? [];
var tools = aiContext.Tools as IList ?? aiContext.Tools?.ToList();
diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AIContextProviderDecorators/AIContextProviderChatClientTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AIContextProviderDecorators/AIContextProviderChatClientTests.cs
index 3b06bbb772..5e65c4a1a6 100644
--- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AIContextProviderDecorators/AIContextProviderChatClientTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AIContextProviderDecorators/AIContextProviderChatClientTests.cs
@@ -250,6 +250,129 @@ public class AIContextProviderChatClientTests
#endregion
+ #region Shared Options Tests
+
+ [Fact]
+ public async Task GetResponseAsync_SharedOptions_ProviderToolsDoNotAccumulateAcrossCallsAsync()
+ {
+ // Arrange: track tool count seen by the inner client on each call
+ var toolCountsSeenByInner = new List();
+
+ var innerClient = CreateMockChatClient(
+ onGetResponse: (_, options, _) =>
+ {
+ toolCountsSeenByInner.Add(options?.Tools?.Count ?? 0);
+ return Task.FromResult(new ChatResponse([new ChatMessage(ChatRole.Assistant, "Response")]));
+ });
+
+ var provider = new TestAIContextProvider("key1", provideTools: [new TestAITool()]);
+ var chatClient = new AIContextProviderChatClient(innerClient, [provider]);
+
+ var sharedOptions = new ChatOptions
+ {
+ Tools = new List { new TestAITool() }
+ };
+
+ // Act: make 3 calls reusing the same ChatOptions
+ for (int i = 0; i < 3; i++)
+ {
+ await RunWithAgentContextAsync(chatClient, sharedOptions);
+ }
+
+ // Assert: each call should see exactly 2 tools (1 baseline + 1 injected)
+ Assert.Equal(3, toolCountsSeenByInner.Count);
+ Assert.All(toolCountsSeenByInner, count => Assert.Equal(2, count));
+ }
+
+ [Fact]
+ public async Task GetResponseAsync_SharedOptions_OriginalToolsNotMutatedAsync()
+ {
+ // Arrange
+ var innerClient = CreateMockChatClient(
+ onGetResponse: (_, _, _) =>
+ Task.FromResult(new ChatResponse([new ChatMessage(ChatRole.Assistant, "Response")])));
+
+ var provider = new TestAIContextProvider("key1", provideTools: [new TestAITool()]);
+ var chatClient = new AIContextProviderChatClient(innerClient, [provider]);
+
+ var baselineTool = new TestAITool();
+ var originalTools = new List { baselineTool };
+ var sharedOptions = new ChatOptions
+ {
+ Tools = originalTools
+ };
+
+ // Act
+ await RunWithAgentContextAsync(chatClient, sharedOptions);
+
+ // Assert: the original list should still contain only the baseline tool
+ Assert.Single(originalTools);
+ Assert.Same(baselineTool, originalTools[0]);
+ Assert.Same(originalTools, sharedOptions.Tools);
+ Assert.Same(baselineTool, originalTools[0]);
+ }
+
+ [Fact]
+ public async Task GetStreamingResponseAsync_SharedOptions_ProviderToolsDoNotAccumulateAcrossCallsAsync()
+ {
+ // Arrange
+ var toolCountsSeenByInner = new List();
+
+ var innerClient = CreateMockStreamingChatClient(
+ onGetStreamingResponse: (_, options, _) =>
+ {
+ toolCountsSeenByInner.Add(options?.Tools?.Count ?? 0);
+ return ToAsyncEnumerableAsync(
+ new ChatResponseUpdate(ChatRole.Assistant, "Response"));
+ });
+
+ var provider = new TestAIContextProvider("key1", provideTools: [new TestAITool()]);
+ var chatClient = new AIContextProviderChatClient(innerClient, [provider]);
+
+ var sharedOptions = new ChatOptions
+ {
+ Tools = new List { new TestAITool() }
+ };
+
+ // Act: make 3 streaming calls reusing the same ChatOptions
+ for (int i = 0; i < 3; i++)
+ {
+ await RunStreamingWithAgentContextAsync(chatClient, [], sharedOptions);
+ }
+
+ // Assert: each call should see exactly 2 tools (1 baseline + 1 injected)
+ Assert.Equal(3, toolCountsSeenByInner.Count);
+ Assert.All(toolCountsSeenByInner, count => Assert.Equal(2, count));
+ }
+
+ [Fact]
+ public async Task GetStreamingResponseAsync_SharedOptions_OriginalToolsNotMutatedAsync()
+ {
+ // Arrange
+ var innerClient = CreateMockStreamingChatClient(
+ onGetStreamingResponse: (_, _, _) => ToAsyncEnumerableAsync(
+ new ChatResponseUpdate(ChatRole.Assistant, "Response")));
+
+ var provider = new TestAIContextProvider("key1", provideTools: [new TestAITool()]);
+ var chatClient = new AIContextProviderChatClient(innerClient, [provider]);
+
+ var baselineTool = new TestAITool();
+ var originalTools = new List { baselineTool };
+ var sharedOptions = new ChatOptions
+ {
+ Tools = originalTools
+ };
+
+ // Act
+ await RunStreamingWithAgentContextAsync(chatClient, [], sharedOptions);
+
+ // Assert: the original list should still contain only the baseline tool
+ Assert.Single(originalTools);
+ Assert.Same(baselineTool, originalTools[0]);
+ }
+
+ #endregion
+
#region Builder Extension Tests
[Fact]
@@ -341,6 +464,44 @@ public class AIContextProviderChatClientTests
await agent.RunAsync([new ChatMessage(ChatRole.User, "Hello")], s_mockSession);
}
+ ///
+ /// Runs a chat client within an agent context with the specified options.
+ ///
+ private static async Task RunWithAgentContextAsync(AIContextProviderChatClient chatClient, ChatOptions options)
+ {
+ var agent = new TestAIAgent
+ {
+ RunAsyncFunc = async (messages, session, agentOptions, ct) =>
+ {
+ var response = await chatClient.GetResponseAsync(messages, options, ct);
+ return new AgentResponse(response);
+ }
+ };
+
+ await agent.RunAsync([new ChatMessage(ChatRole.User, "Hello")], s_mockSession);
+ }
+
+ ///
+ /// Runs a streaming chat client within an agent context with the specified options.
+ ///
+ private static async Task RunStreamingWithAgentContextAsync(AIContextProviderChatClient chatClient, List updates, ChatOptions options)
+ {
+ var agent = new TestAIAgent
+ {
+ RunAsyncFunc = async (messages, session, agentOptions, ct) =>
+ {
+ await foreach (var update in chatClient.GetStreamingResponseAsync(messages, options, ct))
+ {
+ updates.Add(update);
+ }
+
+ return new AgentResponse([new ChatMessage(ChatRole.Assistant, "done")]);
+ }
+ };
+
+ await agent.RunAsync([new ChatMessage(ChatRole.User, "Hello")], s_mockSession);
+ }
+
private static IChatClient CreateMockChatClient(
Func, ChatOptions?, CancellationToken, Task> onGetResponse)
{