.NET: Add support for background responses (#1501)

* add support for background responses

* Update dotnet/src/Microsoft.Agents.AI.Abstractions/AgentRunResponseUpdate.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix broken link

* fix xml comments and background responses properties override funcitonity

* change ai model provider

* use Run{Streaming}Async overloads that don't require messages

* stop using m: prefix in cref attribute of <see/> element.

* reject input messages provided with continuation token + don't extract messages from message store and context provide if continuation token is provided

* use agent thread for background-responses sample

* require agent thread for background responses

* Update dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs

Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>

* Update dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs

Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>

* remove CA1200

* Update dotnet/src/Microsoft.Agents.AI.Abstractions/AgentRunOptions.cs

Co-authored-by: westey <164392973+westey-m@users.noreply.github.com>

* Update dotnet/src/Microsoft.Agents.AI.Abstractions/AgentRunResponse.cs

Co-authored-by: westey <164392973+westey-m@users.noreply.github.com>

* Update dotnet/src/Microsoft.Agents.AI.Abstractions/AgentRunResponse.cs

Co-authored-by: westey <164392973+westey-m@users.noreply.github.com>

* address pr review comments

* Update dotnet/samples/GettingStarted/Agents/Agent_Step17_BackgroundResponses/Program.cs

Co-authored-by: westey <164392973+westey-m@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
Co-authored-by: westey <164392973+westey-m@users.noreply.github.com>
This commit is contained in:
SergeyMenshykh
2025-10-22 17:43:57 +00:00
committed by GitHub
co-authored by Copilot Roger Barreto westey
parent 699149c260
commit 1bf520a7c2
17 changed files with 2499 additions and 39 deletions
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Text.Json;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.Abstractions.UnitTests;
@@ -13,15 +15,44 @@ public class AgentRunOptionsTests
public void CloningConstructorCopiesProperties()
{
// Arrange
var options = new AgentRunOptions();
var options = new AgentRunOptions
{
ContinuationToken = new object(),
AllowBackgroundResponses = true
};
// Act
var clone = new AgentRunOptions(options);
// Assert
Assert.NotNull(clone);
Assert.Same(options.ContinuationToken, clone.ContinuationToken);
Assert.Equal(options.AllowBackgroundResponses, clone.AllowBackgroundResponses);
}
[Fact]
public void CloningConstructorThrowsIfNull() =>
// Act & Assert
Assert.Throws<ArgumentNullException>(() => new AgentRunOptions(null!));
[Fact]
public void JsonSerializationRoundtrips()
{
// Arrange
var options = new AgentRunOptions
{
ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }),
AllowBackgroundResponses = true
};
// Act
string json = JsonSerializer.Serialize(options, AgentAbstractionsJsonUtilities.DefaultOptions);
var deserialized = JsonSerializer.Deserialize<AgentRunOptions>(json, AgentAbstractionsJsonUtilities.DefaultOptions);
// Assert
Assert.NotNull(deserialized);
Assert.Equivalent(ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }), deserialized!.ContinuationToken);
Assert.Equal(options.AllowBackgroundResponses, deserialized.AllowBackgroundResponses);
}
}
@@ -19,10 +19,12 @@ public class AgentRunResponseTests
response = new();
Assert.Empty(response.Messages);
Assert.Empty(response.Text);
Assert.Null(response.ContinuationToken);
response = new((IList<ChatMessage>?)null);
Assert.Empty(response.Messages);
Assert.Empty(response.Text);
Assert.Null(response.ContinuationToken);
Assert.Throws<ArgumentNullException>("message", () => new AgentRunResponse((ChatMessage)null!));
}
@@ -55,6 +57,7 @@ public class AgentRunResponseTests
RawRepresentation = new object(),
ResponseId = "responseId",
Usage = new UsageDetails(),
ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }),
};
AgentRunResponse response = new(chatResponse);
@@ -64,6 +67,7 @@ public class AgentRunResponseTests
Assert.Equal(chatResponse.ResponseId, response.ResponseId);
Assert.Same(chatResponse, response.RawRepresentation as ChatResponse);
Assert.Same(chatResponse.Usage, response.Usage);
Assert.Equivalent(ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }), response.ContinuationToken);
}
[Fact]
@@ -97,6 +101,10 @@ public class AgentRunResponseTests
AdditionalPropertiesDictionary additionalProps = [];
response.AdditionalProperties = additionalProps;
Assert.Same(additionalProps, response.AdditionalProperties);
Assert.Null(response.ContinuationToken);
response.ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 });
Assert.Equivalent(ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }), response.ContinuationToken);
}
[Fact]
@@ -110,11 +118,12 @@ public class AgentRunResponseTests
Usage = new UsageDetails(),
RawRepresentation = new(),
AdditionalProperties = new() { ["key"] = "value" },
ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }),
};
string json = JsonSerializer.Serialize(original, TestJsonSerializerContext.Default.AgentRunResponse);
string json = JsonSerializer.Serialize(original, AgentAbstractionsJsonUtilities.DefaultOptions);
AgentRunResponse? result = JsonSerializer.Deserialize(json, TestJsonSerializerContext.Default.AgentRunResponse);
AgentRunResponse? result = JsonSerializer.Deserialize<AgentRunResponse>(json, AgentAbstractionsJsonUtilities.DefaultOptions);
Assert.NotNull(result);
Assert.Equal(ChatRole.Assistant, result.Messages.Single().Role);
@@ -130,6 +139,7 @@ public class AgentRunResponseTests
Assert.True(result.AdditionalProperties.TryGetValue("key", out object? value));
Assert.IsType<JsonElement>(value);
Assert.Equal("value", ((JsonElement)value!).GetString());
Assert.Equivalent(ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }), result.ContinuationToken);
}
[Fact]
@@ -23,6 +23,7 @@ public class AgentRunResponseUpdateTests
Assert.Null(update.MessageId);
Assert.Null(update.CreatedAt);
Assert.Equal(string.Empty, update.ToString());
Assert.Null(update.ContinuationToken);
}
[Fact]
@@ -41,6 +42,7 @@ public class AgentRunResponseUpdateTests
RawRepresentation = new object(),
ResponseId = "responseId",
Role = ChatRole.Assistant,
ContinuationToken = new object(),
};
AgentRunResponseUpdate response = new(chatResponseUpdate);
@@ -52,6 +54,7 @@ public class AgentRunResponseUpdateTests
Assert.Same(chatResponseUpdate, response.RawRepresentation as ChatResponseUpdate);
Assert.Equal(chatResponseUpdate.ResponseId, response.ResponseId);
Assert.Equal(chatResponseUpdate.Role, response.Role);
Assert.Same(chatResponseUpdate.ContinuationToken, response.ContinuationToken);
}
[Fact]
@@ -102,6 +105,10 @@ public class AgentRunResponseUpdateTests
Assert.Null(update.CreatedAt);
update.CreatedAt = new DateTimeOffset(2022, 1, 1, 0, 0, 0, TimeSpan.Zero);
Assert.Equal(new DateTimeOffset(2022, 1, 1, 0, 0, 0, TimeSpan.Zero), update.CreatedAt);
Assert.Null(update.ContinuationToken);
update.ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 });
Assert.Equivalent(ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }), update.ContinuationToken);
}
[Fact]
@@ -152,11 +159,12 @@ public class AgentRunResponseUpdateTests
MessageId = "messageid",
CreatedAt = new DateTimeOffset(2022, 1, 1, 0, 0, 0, TimeSpan.Zero),
AdditionalProperties = new() { ["key"] = "value" },
ContinuationToken = ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 })
};
string json = JsonSerializer.Serialize(original, TestJsonSerializerContext.Default.AgentRunResponseUpdate);
string json = JsonSerializer.Serialize(original, AgentAbstractionsJsonUtilities.DefaultOptions);
AgentRunResponseUpdate? result = JsonSerializer.Deserialize(json, TestJsonSerializerContext.Default.AgentRunResponseUpdate);
AgentRunResponseUpdate? result = JsonSerializer.Deserialize<AgentRunResponseUpdate>(json, AgentAbstractionsJsonUtilities.DefaultOptions);
Assert.NotNull(result);
Assert.Equal(5, result.Contents.Count);
@@ -187,5 +195,8 @@ public class AgentRunResponseUpdateTests
Assert.True(result.AdditionalProperties.TryGetValue("key", out object? value));
Assert.IsType<JsonElement>(value);
Assert.Equal("value", ((JsonElement)value!).GetString());
Assert.NotNull(result.ContinuationToken);
Assert.Equivalent(ResponseContinuationToken.FromBytes(new byte[] { 1, 2, 3 }), result.ContinuationToken);
}
}
@@ -1951,6 +1951,499 @@ public partial class ChatClientAgentTests
#endregion
#region Background Responses Tests
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task RunAsyncPropagatesBackgroundResponsesPropertiesToChatClientAsync(bool providePropsViaChatOptions)
{
// Arrange
object continuationToken = new();
ChatOptions? capturedChatOptions = null;
Mock<IChatClient> mockChatClient = new();
mockChatClient
.Setup(c => c.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.Callback<IEnumerable<ChatMessage>, ChatOptions, CancellationToken>((m, co, ct) => capturedChatOptions = co)
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]) { ContinuationToken = null });
AgentRunOptions agentRunOptions;
if (providePropsViaChatOptions)
{
ChatOptions chatOptions = new()
{
AllowBackgroundResponses = true,
ContinuationToken = continuationToken
};
agentRunOptions = new ChatClientAgentRunOptions(chatOptions);
}
else
{
agentRunOptions = new AgentRunOptions()
{
AllowBackgroundResponses = true,
ContinuationToken = continuationToken
};
}
ChatClientAgent agent = new(mockChatClient.Object);
ChatClientAgentThread thread = new();
// Act
await agent.RunAsync(thread, options: agentRunOptions);
// Assert
Assert.NotNull(capturedChatOptions);
Assert.True(capturedChatOptions.AllowBackgroundResponses);
Assert.Same(continuationToken, capturedChatOptions.ContinuationToken);
}
[Fact]
public async Task RunAsyncPrioritizesBackgroundResponsesPropertiesFromAgentRunOptionsOverOnesFromChatOptionsAsync()
{
// Arrange
object continuationToken1 = new();
object continuationToken2 = new();
ChatOptions? capturedChatOptions = null;
Mock<IChatClient> mockChatClient = new();
mockChatClient
.Setup(c => c.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.Callback<IEnumerable<ChatMessage>, ChatOptions, CancellationToken>((m, co, ct) => capturedChatOptions = co)
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "response")]) { ContinuationToken = null });
ChatOptions chatOptions = new()
{
AllowBackgroundResponses = true,
ContinuationToken = continuationToken1
};
ChatClientAgentRunOptions agentRunOptions = new(chatOptions)
{
AllowBackgroundResponses = false,
ContinuationToken = continuationToken2
};
ChatClientAgent agent = new(mockChatClient.Object);
// Act
await agent.RunAsync(options: agentRunOptions);
// Assert
Assert.NotNull(capturedChatOptions);
Assert.False(capturedChatOptions.AllowBackgroundResponses);
Assert.Same(continuationToken2, capturedChatOptions.ContinuationToken);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task RunStreamingAsyncPropagatesBackgroundResponsesPropertiesToChatClientAsync(bool providePropsViaChatOptions)
{
// Arrange
ChatResponseUpdate[] returnUpdates =
[
new ChatResponseUpdate(role: ChatRole.Assistant, content: "wh"),
new ChatResponseUpdate(role: ChatRole.Assistant, content: "at?"),
];
object continuationToken = new();
ChatOptions? capturedChatOptions = null;
Mock<IChatClient> mockChatClient = new();
mockChatClient
.Setup(c => c.GetStreamingResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.Callback<IEnumerable<ChatMessage>, ChatOptions, CancellationToken>((m, co, ct) => capturedChatOptions = co)
.Returns(ToAsyncEnumerableAsync(returnUpdates));
AgentRunOptions agentRunOptions;
if (providePropsViaChatOptions)
{
ChatOptions chatOptions = new()
{
AllowBackgroundResponses = true,
ContinuationToken = continuationToken
};
agentRunOptions = new ChatClientAgentRunOptions(chatOptions);
}
else
{
agentRunOptions = new AgentRunOptions()
{
AllowBackgroundResponses = true,
ContinuationToken = continuationToken
};
}
ChatClientAgent agent = new(mockChatClient.Object);
ChatClientAgentThread thread = new();
// Act
await foreach (var _ in agent.RunStreamingAsync(thread, options: agentRunOptions))
{
}
// Assert
Assert.NotNull(capturedChatOptions);
Assert.True(capturedChatOptions.AllowBackgroundResponses);
Assert.Same(continuationToken, capturedChatOptions.ContinuationToken);
}
[Fact]
public async Task RunStreamingAsyncPrioritizesBackgroundResponsesPropertiesFromAgentRunOptionsOverOnesFromChatOptionsAsync()
{
// Arrange
ChatResponseUpdate[] returnUpdates =
[
new ChatResponseUpdate(role: ChatRole.Assistant, content: "wh"),
];
object continuationToken1 = new();
object continuationToken2 = new();
ChatOptions? capturedChatOptions = null;
Mock<IChatClient> mockChatClient = new();
mockChatClient
.Setup(c => c.GetStreamingResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.Callback<IEnumerable<ChatMessage>, ChatOptions, CancellationToken>((m, co, ct) => capturedChatOptions = co)
.Returns(ToAsyncEnumerableAsync(returnUpdates));
ChatOptions chatOptions = new()
{
AllowBackgroundResponses = true,
ContinuationToken = continuationToken1
};
ChatClientAgentRunOptions agentRunOptions = new(chatOptions)
{
AllowBackgroundResponses = false,
ContinuationToken = continuationToken2
};
ChatClientAgent agent = new(mockChatClient.Object);
// Act
await foreach (var _ in agent.RunStreamingAsync(options: agentRunOptions))
{
}
// Assert
Assert.NotNull(capturedChatOptions);
Assert.False(capturedChatOptions.AllowBackgroundResponses);
Assert.Same(continuationToken2, capturedChatOptions.ContinuationToken);
}
[Fact]
public async Task RunAsyncPropagatesContinuationTokenFromChatResponseToAgentRunResponseAsync()
{
// Arrange
object continuationToken = new();
Mock<IChatClient> mockChatClient = new();
mockChatClient
.Setup(c => c.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions?>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "partial")]) { ContinuationToken = continuationToken });
ChatClientAgent agent = new(mockChatClient.Object);
var runOptions = new ChatClientAgentRunOptions(new ChatOptions { AllowBackgroundResponses = true });
ChatClientAgentThread thread = new();
// Act
var response = await agent.RunAsync([new(ChatRole.User, "hi")], thread, options: runOptions);
// Assert
Assert.Same(continuationToken, response.ContinuationToken);
}
[Fact]
public async Task RunStreamingAsyncPropagatesContinuationTokensFromUpdatesAsync()
{
// Arrange
object token1 = new();
ChatResponseUpdate[] expectedUpdates =
[
new ChatResponseUpdate(ChatRole.Assistant, "pa") { ContinuationToken = token1 },
new ChatResponseUpdate(ChatRole.Assistant, "rt") { ContinuationToken = null } // terminal
];
Mock<IChatClient> mockChatClient = new();
mockChatClient
.Setup(c => c.GetStreamingResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions?>(),
It.IsAny<CancellationToken>()))
.Returns(ToAsyncEnumerableAsync(expectedUpdates));
ChatClientAgent agent = new(mockChatClient.Object);
ChatClientAgentThread thread = new();
// Act
var actualUpdates = new List<AgentRunResponseUpdate>();
await foreach (var u in agent.RunStreamingAsync([new(ChatRole.User, "hi")], thread, options: new ChatClientAgentRunOptions(new ChatOptions { AllowBackgroundResponses = true })))
{
actualUpdates.Add(u);
}
// Assert
Assert.Equal(2, actualUpdates.Count);
Assert.Same(token1, actualUpdates[0].ContinuationToken);
Assert.Null(actualUpdates[1].ContinuationToken); // last update has null token
}
[Fact]
public async Task RunAsyncThrowsWhenMessagesProvidedWithContinuationTokenAsync()
{
// Arrange
Mock<IChatClient> mockChatClient = new();
ChatClientAgent agent = new(mockChatClient.Object);
AgentRunOptions runOptions = new() { ContinuationToken = new() };
IEnumerable<ChatMessage> inputMessages = [new ChatMessage(ChatRole.User, "test message")];
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(() => agent.RunAsync(inputMessages, options: runOptions));
// Verify that the IChatClient was never called due to early validation
mockChatClient.Verify(
c => c.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()),
Times.Never);
}
[Fact]
public async Task RunStreamingAsyncThrowsWhenMessagesProvidedWithContinuationTokenAsync()
{
// Arrange
Mock<IChatClient> mockChatClient = new();
ChatClientAgent agent = new(mockChatClient.Object);
AgentRunOptions runOptions = new() { ContinuationToken = new() };
IEnumerable<ChatMessage> inputMessages = [new ChatMessage(ChatRole.User, "test message")];
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(async () =>
{
await foreach (var update in agent.RunStreamingAsync(inputMessages, options: runOptions))
{
// Should not reach here
}
});
// Verify that the IChatClient was never called due to early validation
mockChatClient.Verify(
c => c.GetStreamingResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()),
Times.Never);
}
[Fact]
public async Task RunAsyncSkipsThreadMessagePopulationWithContinuationTokenAsync()
{
// Arrange
List<ChatMessage> capturedMessages = [];
// Create a mock message store that would normally provide messages
var mockMessageStore = new Mock<ChatMessageStore>();
mockMessageStore
.Setup(ms => ms.GetMessagesAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync([new(ChatRole.User, "Message from message store")]);
// Create a mock AI context provider that would normally provide context
var mockContextProvider = new Mock<AIContextProvider>();
mockContextProvider
.Setup(p => p.InvokingAsync(It.IsAny<AIContextProvider.InvokingContext>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new AIContext
{
Messages = [new(ChatRole.System, "Message from AI context")],
Instructions = "context instructions"
});
Mock<IChatClient> mockChatClient = new();
mockChatClient
.Setup(c => c.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.Callback<IEnumerable<ChatMessage>, ChatOptions, CancellationToken>((msgs, opts, ct) =>
capturedMessages.AddRange(msgs))
.ReturnsAsync(new ChatResponse([new(ChatRole.Assistant, "continued response")]));
ChatClientAgent agent = new(mockChatClient.Object);
// Create a thread with both message store and AI context provider
ChatClientAgentThread thread = new()
{
MessageStore = mockMessageStore.Object,
AIContextProvider = mockContextProvider.Object
};
AgentRunOptions runOptions = new() { ContinuationToken = new() };
// Act
await agent.RunAsync([], thread, options: runOptions);
// Assert
// With continuation token, thread message population should be skipped
Assert.Empty(capturedMessages);
// Verify that message store was never called due to continuation token
mockMessageStore.Verify(
ms => ms.GetMessagesAsync(It.IsAny<CancellationToken>()),
Times.Never);
// Verify that AI context provider was never called due to continuation token
mockContextProvider.Verify(
p => p.InvokingAsync(It.IsAny<AIContextProvider.InvokingContext>(), It.IsAny<CancellationToken>()),
Times.Never);
}
[Fact]
public async Task RunStreamingAsyncSkipsThreadMessagePopulationWithContinuationTokenAsync()
{
// Arrange
List<ChatMessage> capturedMessages = [];
// Create a mock message store that would normally provide messages
var mockMessageStore = new Mock<ChatMessageStore>();
mockMessageStore
.Setup(ms => ms.GetMessagesAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync([new(ChatRole.User, "Message from message store")]);
// Create a mock AI context provider that would normally provide context
var mockContextProvider = new Mock<AIContextProvider>();
mockContextProvider
.Setup(p => p.InvokingAsync(It.IsAny<AIContextProvider.InvokingContext>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new AIContext
{
Messages = [new(ChatRole.System, "Message from AI context")],
Instructions = "context instructions"
});
Mock<IChatClient> mockChatClient = new();
mockChatClient
.Setup(c => c.GetStreamingResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.Callback<IEnumerable<ChatMessage>, ChatOptions, CancellationToken>((msgs, opts, ct) =>
capturedMessages.AddRange(msgs))
.Returns(ToAsyncEnumerableAsync([new ChatResponseUpdate(role: ChatRole.Assistant, content: "continued response")]));
ChatClientAgent agent = new(mockChatClient.Object);
// Create a thread with both message store and AI context provider
ChatClientAgentThread thread = new()
{
MessageStore = mockMessageStore.Object,
AIContextProvider = mockContextProvider.Object
};
AgentRunOptions runOptions = new() { ContinuationToken = new() };
// Act
await agent.RunStreamingAsync([], thread, options: runOptions).ToListAsync();
// Assert
// With continuation token, thread message population should be skipped
Assert.Empty(capturedMessages);
// Verify that message store was never called due to continuation token
mockMessageStore.Verify(
ms => ms.GetMessagesAsync(It.IsAny<CancellationToken>()),
Times.Never);
// Verify that AI context provider was never called due to continuation token
mockContextProvider.Verify(
p => p.InvokingAsync(It.IsAny<AIContextProvider.InvokingContext>(), It.IsAny<CancellationToken>()),
Times.Never);
}
[Fact]
public async Task RunAsyncThrowsWhenNoThreadProvideForBackgroundResponsesAsync()
{
// Arrange
Mock<IChatClient> mockChatClient = new();
ChatClientAgent agent = new(mockChatClient.Object);
AgentRunOptions runOptions = new() { AllowBackgroundResponses = true };
IEnumerable<ChatMessage> inputMessages = [new ChatMessage(ChatRole.User, "test message")];
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(() => agent.RunAsync(inputMessages, options: runOptions));
// Verify that the IChatClient was never called due to early validation
mockChatClient.Verify(
c => c.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()),
Times.Never);
}
[Fact]
public async Task RunStreamingAsyncThrowsWhenNoThreadProvideForBackgroundResponsesAsync()
{
// Arrange
Mock<IChatClient> mockChatClient = new();
ChatClientAgent agent = new(mockChatClient.Object);
AgentRunOptions runOptions = new() { AllowBackgroundResponses = true };
IEnumerable<ChatMessage> inputMessages = [new ChatMessage(ChatRole.User, "test message")];
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(async () =>
{
await foreach (var update in agent.RunStreamingAsync(inputMessages, options: runOptions))
{
// Should not reach here
}
});
// Verify that the IChatClient was never called due to early validation
mockChatClient.Verify(
c => c.GetStreamingResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()),
Times.Never);
}
#endregion
private static async IAsyncEnumerable<T> ToAsyncEnumerableAsync<T>(IEnumerable<T> values)
{
await Task.Yield();