From efb67afc135edbc926dd6e451e1a72e074b6cca1 Mon Sep 17 00:00:00 2001
From: westey <164392973+westey-m@users.noreply.github.com>
Date: Mon, 12 Jan 2026 18:37:45 +0000
Subject: [PATCH 1/2] Merge AgentRunOptions.AdditionalProperties into
ChatOptions.AdditionalProperties
---
.../ChatClient/ChatClientAgent.cs | 27 +-
.../ChatClient/ChatClientAgentTests.cs | 427 -----------------
...ChatClientAgent_ChatOptionsMergingTests.cs | 442 ++++++++++++++++++
3 files changed, 461 insertions(+), 435 deletions(-)
create mode 100644 dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_ChatOptionsMergingTests.cs
diff --git a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs
index 0c02932b0a..bc51e32601 100644
--- a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs
+++ b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs
@@ -519,16 +519,16 @@ await thread.AIContextProvider.InvokedAsync(new(inputMessages, aiContextProvider
{
ChatOptions? requestChatOptions = (runOptions as ChatClientAgentRunOptions)?.ChatOptions?.Clone();
- // If no agent chat options were provided, return the request chat options as is.
+ // If no agent chat options were provided, return the request chat options with just agent run options overrides.
if (this._agentOptions?.ChatOptions is null)
{
- return GetContinuationTokenAndApplyBackgroundResponsesProperties(requestChatOptions, runOptions);
+ return ApplyAgentRunOptionsOverrides(requestChatOptions, runOptions);
}
- // If no request chat options were provided, use the agent's chat options clone.
+ // If no request chat options were provided, use the agent's chat options clone with agent run options overrides.
if (requestChatOptions is null)
{
- return GetContinuationTokenAndApplyBackgroundResponsesProperties(this._agentOptions?.ChatOptions.Clone(), runOptions);
+ return ApplyAgentRunOptionsOverrides(this._agentOptions?.ChatOptions.Clone(), runOptions);
}
// If both are present, we need to merge them.
@@ -557,9 +557,9 @@ await thread.AIContextProvider.InvokedAsync(new(inputMessages, aiContextProvider
// Merge only the additional properties from the agent if they are not already set in the request options.
if (requestChatOptions.AdditionalProperties is not null && this._agentOptions.ChatOptions.AdditionalProperties is not null)
{
- foreach (var propertyKey in this._agentOptions.ChatOptions.AdditionalProperties.Keys)
+ foreach (var kvp in this._agentOptions.ChatOptions.AdditionalProperties)
{
- _ = requestChatOptions.AdditionalProperties.TryAdd(propertyKey, this._agentOptions.ChatOptions.AdditionalProperties[propertyKey]);
+ _ = requestChatOptions.AdditionalProperties.TryAdd(kvp.Key, kvp.Value);
}
}
else
@@ -624,9 +624,9 @@ await thread.AIContextProvider.InvokedAsync(new(inputMessages, aiContextProvider
}
}
- return GetContinuationTokenAndApplyBackgroundResponsesProperties(requestChatOptions, runOptions);
+ return ApplyAgentRunOptionsOverrides(requestChatOptions, runOptions);
- static (ChatOptions?, ChatClientAgentContinuationToken?) GetContinuationTokenAndApplyBackgroundResponsesProperties(ChatOptions? chatOptions, AgentRunOptions? agentRunOptions)
+ static (ChatOptions?, ChatClientAgentContinuationToken?) ApplyAgentRunOptionsOverrides(ChatOptions? chatOptions, AgentRunOptions? agentRunOptions)
{
if (agentRunOptions?.AllowBackgroundResponses is not null)
{
@@ -643,6 +643,17 @@ await thread.AIContextProvider.InvokedAsync(new(inputMessages, aiContextProvider
chatOptions.ContinuationToken = agentContinuationToken!.InnerToken;
}
+ // Add/Replace any additional properties from the AgentRunOptions, since they should always take precedence.
+ if (agentRunOptions?.AdditionalProperties is { Count: > 0 })
+ {
+ chatOptions ??= new ChatOptions();
+ chatOptions.AdditionalProperties ??= new();
+ foreach (var kvp in agentRunOptions.AdditionalProperties)
+ {
+ chatOptions.AdditionalProperties[kvp.Key] = kvp.Value;
+ }
+ }
+
return (chatOptions, agentContinuationToken);
}
}
diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentTests.cs
index 18d20e3e76..e5a109a7e5 100644
--- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentTests.cs
@@ -1120,433 +1120,6 @@ public void ChatOptionsReturnsClonedCopyWhenAgentOptionsHaveChatOptions()
#endregion
- #region ChatOptions Merging Tests
-
- ///
- /// Verify that ChatOptions merging works when agent has ChatOptions but request doesn't.
- ///
- [Fact]
- public async Task ChatOptionsMergingUsesAgentOptionsWhenRequestHasNoneAsync()
- {
- // Arrange
- var agentChatOptions = new ChatOptions { MaxOutputTokens = 100, Temperature = 0.7f, Instructions = "test instructions" };
- Mock mockService = new();
- ChatOptions? capturedChatOptions = null;
- mockService.Setup(
- s => s.GetResponseAsync(
- It.IsAny>(),
- It.IsAny(),
- It.IsAny()))
- .Callback, ChatOptions, CancellationToken>((msgs, opts, ct) =>
- capturedChatOptions = opts)
- .ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
-
- ChatClientAgent agent = new(mockService.Object, options: new()
- {
- ChatOptions = agentChatOptions
- });
- var messages = new List { new(ChatRole.User, "test") };
-
- // Act
- await agent.RunAsync(messages);
-
- // Assert
- Assert.NotNull(capturedChatOptions);
- Assert.Equal(100, capturedChatOptions.MaxOutputTokens);
- Assert.Equal(0.7f, capturedChatOptions.Temperature);
- Assert.Equal("test instructions", capturedChatOptions.Instructions);
- }
-
- [Fact]
- public async Task ChatOptionsMergingUsesAgentOptionsConstructorWhenRequestHasNoneAsync()
- {
- Mock mockService = new();
- ChatOptions? capturedChatOptions = null;
- mockService.Setup(
- s => s.GetResponseAsync(
- It.IsAny>(),
- It.IsAny(),
- It.IsAny()))
- .Callback, ChatOptions, CancellationToken>((msgs, opts, ct) =>
- capturedChatOptions = opts)
- .ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
-
- ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "test instructions" } });
- var messages = new List { new(ChatRole.User, "test") };
-
- // Act
- await agent.RunAsync(messages);
-
- // Assert
- Assert.NotNull(capturedChatOptions);
- Assert.Equal("test instructions", capturedChatOptions.Instructions);
- }
-
- ///
- /// Verify that ChatOptions merging works when request has ChatOptions but agent doesn't.
- ///
- [Fact]
- public async Task ChatOptionsMergingUsesRequestOptionsWhenAgentHasNoneAsync()
- {
- // Arrange
- var requestChatOptions = new ChatOptions { MaxOutputTokens = 200, Temperature = 0.3f, Instructions = "test instructions" };
- Mock mockService = new();
- ChatOptions? capturedChatOptions = null;
- mockService.Setup(
- s => s.GetResponseAsync(
- It.IsAny>(),
- It.IsAny(),
- It.IsAny()))
- .Callback, ChatOptions, CancellationToken>((msgs, opts, ct) =>
- capturedChatOptions = opts)
- .ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
-
- ChatClientAgent agent = new(mockService.Object);
- var messages = new List { new(ChatRole.User, "test") };
-
- // Act
- await agent.RunAsync(messages, options: new ChatClientAgentRunOptions(requestChatOptions));
-
- // Assert
- Assert.NotNull(capturedChatOptions);
- Assert.Equivalent(requestChatOptions, capturedChatOptions); // Should be the same instance since no merging needed
- Assert.Equal(200, capturedChatOptions.MaxOutputTokens);
- Assert.Equal(0.3f, capturedChatOptions.Temperature);
- Assert.Equal("test instructions", capturedChatOptions.Instructions);
- }
-
- ///
- /// Verify that ChatOptions merging prioritizes request options over agent options.
- ///
- [Fact]
- public async Task ChatOptionsMergingPrioritizesRequestOptionsOverAgentOptionsAsync()
- {
- // Arrange
- var agentChatOptions = new ChatOptions
- {
- Instructions = "test instructions",
- MaxOutputTokens = 100,
- Temperature = 0.7f,
- TopP = 0.9f,
- ModelId = "agent-model",
- AdditionalProperties = new AdditionalPropertiesDictionary { ["key"] = "agent-value" }
- };
- var requestChatOptions = new ChatOptions
- {
- // TopP and ModelId not set, should use agent values
- MaxOutputTokens = 200,
- Temperature = 0.3f,
- AdditionalProperties = new AdditionalPropertiesDictionary { ["key"] = "request-value" },
- Instructions = "request instructions"
- };
- var expectedChatOptionsMerge = new ChatOptions
- {
- MaxOutputTokens = 200, // Request value takes priority
- Temperature = 0.3f, // Request value takes priority
- AdditionalProperties = new AdditionalPropertiesDictionary { ["key"] = "request-value" }, // Request value takes priority
- TopP = 0.9f, // Agent value used when request doesn't specify
- ModelId = "agent-model", // Agent value used when request doesn't specify
- Instructions = "test instructions\nrequest instructions" // Request is in addition to agent instructions
- };
-
- Mock mockService = new();
- ChatOptions? capturedChatOptions = null;
- mockService.Setup(
- s => s.GetResponseAsync(
- It.IsAny>(),
- It.IsAny(),
- It.IsAny()))
- .Callback, ChatOptions, CancellationToken>((msgs, opts, ct) =>
- capturedChatOptions = opts)
- .ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
-
- ChatClientAgent agent = new(mockService.Object, options: new()
- {
- ChatOptions = agentChatOptions
- });
- var messages = new List { new(ChatRole.User, "test") };
-
- // Act
- await agent.RunAsync(messages, options: new ChatClientAgentRunOptions(requestChatOptions));
-
- // Assert
- Assert.NotNull(capturedChatOptions);
- Assert.Equivalent(expectedChatOptionsMerge, capturedChatOptions); // Should be the same instance (modified in place)
- Assert.Equal(200, capturedChatOptions.MaxOutputTokens); // Request value takes priority
- Assert.Equal(0.3f, capturedChatOptions.Temperature); // Request value takes priority
- Assert.NotNull(capturedChatOptions.AdditionalProperties);
- Assert.Equal("request-value", capturedChatOptions.AdditionalProperties["key"]); // Request value takes priority
- Assert.Equal(0.9f, capturedChatOptions.TopP); // Agent value used when request doesn't specify
- Assert.Equal("agent-model", capturedChatOptions.ModelId); // Agent value used when request doesn't specify
- }
-
- ///
- /// Verify that ChatOptions merging returns null when both agent and request have no ChatOptions.
- ///
- [Fact]
- public async Task ChatOptionsMergingReturnsNullWhenBothAgentAndRequestHaveNoneAsync()
- {
- // Arrange
- Mock mockService = new();
- ChatOptions? capturedChatOptions = null;
- mockService.Setup(
- s => s.GetResponseAsync(
- It.IsAny>(),
- It.IsAny(),
- It.IsAny()))
- .Callback, ChatOptions, CancellationToken>((msgs, opts, ct) =>
- capturedChatOptions = opts)
- .ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
-
- ChatClientAgent agent = new(mockService.Object);
- var messages = new List { new(ChatRole.User, "test") };
-
- // Act
- await agent.RunAsync(messages);
-
- // Assert
- Assert.Null(capturedChatOptions);
- }
-
- ///
- /// Verify that ChatOptions merging concatenates Tools from agent and request.
- ///
- [Fact]
- public async Task ChatOptionsMergingConcatenatesToolsFromAgentAndRequestAsync()
- {
- // Arrange
- var agentTool = AIFunctionFactory.Create(() => "agent tool");
- var requestTool = AIFunctionFactory.Create(() => "request tool");
-
- var agentChatOptions = new ChatOptions
- {
- Instructions = "test instructions",
- Tools = [agentTool]
- };
- var requestChatOptions = new ChatOptions
- {
- Tools = [requestTool]
- };
-
- Mock mockService = new();
- ChatOptions? capturedChatOptions = null;
- mockService.Setup(
- s => s.GetResponseAsync(
- It.IsAny>(),
- It.IsAny(),
- It.IsAny()))
- .Callback, ChatOptions, CancellationToken>((msgs, opts, ct) =>
- capturedChatOptions = opts)
- .ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
-
- ChatClientAgent agent = new(mockService.Object, options: new()
- {
- ChatOptions = agentChatOptions
- });
- var messages = new List { new(ChatRole.User, "test") };
-
- // Act
- await agent.RunAsync(messages, options: new ChatClientAgentRunOptions(requestChatOptions));
-
- // Assert
- Assert.NotNull(capturedChatOptions);
- Assert.NotNull(capturedChatOptions.Tools);
- Assert.Equal(2, capturedChatOptions.Tools.Count);
-
- // Request tools should come first, then agent tools
- Assert.Contains(requestTool, capturedChatOptions.Tools);
- Assert.Contains(agentTool, capturedChatOptions.Tools);
- }
-
- ///
- /// Verify that ChatOptions merging uses agent Tools when request has no Tools.
- ///
- [Fact]
- public async Task ChatOptionsMergingUsesAgentToolsWhenRequestHasNoToolsAsync()
- {
- // Arrange
- var agentTool = AIFunctionFactory.Create(() => "agent tool");
-
- var agentChatOptions = new ChatOptions
- {
- Instructions = "test instructions",
- Tools = [agentTool]
- };
- var requestChatOptions = new ChatOptions
- {
- // No Tools specified
- MaxOutputTokens = 100
- };
-
- Mock mockService = new();
- ChatOptions? capturedChatOptions = null;
- mockService.Setup(
- s => s.GetResponseAsync(
- It.IsAny>(),
- It.IsAny(),
- It.IsAny()))
- .Callback, ChatOptions, CancellationToken>((msgs, opts, ct) =>
- capturedChatOptions = opts)
- .ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
-
- ChatClientAgent agent = new(mockService.Object, options: new()
- {
- ChatOptions = agentChatOptions
- });
- var messages = new List { new(ChatRole.User, "test") };
-
- // Act
- await agent.RunAsync(messages, options: new ChatClientAgentRunOptions(requestChatOptions));
-
- // Assert
- Assert.NotNull(capturedChatOptions);
- Assert.NotNull(capturedChatOptions.Tools);
- Assert.Single(capturedChatOptions.Tools);
- Assert.Contains(agentTool, capturedChatOptions.Tools); // Should contain the agent's tool
- }
-
- ///
- /// Verify that ChatOptions merging uses RawRepresentationFactory from request first, with fallback to agent.
- ///
- [Theory]
- [InlineData("MockAgentSetting", "MockRequestSetting", "MockRequestSetting")]
- [InlineData("MockAgentSetting", null, "MockAgentSetting")]
- [InlineData(null, "MockRequestSetting", "MockRequestSetting")]
- public async Task ChatOptionsMergingUsesRawRepresentationFactoryWithFallbackAsync(string? agentSetting, string? requestSetting, string expectedSetting)
- {
- // Arrange
- var agentChatOptions = new ChatOptions
- {
- Instructions = "test instructions",
- RawRepresentationFactory = _ => agentSetting
- };
- var requestChatOptions = new ChatOptions
- {
- RawRepresentationFactory = _ => requestSetting
- };
-
- Mock mockService = new();
- ChatOptions? capturedChatOptions = null;
- mockService.Setup(
- s => s.GetResponseAsync(
- It.IsAny>(),
- It.IsAny(),
- It.IsAny()))
- .Callback, ChatOptions, CancellationToken>((msgs, opts, ct) =>
- capturedChatOptions = opts)
- .ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
-
- ChatClientAgent agent = new(mockService.Object, options: new()
- {
- ChatOptions = agentChatOptions
- });
- var messages = new List { new(ChatRole.User, "test") };
-
- // Act
- await agent.RunAsync(messages, options: new ChatClientAgentRunOptions(requestChatOptions));
-
- // Assert
- Assert.NotNull(capturedChatOptions);
- Assert.NotNull(capturedChatOptions.RawRepresentationFactory);
- Assert.Equal(expectedSetting, capturedChatOptions.RawRepresentationFactory(null!));
- }
-
- ///
- /// Verify that ChatOptions merging handles all scalar properties correctly.
- ///
- [Fact]
- public async Task ChatOptionsMergingHandlesAllScalarPropertiesCorrectlyAsync()
- {
- // Arrange
- var agentChatOptions = new ChatOptions
- {
- MaxOutputTokens = 100,
- Temperature = 0.7f,
- TopP = 0.9f,
- TopK = 50,
- PresencePenalty = 0.1f,
- FrequencyPenalty = 0.2f,
- Instructions = "agent instructions",
- ModelId = "agent-model",
- Seed = 12345,
- ConversationId = "agent-conversation",
- AllowMultipleToolCalls = true,
- StopSequences = ["agent-stop"]
- };
- var requestChatOptions = new ChatOptions
- {
- MaxOutputTokens = 200,
- Temperature = 0.3f,
- Instructions = "request instructions",
-
- // Other properties not set, should use agent values
- StopSequences = ["request-stop"]
- };
-
- var expectedChatOptionsMerge = new ChatOptions
- {
- MaxOutputTokens = 200,
- Temperature = 0.3f,
-
- // Agent value used when request doesn't specify
- TopP = 0.9f,
- TopK = 50,
- PresencePenalty = 0.1f,
- FrequencyPenalty = 0.2f,
- Instructions = "agent instructions\nrequest instructions",
- ModelId = "agent-model",
- Seed = 12345,
- ConversationId = "agent-conversation",
- AllowMultipleToolCalls = true,
-
- // Merged StopSequences
- StopSequences = ["request-stop", "agent-stop"]
- };
-
- Mock mockService = new();
- ChatOptions? capturedChatOptions = null;
- mockService.Setup(
- s => s.GetResponseAsync(
- It.IsAny>(),
- It.IsAny(),
- It.IsAny()))
- .Callback, ChatOptions, CancellationToken>((msgs, opts, ct) =>
- capturedChatOptions = opts)
- .ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
-
- ChatClientAgent agent = new(mockService.Object, options: new()
- {
- ChatOptions = agentChatOptions
- });
- var messages = new List { new(ChatRole.User, "test") };
-
- // Act
- await agent.RunAsync(messages, options: new ChatClientAgentRunOptions(requestChatOptions));
-
- // Assert
- Assert.NotNull(capturedChatOptions);
- Assert.Equivalent(expectedChatOptionsMerge, capturedChatOptions); // Should be the equivalent instance (modified in place)
-
- // Request values should take priority
- Assert.Equal(200, capturedChatOptions.MaxOutputTokens);
- Assert.Equal(0.3f, capturedChatOptions.Temperature);
-
- // Merge StopSequences
- Assert.Equal(["request-stop", "agent-stop"], capturedChatOptions.StopSequences);
-
- // Agent values should be used when request doesn't specify
- Assert.Equal(0.9f, capturedChatOptions.TopP);
- Assert.Equal(50, capturedChatOptions.TopK);
- Assert.Equal(0.1f, capturedChatOptions.PresencePenalty);
- Assert.Equal(0.2f, capturedChatOptions.FrequencyPenalty);
- Assert.Equal("agent-model", capturedChatOptions.ModelId);
- Assert.Equal(12345, capturedChatOptions.Seed);
- Assert.Equal("agent-conversation", capturedChatOptions.ConversationId);
- Assert.Equal(true, capturedChatOptions.AllowMultipleToolCalls);
- }
-
- #endregion
-
#region GetService Method Tests
///
diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_ChatOptionsMergingTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_ChatOptionsMergingTests.cs
new file mode 100644
index 0000000000..70f13e5b2a
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_ChatOptionsMergingTests.cs
@@ -0,0 +1,442 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.AI;
+using Moq;
+
+namespace Microsoft.Agents.AI.UnitTests.ChatClient;
+
+///
+/// Contains tests for merging in .
+///
+public class ChatClientAgent_ChatOptionsMergingTests
+{
+ ///
+ /// Verify that ChatOptions merging works when agent has ChatOptions but request doesn't.
+ ///
+ [Fact]
+ public async Task ChatOptionsMergingUsesAgentOptionsWhenRequestHasNoneAsync()
+ {
+ // Arrange
+ var agentChatOptions = new ChatOptions { MaxOutputTokens = 100, Temperature = 0.7f, Instructions = "test instructions" };
+ Mock mockService = new();
+ ChatOptions? capturedChatOptions = null;
+ mockService.Setup(
+ s => s.GetResponseAsync(
+ It.IsAny>(),
+ It.IsAny(),
+ It.IsAny()))
+ .Callback, ChatOptions, CancellationToken>((msgs, opts, ct) =>
+ capturedChatOptions = opts)
+ .ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
+
+ ChatClientAgent agent = new(mockService.Object, options: new()
+ {
+ ChatOptions = agentChatOptions
+ });
+ var messages = new List { new(ChatRole.User, "test") };
+
+ // Act
+ await agent.RunAsync(messages);
+
+ // Assert
+ Assert.NotNull(capturedChatOptions);
+ Assert.Equal(100, capturedChatOptions.MaxOutputTokens);
+ Assert.Equal(0.7f, capturedChatOptions.Temperature);
+ Assert.Equal("test instructions", capturedChatOptions.Instructions);
+ }
+
+ [Fact]
+ public async Task ChatOptionsMergingUsesAgentOptionsConstructorWhenRequestHasNoneAsync()
+ {
+ Mock mockService = new();
+ ChatOptions? capturedChatOptions = null;
+ mockService.Setup(
+ s => s.GetResponseAsync(
+ It.IsAny>(),
+ It.IsAny(),
+ It.IsAny()))
+ .Callback, ChatOptions, CancellationToken>((msgs, opts, ct) =>
+ capturedChatOptions = opts)
+ .ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
+
+ ChatClientAgent agent = new(mockService.Object, options: new() { ChatOptions = new() { Instructions = "test instructions" } });
+ var messages = new List { new(ChatRole.User, "test") };
+
+ // Act
+ await agent.RunAsync(messages);
+
+ // Assert
+ Assert.NotNull(capturedChatOptions);
+ Assert.Equal("test instructions", capturedChatOptions.Instructions);
+ }
+
+ ///
+ /// Verify that ChatOptions merging works when request has ChatOptions but agent doesn't.
+ ///
+ [Fact]
+ public async Task ChatOptionsMergingUsesRequestOptionsWhenAgentHasNoneAsync()
+ {
+ // Arrange
+ var requestChatOptions = new ChatOptions { MaxOutputTokens = 200, Temperature = 0.3f, Instructions = "test instructions" };
+ Mock mockService = new();
+ ChatOptions? capturedChatOptions = null;
+ mockService.Setup(
+ s => s.GetResponseAsync(
+ It.IsAny>(),
+ It.IsAny(),
+ It.IsAny()))
+ .Callback, ChatOptions, CancellationToken>((msgs, opts, ct) =>
+ capturedChatOptions = opts)
+ .ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
+
+ ChatClientAgent agent = new(mockService.Object);
+ var messages = new List { new(ChatRole.User, "test") };
+
+ // Act
+ await agent.RunAsync(messages, options: new ChatClientAgentRunOptions(requestChatOptions));
+
+ // Assert
+ Assert.NotNull(capturedChatOptions);
+ Assert.Equivalent(requestChatOptions, capturedChatOptions); // Should be the same instance since no merging needed
+ Assert.Equal(200, capturedChatOptions.MaxOutputTokens);
+ Assert.Equal(0.3f, capturedChatOptions.Temperature);
+ Assert.Equal("test instructions", capturedChatOptions.Instructions);
+ }
+
+ ///
+ /// Verify that merging prioritizes over request and that in turn over agent level .
+ ///
+ [Fact]
+ public async Task ChatOptionsMergingPrioritizesRequestOptionsOverAgentOptionsAsync()
+ {
+ // Arrange
+ var agentChatOptions = new ChatOptions
+ {
+ Instructions = "test instructions",
+ MaxOutputTokens = 100,
+ Temperature = 0.7f,
+ TopP = 0.9f,
+ ModelId = "agent-model",
+ AdditionalProperties = new AdditionalPropertiesDictionary { ["key1"] = "agent-value", ["key2"] = "agent-value", ["key3"] = "agent-value" }
+ };
+ var requestChatOptions = new ChatOptions
+ {
+ // TopP and ModelId not set, should use agent values
+ MaxOutputTokens = 200,
+ Temperature = 0.3f,
+ AdditionalProperties = new AdditionalPropertiesDictionary { ["key2"] = "request-value", ["key3"] = "request-value" },
+ Instructions = "request instructions"
+ };
+ var agentRunOptionsAdditionalProperties = new AdditionalPropertiesDictionary { ["key3"] = "runoptions-value" };
+ var expectedChatOptionsMerge = new ChatOptions
+ {
+ MaxOutputTokens = 200, // Request value takes priority
+ Temperature = 0.3f, // Request value takes priority
+ // Check that each level of presedence is respected in AdditionalProperties
+ AdditionalProperties = new AdditionalPropertiesDictionary { ["key1"] = "agent-value", ["key2"] = "request-value", ["key3"] = "runoptions-value" },
+ TopP = 0.9f, // Agent value used when request doesn't specify
+ ModelId = "agent-model", // Agent value used when request doesn't specify
+ Instructions = "test instructions\nrequest instructions" // Request is in addition to agent instructions
+ };
+
+ Mock mockService = new();
+ ChatOptions? capturedChatOptions = null;
+ mockService.Setup(
+ s => s.GetResponseAsync(
+ It.IsAny>(),
+ It.IsAny(),
+ It.IsAny()))
+ .Callback, ChatOptions, CancellationToken>((msgs, opts, ct) =>
+ capturedChatOptions = opts)
+ .ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
+
+ ChatClientAgent agent = new(mockService.Object, options: new()
+ {
+ ChatOptions = agentChatOptions
+ });
+ var messages = new List { new(ChatRole.User, "test") };
+
+ // Act
+ await agent.RunAsync(messages, options: new ChatClientAgentRunOptions(requestChatOptions) { AdditionalProperties = agentRunOptionsAdditionalProperties });
+
+ // Assert
+ Assert.NotNull(capturedChatOptions);
+ Assert.Equivalent(expectedChatOptionsMerge, capturedChatOptions); // Should be the same instance (modified in place)
+ Assert.Equal(200, capturedChatOptions.MaxOutputTokens); // Request value takes priority
+ Assert.Equal(0.3f, capturedChatOptions.Temperature); // Request value takes priority
+ Assert.NotNull(capturedChatOptions.AdditionalProperties);
+ Assert.Equal("agent-value", capturedChatOptions.AdditionalProperties["key1"]); // Agent value used when request doesn't specify
+ Assert.Equal("request-value", capturedChatOptions.AdditionalProperties["key2"]); // Request ChatOptions value takes priority over agent ChatOptions value
+ Assert.Equal("runoptions-value", capturedChatOptions.AdditionalProperties["key3"]); // Run options value takes priority over request and agent ChatOptions values
+ Assert.Equal(0.9f, capturedChatOptions.TopP); // Agent value used when request doesn't specify
+ Assert.Equal("agent-model", capturedChatOptions.ModelId); // Agent value used when request doesn't specify
+ }
+
+ ///
+ /// Verify that ChatOptions merging returns null when both agent and request have no ChatOptions.
+ ///
+ [Fact]
+ public async Task ChatOptionsMergingReturnsNullWhenBothAgentAndRequestHaveNoneAsync()
+ {
+ // Arrange
+ Mock mockService = new();
+ ChatOptions? capturedChatOptions = null;
+ mockService.Setup(
+ s => s.GetResponseAsync(
+ It.IsAny>(),
+ It.IsAny(),
+ It.IsAny()))
+ .Callback, ChatOptions, CancellationToken>((msgs, opts, ct) =>
+ capturedChatOptions = opts)
+ .ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
+
+ ChatClientAgent agent = new(mockService.Object);
+ var messages = new List { new(ChatRole.User, "test") };
+
+ // Act
+ await agent.RunAsync(messages);
+
+ // Assert
+ Assert.Null(capturedChatOptions);
+ }
+
+ ///
+ /// Verify that ChatOptions merging concatenates Tools from agent and request.
+ ///
+ [Fact]
+ public async Task ChatOptionsMergingConcatenatesToolsFromAgentAndRequestAsync()
+ {
+ // Arrange
+ var agentTool = AIFunctionFactory.Create(() => "agent tool");
+ var requestTool = AIFunctionFactory.Create(() => "request tool");
+
+ var agentChatOptions = new ChatOptions
+ {
+ Instructions = "test instructions",
+ Tools = [agentTool]
+ };
+ var requestChatOptions = new ChatOptions
+ {
+ Tools = [requestTool]
+ };
+
+ Mock mockService = new();
+ ChatOptions? capturedChatOptions = null;
+ mockService.Setup(
+ s => s.GetResponseAsync(
+ It.IsAny>(),
+ It.IsAny(),
+ It.IsAny()))
+ .Callback, ChatOptions, CancellationToken>((msgs, opts, ct) =>
+ capturedChatOptions = opts)
+ .ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
+
+ ChatClientAgent agent = new(mockService.Object, options: new()
+ {
+ ChatOptions = agentChatOptions
+ });
+ var messages = new List { new(ChatRole.User, "test") };
+
+ // Act
+ await agent.RunAsync(messages, options: new ChatClientAgentRunOptions(requestChatOptions));
+
+ // Assert
+ Assert.NotNull(capturedChatOptions);
+ Assert.NotNull(capturedChatOptions.Tools);
+ Assert.Equal(2, capturedChatOptions.Tools.Count);
+
+ // Request tools should come first, then agent tools
+ Assert.Contains(requestTool, capturedChatOptions.Tools);
+ Assert.Contains(agentTool, capturedChatOptions.Tools);
+ }
+
+ ///
+ /// Verify that ChatOptions merging uses agent Tools when request has no Tools.
+ ///
+ [Fact]
+ public async Task ChatOptionsMergingUsesAgentToolsWhenRequestHasNoToolsAsync()
+ {
+ // Arrange
+ var agentTool = AIFunctionFactory.Create(() => "agent tool");
+
+ var agentChatOptions = new ChatOptions
+ {
+ Instructions = "test instructions",
+ Tools = [agentTool]
+ };
+ var requestChatOptions = new ChatOptions
+ {
+ // No Tools specified
+ MaxOutputTokens = 100
+ };
+
+ Mock mockService = new();
+ ChatOptions? capturedChatOptions = null;
+ mockService.Setup(
+ s => s.GetResponseAsync(
+ It.IsAny>(),
+ It.IsAny(),
+ It.IsAny()))
+ .Callback, ChatOptions, CancellationToken>((msgs, opts, ct) =>
+ capturedChatOptions = opts)
+ .ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
+
+ ChatClientAgent agent = new(mockService.Object, options: new()
+ {
+ ChatOptions = agentChatOptions
+ });
+ var messages = new List { new(ChatRole.User, "test") };
+
+ // Act
+ await agent.RunAsync(messages, options: new ChatClientAgentRunOptions(requestChatOptions));
+
+ // Assert
+ Assert.NotNull(capturedChatOptions);
+ Assert.NotNull(capturedChatOptions.Tools);
+ Assert.Single(capturedChatOptions.Tools);
+ Assert.Contains(agentTool, capturedChatOptions.Tools); // Should contain the agent's tool
+ }
+
+ ///
+ /// Verify that ChatOptions merging uses RawRepresentationFactory from request first, with fallback to agent.
+ ///
+ [Theory]
+ [InlineData("MockAgentSetting", "MockRequestSetting", "MockRequestSetting")]
+ [InlineData("MockAgentSetting", null, "MockAgentSetting")]
+ [InlineData(null, "MockRequestSetting", "MockRequestSetting")]
+ public async Task ChatOptionsMergingUsesRawRepresentationFactoryWithFallbackAsync(string? agentSetting, string? requestSetting, string expectedSetting)
+ {
+ // Arrange
+ var agentChatOptions = new ChatOptions
+ {
+ Instructions = "test instructions",
+ RawRepresentationFactory = _ => agentSetting
+ };
+ var requestChatOptions = new ChatOptions
+ {
+ RawRepresentationFactory = _ => requestSetting
+ };
+
+ Mock mockService = new();
+ ChatOptions? capturedChatOptions = null;
+ mockService.Setup(
+ s => s.GetResponseAsync(
+ It.IsAny>(),
+ It.IsAny(),
+ It.IsAny()))
+ .Callback, ChatOptions, CancellationToken>((msgs, opts, ct) =>
+ capturedChatOptions = opts)
+ .ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
+
+ ChatClientAgent agent = new(mockService.Object, options: new()
+ {
+ ChatOptions = agentChatOptions
+ });
+ var messages = new List { new(ChatRole.User, "test") };
+
+ // Act
+ await agent.RunAsync(messages, options: new ChatClientAgentRunOptions(requestChatOptions));
+
+ // Assert
+ Assert.NotNull(capturedChatOptions);
+ Assert.NotNull(capturedChatOptions.RawRepresentationFactory);
+ Assert.Equal(expectedSetting, capturedChatOptions.RawRepresentationFactory(null!));
+ }
+
+ ///
+ /// Verify that ChatOptions merging handles all scalar properties correctly.
+ ///
+ [Fact]
+ public async Task ChatOptionsMergingHandlesAllScalarPropertiesCorrectlyAsync()
+ {
+ // Arrange
+ var agentChatOptions = new ChatOptions
+ {
+ MaxOutputTokens = 100,
+ Temperature = 0.7f,
+ TopP = 0.9f,
+ TopK = 50,
+ PresencePenalty = 0.1f,
+ FrequencyPenalty = 0.2f,
+ Instructions = "agent instructions",
+ ModelId = "agent-model",
+ Seed = 12345,
+ ConversationId = "agent-conversation",
+ AllowMultipleToolCalls = true,
+ StopSequences = ["agent-stop"]
+ };
+ var requestChatOptions = new ChatOptions
+ {
+ MaxOutputTokens = 200,
+ Temperature = 0.3f,
+ Instructions = "request instructions",
+
+ // Other properties not set, should use agent values
+ StopSequences = ["request-stop"]
+ };
+
+ var expectedChatOptionsMerge = new ChatOptions
+ {
+ MaxOutputTokens = 200,
+ Temperature = 0.3f,
+
+ // Agent value used when request doesn't specify
+ TopP = 0.9f,
+ TopK = 50,
+ PresencePenalty = 0.1f,
+ FrequencyPenalty = 0.2f,
+ Instructions = "agent instructions\nrequest instructions",
+ ModelId = "agent-model",
+ Seed = 12345,
+ ConversationId = "agent-conversation",
+ AllowMultipleToolCalls = true,
+
+ // Merged StopSequences
+ StopSequences = ["request-stop", "agent-stop"]
+ };
+
+ Mock mockService = new();
+ ChatOptions? capturedChatOptions = null;
+ mockService.Setup(
+ s => s.GetResponseAsync(
+ It.IsAny>(),
+ It.IsAny(),
+ It.IsAny()))
+ .Callback, ChatOptions, CancellationToken>((msgs, opts, ct) =>
+ capturedChatOptions = opts)
+ .ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]));
+
+ ChatClientAgent agent = new(mockService.Object, options: new()
+ {
+ ChatOptions = agentChatOptions
+ });
+ var messages = new List { new(ChatRole.User, "test") };
+
+ // Act
+ await agent.RunAsync(messages, options: new ChatClientAgentRunOptions(requestChatOptions));
+
+ // Assert
+ Assert.NotNull(capturedChatOptions);
+ Assert.Equivalent(expectedChatOptionsMerge, capturedChatOptions); // Should be the equivalent instance (modified in place)
+
+ // Request values should take priority
+ Assert.Equal(200, capturedChatOptions.MaxOutputTokens);
+ Assert.Equal(0.3f, capturedChatOptions.Temperature);
+
+ // Merge StopSequences
+ Assert.Equal(["request-stop", "agent-stop"], capturedChatOptions.StopSequences);
+
+ // Agent values should be used when request doesn't specify
+ Assert.Equal(0.9f, capturedChatOptions.TopP);
+ Assert.Equal(50, capturedChatOptions.TopK);
+ Assert.Equal(0.1f, capturedChatOptions.PresencePenalty);
+ Assert.Equal(0.2f, capturedChatOptions.FrequencyPenalty);
+ Assert.Equal("agent-model", capturedChatOptions.ModelId);
+ Assert.Equal(12345, capturedChatOptions.Seed);
+ Assert.Equal("agent-conversation", capturedChatOptions.ConversationId);
+ Assert.Equal(true, capturedChatOptions.AllowMultipleToolCalls);
+ }
+}
From 5d994bf71e515fc3add2af1305c64d3d75aa31f4 Mon Sep 17 00:00:00 2001
From: westey <164392973+westey-m@users.noreply.github.com>
Date: Tue, 13 Jan 2026 10:37:23 +0000
Subject: [PATCH 2/2] Fix namespace and typo.
---
.../ChatClient/ChatClientAgent_ChatOptionsMergingTests.cs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_ChatOptionsMergingTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_ChatOptionsMergingTests.cs
index 70f13e5b2a..6dda0f0278 100644
--- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_ChatOptionsMergingTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgent_ChatOptionsMergingTests.cs
@@ -6,7 +6,7 @@
using Microsoft.Extensions.AI;
using Moq;
-namespace Microsoft.Agents.AI.UnitTests.ChatClient;
+namespace Microsoft.Agents.AI.UnitTests;
///
/// Contains tests for merging in .
@@ -135,7 +135,7 @@ public async Task ChatOptionsMergingPrioritizesRequestOptionsOverAgentOptionsAsy
{
MaxOutputTokens = 200, // Request value takes priority
Temperature = 0.3f, // Request value takes priority
- // Check that each level of presedence is respected in AdditionalProperties
+ // Check that each level of precedence is respected in AdditionalProperties
AdditionalProperties = new AdditionalPropertiesDictionary { ["key1"] = "agent-value", ["key2"] = "request-value", ["key3"] = "runoptions-value" },
TopP = 0.9f, // Agent value used when request doesn't specify
ModelId = "agent-model", // Agent value used when request doesn't specify