From 6fcd2e8d81499630a6ea5194f998c50c3a26ec6e Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Tue, 25 Nov 2025 11:16:21 +0000 Subject: [PATCH 1/3] Remove NotifyThreadOfNewMessagesAsync helper from AIAgent --- .../Program.cs | 14 ++++- .../AIAgent.cs | 24 ------- .../AgentThread.cs | 17 ----- .../InMemoryAgentThread.cs | 6 -- .../WorkflowThread.cs | 3 - .../ChatClient/ChatClientAgent.cs | 18 +++++- .../ChatClient/ChatClientAgentThread.cs | 31 --------- .../AIAgentTests.cs | 19 ------ .../AgentThreadTests.cs | 11 ---- .../ChatClient/ChatClientAgentThreadTests.cs | 63 ------------------- 10 files changed, 28 insertions(+), 178 deletions(-) diff --git a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_CustomImplementation/Program.cs b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_CustomImplementation/Program.cs index fd00618f5f..8f1039251d 100644 --- a/dotnet/samples/GettingStarted/AgentProviders/Agent_With_CustomImplementation/Program.cs +++ b/dotnet/samples/GettingStarted/AgentProviders/Agent_With_CustomImplementation/Program.cs @@ -39,11 +39,16 @@ public override async Task RunAsync(IEnumerable m // Create a thread if the user didn't supply one. thread ??= this.GetNewThread(); + if (thread is not CustomAgentThread typedThread) + { + throw new ArgumentException($"The provided thread is not of type {nameof(CustomAgentThread)}.", nameof(thread)); + } + // Clone the input messages and turn them into response messages with upper case text. List responseMessages = CloneAndToUpperCase(messages, this.DisplayName).ToList(); // Notify the thread of the input and output messages. - await NotifyThreadOfNewMessagesAsync(thread, messages.Concat(responseMessages), cancellationToken); + await typedThread.MessageStore.AddMessagesAsync(messages.Concat(responseMessages), cancellationToken); return new AgentRunResponse { @@ -58,11 +63,16 @@ public override async IAsyncEnumerable RunStreamingAsync // Create a thread if the user didn't supply one. thread ??= this.GetNewThread(); + if (thread is not CustomAgentThread typedThread) + { + throw new ArgumentException($"The provided thread is not of type {nameof(CustomAgentThread)}.", nameof(thread)); + } + // Clone the input messages and turn them into response messages with upper case text. List responseMessages = CloneAndToUpperCase(messages, this.DisplayName).ToList(); // Notify the thread of the input and output messages. - await NotifyThreadOfNewMessagesAsync(thread, messages.Concat(responseMessages), cancellationToken); + await typedThread.MessageStore.AddMessagesAsync(messages.Concat(responseMessages), cancellationToken); foreach (var message in responseMessages) { diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/AIAgent.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/AIAgent.cs index 35aa866552..eba6f84687 100644 --- a/dotnet/src/Microsoft.Agents.AI.Abstractions/AIAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/AIAgent.cs @@ -328,28 +328,4 @@ public abstract IAsyncEnumerable RunStreamingAsync( AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default); - - /// - /// Notifies the specified thread about new messages that have been added to the conversation. - /// - /// The conversation thread to notify about the new messages. - /// The collection of new messages to report to the thread. - /// The to monitor for cancellation requests. The default is . - /// A task that represents the asynchronous notification operation. - /// or is . - /// - /// - /// This method ensures that conversation threads are kept informed about message additions, which - /// is important for threads that manage their own state, memory components, or derived context. - /// While all agent implementations should notify their threads, the specific actions taken by - /// each thread type may vary. - /// - /// - protected static async Task NotifyThreadOfNewMessagesAsync(AgentThread thread, IEnumerable messages, CancellationToken cancellationToken) - { - _ = Throw.IfNull(thread); - _ = Throw.IfNull(messages); - - await thread.MessagesReceivedAsync(messages, cancellationToken).ConfigureAwait(false); - } } diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentThread.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentThread.cs index fb5863a5c9..4794457f41 100644 --- a/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentThread.cs +++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/AgentThread.cs @@ -1,11 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System; -using System.Collections.Generic; using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.AI; using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.AI; @@ -65,19 +61,6 @@ protected AgentThread() public virtual JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null) => default; - /// - /// This method is called when new messages have been contributed to the chat by any participant. - /// - /// - /// Inheritors can use this method to update their context based on the new message. - /// - /// The new messages. - /// The to monitor for cancellation requests. The default is . - /// A task that completes when the context has been updated. - /// The thread has been deleted. - protected internal virtual Task MessagesReceivedAsync(IEnumerable newMessages, CancellationToken cancellationToken = default) - => Task.CompletedTask; - /// Asks the for an object of the specified type . /// The type of object being requested. /// An optional key that can be used to help identify the target service. diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/InMemoryAgentThread.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/InMemoryAgentThread.cs index af6080a715..13fcc134f0 100644 --- a/dotnet/src/Microsoft.Agents.AI.Abstractions/InMemoryAgentThread.cs +++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/InMemoryAgentThread.cs @@ -4,8 +4,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; using Microsoft.Extensions.AI; namespace Microsoft.Agents.AI; @@ -116,10 +114,6 @@ public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptio public override object? GetService(Type serviceType, object? serviceKey = null) => base.GetService(serviceType, serviceKey) ?? this.MessageStore?.GetService(serviceType, serviceKey); - /// - protected internal override Task MessagesReceivedAsync(IEnumerable newMessages, CancellationToken cancellationToken = default) - => this.MessageStore.AddMessagesAsync(newMessages, cancellationToken); - [DebuggerBrowsable(DebuggerBrowsableState.Never)] private string DebuggerDisplay => $"Count = {this.MessageStore.Count}"; diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowThread.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowThread.cs index ffa044791f..d27de6bd5c 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowThread.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowThread.cs @@ -68,9 +68,6 @@ public WorkflowThread(Workflow workflow, JsonElement serializedThread, IWorkflow public CheckpointInfo? LastCheckpoint { get; set; } - protected override Task MessagesReceivedAsync(IEnumerable newMessages, CancellationToken cancellationToken = default) - => this.MessageStore.AddMessagesAsync(newMessages, cancellationToken); - public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null) { JsonMarshaller marshaller = new(jsonSerializerOptions); diff --git a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs index bbe1b28352..be5ebd9a1b 100644 --- a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs @@ -270,7 +270,7 @@ public override async IAsyncEnumerable RunStreamingAsync 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 NotifyThreadOfNewMessagesAsync(safeThread, inputMessages.Concat(aiContextProviderMessages ?? []).Concat(chatResponse.Messages), cancellationToken).ConfigureAwait(false); + await NotifyMessageStoreOfNewMessagesAsync(safeThread, inputMessages.Concat(aiContextProviderMessages ?? []).Concat(chatResponse.Messages), cancellationToken).ConfigureAwait(false); // Notify the AIContextProvider of all new messages. await NotifyAIContextProviderOfSuccessAsync(safeThread, inputMessages, aiContextProviderMessages, chatResponse.Messages, cancellationToken).ConfigureAwait(false); @@ -384,7 +384,7 @@ private async Task RunCoreAsync newMessages, CancellationToken cancellationToken) + { + var messageStore = thread.MessageStore; + + // Only notify the message store if we have one. + // If we don't have one, it means that the chat history is service managed and the underlying service is responsible for storing messages. + if (messageStore is not null) + { + return messageStore.AddMessagesAsync(newMessages, cancellationToken); + } + + return Task.CompletedTask; + } + private string GetLoggingAgentName() => this.Name ?? "UnnamedAgent"; #endregion } diff --git a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentThread.cs b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentThread.cs index 71dc7020b6..7f0ce9a1ea 100644 --- a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentThread.cs +++ b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentThread.cs @@ -1,12 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. using System; -using System.Collections.Generic; using System.Diagnostics; using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.AI; using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.AI; @@ -181,33 +177,6 @@ public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptio ?? this.AIContextProvider?.GetService(serviceType, serviceKey) ?? this.MessageStore?.GetService(serviceType, serviceKey); - /// - protected override async Task MessagesReceivedAsync(IEnumerable newMessages, CancellationToken cancellationToken = default) - { - switch (this) - { - case { ConversationId: not null }: - // If the thread messages are stored in the service - // there is nothing to do here, since invoking the - // service should already update the thread. - break; - - case { MessageStore: null }: - // If there is no conversation id, and no store we can createa a default in memory store and add messages to it. - this._messageStore = new InMemoryChatMessageStore(); - await this._messageStore!.AddMessagesAsync(newMessages, cancellationToken).ConfigureAwait(false); - break; - - case { MessageStore: not null }: - // If a store has been provided, we need to add the messages to the store. - await this._messageStore!.AddMessagesAsync(newMessages, cancellationToken).ConfigureAwait(false); - break; - - default: - throw new UnreachableException(); - } - } - [DebuggerBrowsable(DebuggerBrowsableState.Never)] private string DebuggerDisplay => this.ConversationId is { } conversationId ? $"ConversationId = {conversationId}" : diff --git a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AIAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AIAgentTests.cs index bfa14a89d4..5111a97ad1 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AIAgentTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AIAgentTests.cs @@ -8,7 +8,6 @@ using System.Threading.Tasks; using Microsoft.Extensions.AI; using Moq; -using Moq.Protected; namespace Microsoft.Agents.AI.Abstractions.UnitTests; @@ -222,21 +221,6 @@ public void ValidateAgentIDIsIdempotent() Assert.Equal(id, agent.Id); } - [Fact] - public async Task NotifyThreadOfNewMessagesNotifiesThreadAsync() - { - var cancellationToken = default(CancellationToken); - - var messages = new[] { new ChatMessage(ChatRole.User, "msg1"), new ChatMessage(ChatRole.User, "msg2") }; - - var threadMock = new Mock { CallBase = true }; - threadMock.SetupAllProperties(); - - await MockAgent.NotifyThreadOfNewMessagesAsync(threadMock.Object, messages, cancellationToken); - - threadMock.Protected().Verify("MessagesReceivedAsync", Times.Once(), messages, cancellationToken); - } - #region GetService Method Tests /// @@ -360,9 +344,6 @@ public abstract class TestAgentThread : AgentThread; private sealed class MockAgent : AIAgent { - public static new Task NotifyThreadOfNewMessagesAsync(AgentThread thread, IEnumerable messages, CancellationToken cancellationToken) => - AIAgent.NotifyThreadOfNewMessagesAsync(thread, messages, cancellationToken); - public override AgentThread GetNewThread() => throw new NotImplementedException(); diff --git a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AgentThreadTests.cs b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AgentThreadTests.cs index 4d7c4ad219..e75cb4caa1 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AgentThreadTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AgentThreadTests.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System; -using System.Collections.Generic; -using Microsoft.Extensions.AI; #pragma warning disable CA1861 // Avoid constant arrays as arguments @@ -21,15 +19,6 @@ public void Serialize_ReturnsDefaultJsonElement() Assert.Equal(default, result); } - [Fact] - public void MessagesReceivedAsync_ReturnsCompletedTask() - { - var thread = new TestAgentThread(); - var messages = new List { new(ChatRole.User, "hello") }; - var result = thread.MessagesReceivedAsync(messages); - Assert.True(result.IsCompleted); - } - #region GetService Method Tests /// diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentThreadTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentThreadTests.cs index 8226e697ca..48caef1b3d 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentThreadTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentThreadTests.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Linq; using System.Text.Json; -using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.AI; using Moq; @@ -91,50 +90,6 @@ public void SetChatMessageStoreThrowsWhenConversationIdIsSet() #endregion Constructor and Property Tests - #region OnNewMessagesAsync Tests - - [Fact] - public async Task OnNewMessagesAsyncDoesNothingWhenAgentServiceIdAsync() - { - // Arrange - var thread = new ChatClientAgentThread { ConversationId = "thread-123" }; - var messages = new List - { - new(ChatRole.User, "Hello"), - new(ChatRole.Assistant, "Hi there!") - }; - var agent = new MessageSendingAgent(); - - // Act - await agent.SendMessagesAsync(thread, messages, CancellationToken.None); - Assert.Equal("thread-123", thread.ConversationId); - Assert.Null(thread.MessageStore); - } - - [Fact] - public async Task OnNewMessagesAsyncAddsMessagesToStoreAsync() - { - // Arrange - var store = new InMemoryChatMessageStore(); - var thread = new ChatClientAgentThread { MessageStore = store }; - var messages = new List - { - new(ChatRole.User, "Hello"), - new(ChatRole.Assistant, "Hi there!") - }; - var agent = new MessageSendingAgent(); - - // Act - await agent.SendMessagesAsync(thread, messages, CancellationToken.None); - - // Assert - Assert.Equal(2, store.Count); - Assert.Equal("Hello", store[0].Text); - Assert.Equal("Hi there!", store[1].Text); - } - - #endregion OnNewMessagesAsync Tests - #region Deserialize Tests [Fact] @@ -372,22 +327,4 @@ public void GetService_RequestingChatMessageStore_ReturnsChatMessageStore() } #endregion - - private sealed class MessageSendingAgent : AIAgent - { - public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null) - => throw new NotImplementedException(); - - public override AgentThread GetNewThread() - => throw new NotImplementedException(); - - public override Task RunAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) - => throw new NotImplementedException(); - - public override IAsyncEnumerable RunStreamingAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) - => throw new NotImplementedException(); - - public Task SendMessagesAsync(AgentThread thread, IEnumerable messages, CancellationToken cancellationToken = default) - => NotifyThreadOfNewMessagesAsync(thread, messages, cancellationToken); - } } From f8e0c63a47ebf88aa8d4c977042156fadfea9ca7 Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Tue, 25 Nov 2025 11:30:28 +0000 Subject: [PATCH 2/3] Fix bug --- dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs index f302699bbe..ab7426c8b3 100644 --- a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs @@ -712,8 +712,8 @@ private void UpdateThreadWithTypeAndConversationId(ChatClientAgentThread thread, { // If the service doesn't use service side thread storage (i.e. we got no id back from invocation), and // the thread has no MessageStore yet, and we have a custom messages store, we should update the thread - // with the custom MessageStore so that it has somewhere to store the chat history. - thread.MessageStore ??= this._agentOptions?.ChatMessageStoreFactory?.Invoke(new() { SerializedState = default, JsonSerializerOptions = null }); + // with the custom MessageStore or default InMemoryMessageStore so that it has somewhere to store the chat history. + thread.MessageStore ??= this._agentOptions?.ChatMessageStoreFactory?.Invoke(new() { SerializedState = default, JsonSerializerOptions = null }) ?? new InMemoryChatMessageStore(); } } From 2fe097735e1bee5d9d85b493778e2f4cae446667 Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Tue, 25 Nov 2025 11:46:29 +0000 Subject: [PATCH 3/3] Update comment --- dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs index ab7426c8b3..30665fecf3 100644 --- a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs @@ -711,8 +711,8 @@ private void UpdateThreadWithTypeAndConversationId(ChatClientAgentThread thread, else { // If the service doesn't use service side thread storage (i.e. we got no id back from invocation), and - // the thread has no MessageStore yet, and we have a custom messages store, we should update the thread - // with the custom MessageStore or default InMemoryMessageStore so that it has somewhere to store the chat history. + // the thread has no MessageStore yet, we should update the thread with the custom MessageStore or + // default InMemoryMessageStore so that it has somewhere to store the chat history. thread.MessageStore ??= this._agentOptions?.ChatMessageStoreFactory?.Invoke(new() { SerializedState = default, JsonSerializerOptions = null }) ?? new InMemoryChatMessageStore(); } }