From 6b22b6bbc7744329bf212ad16764bd077fb834aa Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Thu, 28 Aug 2025 11:59:23 +0100 Subject: [PATCH] .NET: Fixes for agent abstractions to improve MEAI static analysis compliance (#498) * Fixes for agent abstractions to improve MEAI static analysis compliance * Fix build error --- .../AIAgent.cs | 50 ++++++++++++++----- .../AgentRunOptions.cs | 2 +- .../AgentRunResponse.cs | 14 ++++-- .../AgentRunResponseUpdate.cs | 2 +- .../AgentRunResponseUpdateExtensions.cs | 4 +- .../AgentThread.cs | 47 ++++++++--------- .../IChatMessageStore.cs | 1 + .../InMemoryChatMessageStore.cs | 4 +- .../ChatCompletion/ChatClientAgent.cs | 8 +-- .../{AgentTests.cs => AIAgentTests.cs} | 45 ++++++++++------- .../AgentRunOptionsTests.cs | 1 + ...ests.cs => AgentRunResponseUpdateTests.cs} | 0 .../AgentThreadTests.cs | 14 ++++-- 13 files changed, 120 insertions(+), 72 deletions(-) rename dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/{AgentTests.cs => AIAgentTests.cs} (88%) rename dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/{AgentRunResponseUpdatesTests.cs => AgentRunResponseUpdateTests.cs} (100%) diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/AIAgent.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/AIAgent.cs index 328d15352f..05bdf49039 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/AIAgent.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/AIAgent.cs @@ -100,7 +100,10 @@ public abstract class AIAgent /// /// Run the agent with no message assuming that all required instructions are already provided to the agent or on the thread. /// - /// The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent response. + /// + /// The conversation thread to continue with this invocation. If not provided, creates a new thread. + /// The thread will be mutated with the provided messages and agent response. + /// /// Optional parameters for agent invocation. /// The to monitor for cancellation requests. The default is . /// A containing the list of items. @@ -116,7 +119,10 @@ public abstract class AIAgent /// Run the agent with the provided message and arguments. /// /// The message to pass to the agent. - /// The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent response. + /// + /// The conversation thread to continue with this invocation. If not provided, creates a new thread. + /// The thread will be mutated with the provided messages and agent response. + /// /// Optional parameters for agent invocation. /// The to monitor for cancellation requests. The default is . /// A containing the list of items. @@ -129,7 +135,7 @@ public abstract class AIAgent AgentRunOptions? options = null, CancellationToken cancellationToken = default) { - Throw.IfNullOrWhitespace(message); + _ = Throw.IfNullOrWhitespace(message); return this.RunAsync(new ChatMessage(ChatRole.User, message), thread, options, cancellationToken); } @@ -138,7 +144,10 @@ public abstract class AIAgent /// Run the agent with the provided message and arguments. /// /// The message to pass to the agent. - /// The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent response. + /// + /// The conversation thread to continue with this invocation. If not provided, creates a new thread. + /// The thread will be mutated with the provided messages and agent response. + /// /// Optional parameters for agent invocation. /// The to monitor for cancellation requests. The default is . /// A containing the list of items. @@ -148,7 +157,7 @@ public abstract class AIAgent AgentRunOptions? options = null, CancellationToken cancellationToken = default) { - Throw.IfNull(message); + _ = Throw.IfNull(message); return this.RunAsync([message], thread, options, cancellationToken); } @@ -157,7 +166,10 @@ public abstract class AIAgent /// Run the agent with the provided message and arguments. /// /// The messages to pass to the agent. - /// The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent response. + /// + /// The conversation thread to continue with this invocation. If not provided, creates a new thread. + /// The thread will be mutated with the provided messages and agent response. + /// /// Optional parameters for agent invocation. /// The to monitor for cancellation requests. The default is . /// A containing the list of items. @@ -170,7 +182,10 @@ public abstract class AIAgent /// /// Run the agent with no message assuming that all required instructions are already provided to the agent or on the thread. /// - /// The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent response. + /// + /// The conversation thread to continue with this invocation. If not provided, creates a new thread. + /// The thread will be mutated with the provided messages and agent response. + /// /// Optional parameters for agent invocation. /// The to monitor for cancellation requests. The default is . /// An async list of response items that each contain a . @@ -186,7 +201,10 @@ public abstract class AIAgent /// Run the agent with the provided message and arguments. /// /// The message to pass to the agent. - /// The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent response. + /// + /// The conversation thread to continue with this invocation. If not provided, creates a new thread. + /// The thread will be mutated with the provided messages and agent response. + /// /// Optional parameters for agent invocation. /// The to monitor for cancellation requests. The default is . /// An async list of response items that each contain a . @@ -199,7 +217,7 @@ public abstract class AIAgent AgentRunOptions? options = null, CancellationToken cancellationToken = default) { - Throw.IfNullOrWhitespace(message); + _ = Throw.IfNullOrWhitespace(message); return this.RunStreamingAsync(new ChatMessage(ChatRole.User, message), thread, options, cancellationToken); } @@ -208,7 +226,10 @@ public abstract class AIAgent /// Run the agent with the provided message and arguments. /// /// The message to pass to the agent. - /// The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent response. + /// + /// The conversation thread to continue with this invocation. If not provided, creates a new thread. + /// The thread will be mutated with the provided messages and agent response. + /// /// Optional parameters for agent invocation. /// The to monitor for cancellation requests. The default is . /// An async list of response items that each contain a . @@ -218,7 +239,7 @@ public abstract class AIAgent AgentRunOptions? options = null, CancellationToken cancellationToken = default) { - Throw.IfNull(message); + _ = Throw.IfNull(message); return this.RunStreamingAsync([message], thread, options, cancellationToken); } @@ -227,7 +248,10 @@ public abstract class AIAgent /// Run the agent with the provided message and arguments. /// /// The messages to pass to the agent. - /// The conversation thread to continue with this invocation. If not provided, creates a new thread. The thread will be mutated with the provided messages and agent response. + /// + /// The conversation thread to continue with this invocation. If not provided, creates a new thread. + /// The thread will be mutated with the provided messages and agent response. + /// /// Optional parameters for agent invocation. /// The to monitor for cancellation requests. The default is . /// An async list of response items that each contain a . @@ -261,7 +285,7 @@ public abstract class AIAgent /// The messages to pass to the thread. /// The to monitor for cancellation requests. The default is . /// An async task that completes once the notification is complete. - protected async Task NotifyThreadOfNewMessagesAsync(AgentThread thread, IReadOnlyCollection messages, CancellationToken cancellationToken) + protected static async Task NotifyThreadOfNewMessagesAsync(AgentThread thread, IReadOnlyCollection messages, CancellationToken cancellationToken) { _ = Throw.IfNull(thread); _ = Throw.IfNull(messages); diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/AgentRunOptions.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/AgentRunOptions.cs index 83a98ffd4e..2b0784255c 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/AgentRunOptions.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/AgentRunOptions.cs @@ -22,6 +22,6 @@ public class AgentRunOptions /// The options to clone. public AgentRunOptions(AgentRunOptions options) { - Throw.IfNull(options); + _ = Throw.IfNull(options); } } diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/AgentRunResponse.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/AgentRunResponse.cs index 0f28bfbae9..f2591a4081 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/AgentRunResponse.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/AgentRunResponse.cs @@ -1,14 +1,14 @@ // Copyright (c) Microsoft. All rights reserved. using System; -#if NET8_0_OR_GREATER +#if NET9_0_OR_GREATER using System.Buffers; #endif using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; -#if NET8_0_OR_GREATER +#if NET9_0_OR_GREATER using System.Text; #endif using System.Text.Json; @@ -16,6 +16,9 @@ using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; using Microsoft.Shared.Diagnostics; +#pragma warning disable S109 // Magic numbers should not be used +#pragma warning disable S1121 // Assignments should not be made from within sub-expressions + namespace Microsoft.Extensions.AI.Agents; /// Represents the response to an Agent run request. @@ -84,7 +87,7 @@ public class AgentRunResponse [JsonIgnore] public string Text => this._messages?.ConcatText() ?? string.Empty; - /// Gets or sets the user input requests associated with the response. + /// Gets the user input requests associated with the response. /// /// This property concatenates all instances in the response. /// @@ -170,7 +173,6 @@ public class AgentRunResponse return updates; } - // TODO: Add overloads without serializer options. /// /// Deserializes the response text into the given type using the specified serializer options. /// @@ -180,6 +182,8 @@ public class AgentRunResponse /// The result is not parsable into the requested type. public T Deserialize(JsonSerializerOptions serializerOptions) { + _ = Throw.IfNull(serializerOptions); + var structuredOutput = this.GetResultCore(serializerOptions, out var failureReason); return failureReason switch { @@ -198,6 +202,8 @@ public class AgentRunResponse /// if parsing was successful; otherwise, . public bool TryDeserialize(JsonSerializerOptions serializerOptions, [NotNullWhen(true)] out T? structuredOutput) { + _ = Throw.IfNull(serializerOptions); + try { structuredOutput = this.GetResultCore(serializerOptions, out var failureReason); diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/AgentRunResponseUpdate.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/AgentRunResponseUpdate.cs index a40fac7c3c..4f158c9e81 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/AgentRunResponseUpdate.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/AgentRunResponseUpdate.cs @@ -93,7 +93,7 @@ public class AgentRunResponseUpdate [JsonIgnore] public string Text => this._contents is not null ? this._contents.ConcatText() : string.Empty; - /// Gets or sets the user input requests associated with the response. + /// Gets the user input requests associated with the response. /// /// This property concatenates all instances in the response. /// diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/AgentRunResponseUpdateExtensions.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/AgentRunResponseUpdateExtensions.cs index 4078ce5939..37ff85ffdd 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/AgentRunResponseUpdateExtensions.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/AgentRunResponseUpdateExtensions.cs @@ -2,12 +2,14 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.Shared.Diagnostics; +#pragma warning disable S109 // Magic numbers should not be used +#pragma warning disable S1121 // Assignments should not be made from within sub-expressions + namespace Microsoft.Extensions.AI.Agents; /// diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/AgentThread.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/AgentThread.cs index 1a8d33a53b..a705ba6530 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/AgentThread.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/AgentThread.cs @@ -51,7 +51,7 @@ public class AgentThread /// public string? ConversationId { - get { return this._conversationId; } + get => this._conversationId; set { if (string.IsNullOrWhiteSpace(this._conversationId) && string.IsNullOrWhiteSpace(value)) @@ -90,7 +90,7 @@ public class AgentThread /// public IChatMessageStore? MessageStore { - get { return this._messageStore; } + get => this._messageStore; set { if (this._messageStore is null && value is null) @@ -126,6 +126,27 @@ public class AgentThread } } + /// + /// Serializes the current object's state to a using the specified serialization options. + /// + /// The JSON serialization options to use. + /// The to monitor for cancellation requests. The default is . + /// A representation of the object's state. + public virtual async Task SerializeAsync(JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) + { + var storeState = this._messageStore is null ? + null : + await this._messageStore.SerializeStateAsync(jsonSerializerOptions, cancellationToken).ConfigureAwait(false); + + var state = new ThreadState + { + ConversationId = this.ConversationId, + StoreState = storeState + }; + + return JsonSerializer.SerializeToElement(state, AgentAbstractionsJsonUtilities.DefaultOptions.GetTypeInfo(typeof(ThreadState))); + } + /// /// This method is called when new messages have been contributed to the chat by any participant. /// @@ -168,6 +189,7 @@ public class AgentThread /// A representing the state of the thread. /// Optional settings for customizing the JSON deserialization process. /// The to monitor for cancellation requests. The default is . + /// A that completes when the state has been deserialized. protected internal virtual async Task DeserializeAsync(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) { var state = JsonSerializer.Deserialize( @@ -197,27 +219,6 @@ public class AgentThread await this._messageStore.DeserializeStateAsync(state!.StoreState.Value, jsonSerializerOptions, cancellationToken).ConfigureAwait(false); } - /// - /// Serializes the current object's state to a using the specified serialization options. - /// - /// The JSON serialization options to use. - /// The to monitor for cancellation requests. The default is . - /// A representation of the object's state. - public virtual async Task SerializeAsync(JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) - { - var storeState = this._messageStore is null ? - (JsonElement?)null : - await this._messageStore.SerializeStateAsync(jsonSerializerOptions, cancellationToken).ConfigureAwait(false); - - var state = new ThreadState - { - ConversationId = this.ConversationId, - StoreState = storeState - }; - - return JsonSerializer.SerializeToElement(state, AgentAbstractionsJsonUtilities.DefaultOptions.GetTypeInfo(typeof(ThreadState))); - } - internal class ThreadState { public string? ConversationId { get; set; } diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/IChatMessageStore.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/IChatMessageStore.cs index f54c091264..d1713e9336 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/IChatMessageStore.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/IChatMessageStore.cs @@ -50,6 +50,7 @@ public interface IChatMessageStore /// A representing the state of the store. /// Optional settings for customizing the JSON deserialization process. /// The to monitor for cancellation requests. The default is . + /// A that completes when the state has been deserialized. /// /// This method, together with can be used to save and load messages from a persistent store /// if this store only has messages in memory. diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/InMemoryChatMessageStore.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/InMemoryChatMessageStore.cs index 57dce94c71..b535f10081 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/InMemoryChatMessageStore.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents.Abstractions/InMemoryChatMessageStore.cs @@ -48,7 +48,7 @@ internal class InMemoryChatMessageStore : IList, IChatMessageStore { if (serializedStoreState is null) { - return new ValueTask(); + return default; } var state = JsonSerializer.Deserialize( @@ -60,7 +60,7 @@ internal class InMemoryChatMessageStore : IList, IChatMessageStore this._messages.AddRange(messages); } - return new ValueTask(); + return default; } /// diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents/ChatCompletion/ChatClientAgent.cs b/dotnet/src/Microsoft.Extensions.AI.Agents/ChatCompletion/ChatClientAgent.cs index f53b6248a8..e33b3a7f9f 100644 --- a/dotnet/src/Microsoft.Extensions.AI.Agents/ChatCompletion/ChatClientAgent.cs +++ b/dotnet/src/Microsoft.Extensions.AI.Agents/ChatCompletion/ChatClientAgent.cs @@ -122,7 +122,7 @@ public sealed class ChatClientAgent : AIAgent this.UpdateThreadWithTypeAndConversationId(safeThread, chatResponse.ConversationId); // Only notify the thread of new messages if the chatResponse was successful to avoid inconsistent messages state in the thread. - await this.NotifyThreadOfNewMessagesAsync(safeThread, messages, cancellationToken).ConfigureAwait(false); + await NotifyThreadOfNewMessagesAsync(safeThread, messages, cancellationToken).ConfigureAwait(false); // Ensure that the author name is set for each message in the response. foreach (ChatMessage chatResponseMessage in chatResponse.Messages) @@ -133,7 +133,7 @@ public sealed class ChatClientAgent : AIAgent // Convert the chat response messages to a valid IReadOnlyCollection for notification signatures below. var chatResponseMessages = chatResponse.Messages as IReadOnlyCollection ?? [.. chatResponse.Messages]; - await this.NotifyThreadOfNewMessagesAsync(safeThread, chatResponseMessages, cancellationToken).ConfigureAwait(false); + await NotifyThreadOfNewMessagesAsync(safeThread, chatResponseMessages, cancellationToken).ConfigureAwait(false); return new(chatResponse) { AgentId = this.Id }; } @@ -186,9 +186,9 @@ public sealed class ChatClientAgent : AIAgent this.UpdateThreadWithTypeAndConversationId(safeThread, chatResponse.ConversationId); // To avoid inconsistent state we only notify the thread of the input messages if no error occurs after the initial request. - await this.NotifyThreadOfNewMessagesAsync(safeThread, inputMessages, cancellationToken).ConfigureAwait(false); + await NotifyThreadOfNewMessagesAsync(safeThread, inputMessages, cancellationToken).ConfigureAwait(false); - await this.NotifyThreadOfNewMessagesAsync(safeThread, chatResponseMessages, cancellationToken).ConfigureAwait(false); + await NotifyThreadOfNewMessagesAsync(safeThread, chatResponseMessages, cancellationToken).ConfigureAwait(false); } /// diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/AgentTests.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/AIAgentTests.cs similarity index 88% rename from dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/AgentTests.cs rename to dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/AIAgentTests.cs index ec8175f26b..5456c1b548 100644 --- a/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/AgentTests.cs +++ b/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/AIAgentTests.cs @@ -13,24 +13,24 @@ namespace Microsoft.Extensions.AI.Agents.Abstractions.UnitTests; /// /// Unit tests for the class. /// -public class AgentTests +public class AIAgentTests { private readonly Mock _agentMock; private readonly Mock _agentThreadMock; - private readonly AgentRunResponse _invokeResponse = new(); + private readonly AgentRunResponse _invokeResponse; private readonly List _invokeStreamingResponses = []; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - public AgentTests() + public AIAgentTests() { this._agentThreadMock = new Mock(MockBehavior.Strict); this._invokeResponse = new AgentRunResponse(new ChatMessage(ChatRole.Assistant, "Hi")); this._invokeStreamingResponses.Add(new AgentRunResponseUpdate(ChatRole.Assistant, "Hi")); - this._agentMock = new Mock() { CallBase = true }; + this._agentMock = new Mock { CallBase = true }; this._agentMock .Setup(x => x.RunAsync( It.IsAny>(), @@ -56,7 +56,7 @@ public class AgentTests { // Arrange var options = new AgentRunOptions(); - var cancellationToken = new CancellationToken(); + var cancellationToken = default(CancellationToken); // Act var response = await this._agentMock.Object.RunAsync(this._agentThreadMock.Object, options, cancellationToken); @@ -82,7 +82,7 @@ public class AgentTests // Arrange var message = "Hello, Agent!"; var options = new AgentRunOptions(); - var cancellationToken = new CancellationToken(); + var cancellationToken = default(CancellationToken); // Act var response = await this._agentMock.Object.RunAsync(message, this._agentThreadMock.Object, options, cancellationToken); @@ -108,7 +108,7 @@ public class AgentTests // Arrange var message = new ChatMessage(ChatRole.User, "Hello, Agent!"); var options = new AgentRunOptions(); - var cancellationToken = new CancellationToken(); + var cancellationToken = default(CancellationToken); // Act var response = await this._agentMock.Object.RunAsync(message, this._agentThreadMock.Object, options, cancellationToken); @@ -133,7 +133,7 @@ public class AgentTests { // Arrange var options = new AgentRunOptions(); - var cancellationToken = new CancellationToken(); + var cancellationToken = default(CancellationToken); // Act await foreach (var response in this._agentMock.Object.RunStreamingAsync(this._agentThreadMock.Object, options, cancellationToken)) @@ -162,7 +162,7 @@ public class AgentTests // Arrange var message = "Hello, Agent!"; var options = new AgentRunOptions(); - var cancellationToken = new CancellationToken(); + var cancellationToken = default(CancellationToken); // Act await foreach (var response in this._agentMock.Object.RunStreamingAsync(message, this._agentThreadMock.Object, options, cancellationToken)) @@ -191,7 +191,7 @@ public class AgentTests // Arrange var message = new ChatMessage(ChatRole.User, "Hello, Agent!"); var options = new AgentRunOptions(); - var cancellationToken = new CancellationToken(); + var cancellationToken = default(CancellationToken); // Act await foreach (var response in this._agentMock.Object.RunStreamingAsync(message, this._agentThreadMock.Object, options, cancellationToken)) @@ -223,16 +223,15 @@ public class AgentTests [Fact] public async Task NotifyThreadOfNewMessagesNotifiesThreadAsync() { - var cancellationToken = new CancellationToken(); + var cancellationToken = default(CancellationToken); var messages = new[] { new ChatMessage(ChatRole.User, "msg1"), new ChatMessage(ChatRole.User, "msg2") }; - var threadMock = new Mock() { CallBase = true }; + var threadMock = new Mock { CallBase = true }; threadMock.SetupAllProperties(); threadMock.Object.ConversationId = "test-thread-id"; - var agent = new MockAgent(); - await agent.NotifyThreadOfNewMessagesAsync(threadMock.Object, messages, cancellationToken); + await MockAgent.NotifyThreadOfNewMessagesAsync(threadMock.Object, messages, cancellationToken); threadMock.Protected().Verify("OnNewMessagesAsync", Times.Once(), messages, cancellationToken); } @@ -360,17 +359,25 @@ public class AgentTests private sealed class MockAgent : AIAgent { - public new Task NotifyThreadOfNewMessagesAsync(AgentThread thread, IReadOnlyCollection messages, CancellationToken cancellationToken) + public static new Task NotifyThreadOfNewMessagesAsync(AgentThread thread, IReadOnlyCollection messages, CancellationToken cancellationToken) { - return base.NotifyThreadOfNewMessagesAsync(thread, messages, cancellationToken); + return AIAgent.NotifyThreadOfNewMessagesAsync(thread, messages, cancellationToken); } - public override Task RunAsync(IReadOnlyCollection messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) + public override Task RunAsync( + IReadOnlyCollection messages, + AgentThread? thread = null, + AgentRunOptions? options = null, + CancellationToken cancellationToken = default) { throw new System.NotImplementedException(); } - public override IAsyncEnumerable RunStreamingAsync(IReadOnlyCollection messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) + public override IAsyncEnumerable RunStreamingAsync( + IReadOnlyCollection messages, + AgentThread? thread = null, + AgentRunOptions? options = null, + CancellationToken cancellationToken = default) { throw new System.NotImplementedException(); } diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/AgentRunOptionsTests.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/AgentRunOptionsTests.cs index 473501a554..7ea4b83ff0 100644 --- a/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/AgentRunOptionsTests.cs +++ b/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/AgentRunOptionsTests.cs @@ -17,6 +17,7 @@ public class AgentRunOptionsTests // Act var clone = new AgentRunOptions(options); + Assert.NotNull(clone); } [Fact] diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/AgentRunResponseUpdatesTests.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/AgentRunResponseUpdateTests.cs similarity index 100% rename from dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/AgentRunResponseUpdatesTests.cs rename to dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/AgentRunResponseUpdateTests.cs diff --git a/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/AgentThreadTests.cs b/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/AgentThreadTests.cs index 3a598c36a6..09c00a6c8d 100644 --- a/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/AgentThreadTests.cs +++ b/dotnet/tests/Microsoft.Extensions.AI.Agents.Abstractions.UnitTests/AgentThreadTests.cs @@ -59,8 +59,10 @@ public class AgentThreadTests public void SetConversationIdThrowsWhenMessageStoreIsSet() { // Arrange - var thread = new AgentThread(); - thread.MessageStore = new InMemoryChatMessageStore(); + var thread = new AgentThread + { + MessageStore = new InMemoryChatMessageStore() + }; // Act & Assert var exception = Assert.Throws(() => thread.ConversationId = "new-thread-id"); @@ -72,8 +74,10 @@ public class AgentThreadTests public void SetChatMessageStoreThrowsWhenConversationIdIsSet() { // Arrange - var thread = new AgentThread(); - thread.ConversationId = "existing-thread-id"; + var thread = new AgentThread + { + ConversationId = "existing-thread-id" + }; var store = new InMemoryChatMessageStore(); // Act & Assert @@ -149,6 +153,8 @@ public class AgentThreadTests // Act await thread.OnNewMessagesAsync(messages, CancellationToken.None); + Assert.Equal("thread-123", thread.ConversationId); + Assert.Null(thread.MessageStore); } [Fact]