From 365acd7e7bff3911b00e3d25060013e7e9750932 Mon Sep 17 00:00:00 2001 From: quifox <289420841+quifox@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:02:53 +0800 Subject: [PATCH 1/3] .NET: Propagate ChatHistoryMemoryProvider caller cancellation --- .../Memory/ChatHistoryMemoryProvider.cs | 4 +- .../Memory/ChatHistoryMemoryProviderTests.cs | 125 ++++++++++++++++++ 2 files changed, 127 insertions(+), 2 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI/Memory/ChatHistoryMemoryProvider.cs b/dotnet/src/Microsoft.Agents.AI/Memory/ChatHistoryMemoryProvider.cs index f8fd7abaa9e..530b4bae727 100644 --- a/dotnet/src/Microsoft.Agents.AI/Memory/ChatHistoryMemoryProvider.cs +++ b/dotnet/src/Microsoft.Agents.AI/Memory/ChatHistoryMemoryProvider.cs @@ -239,7 +239,7 @@ protected override async ValueTask> ProvideMessagesAsyn return [new ChatMessage(ChatRole.User, contextText)]; } - catch (Exception ex) + catch (Exception ex) when (ex is not OperationCanceledException || !cancellationToken.IsCancellationRequested) { if (this._logger?.IsEnabled(LogLevel.Error) is true) { @@ -292,7 +292,7 @@ protected override async ValueTask StoreAIContextAsync(InvokedContext context, C await collection.UpsertAsync(itemsToStore, cancellationToken).ConfigureAwait(false); } } - catch (Exception ex) + catch (Exception ex) when (ex is not OperationCanceledException || !cancellationToken.IsCancellationRequested) { if (this._logger?.IsEnabled(LogLevel.Error) is true) { diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Memory/ChatHistoryMemoryProviderTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Memory/ChatHistoryMemoryProviderTests.cs index 43cabebaed7..ca4ed8e64c0 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Memory/ChatHistoryMemoryProviderTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Memory/ChatHistoryMemoryProviderTests.cs @@ -269,6 +269,58 @@ public async Task InvokedAsync_DoesNotThrow_WhenUpsertThrowsAsync() Times.Once); } + [Fact] + public async Task InvokedAsync_WhenCallerCancels_PropagatesCancellationAsync() + { + // Arrange + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + var provider = new ChatHistoryMemoryProvider( + this._vectorStoreMock.Object, + TestCollectionName, + 1, + _ => new ChatHistoryMemoryProvider.State(new ChatHistoryMemoryProviderScope { UserId = "UID" }), + loggerFactory: this._loggerFactoryMock.Object); + var requestMsg = new ChatMessage(ChatRole.User, "request text"); + var invokedContext = new AIContextProvider.InvokedContext(s_mockAgent, new TestAgentSession(), [requestMsg], []); + + // Act & Assert + await Assert.ThrowsAnyAsync( + () => provider.InvokedAsync(invokedContext, cts.Token).AsTask()); + } + + [Fact] + public async Task InvokedAsync_WhenProviderCancelsWithoutCallerCancellation_DoesNotThrowAsync() + { + // Arrange + this._vectorStoreCollectionMock + .Setup(c => c.UpsertAsync(It.IsAny>>(), It.IsAny())) + .ThrowsAsync(new OperationCanceledException("Provider cancelled")); + + var provider = new ChatHistoryMemoryProvider( + this._vectorStoreMock.Object, + TestCollectionName, + 1, + _ => new ChatHistoryMemoryProvider.State(new ChatHistoryMemoryProviderScope { UserId = "UID" }), + loggerFactory: this._loggerFactoryMock.Object); + var requestMsg = new ChatMessage(ChatRole.User, "request text"); + var invokedContext = new AIContextProvider.InvokedContext(s_mockAgent, new TestAgentSession(), [requestMsg], []); + + // Act + await provider.InvokedAsync(invokedContext, CancellationToken.None); + + // Assert + this._loggerMock.Verify( + l => l.Log( + LogLevel.Error, + It.IsAny(), + It.Is((v, t) => v.ToString()!.Contains("ChatHistoryMemoryProvider: Failed to add messages to chat history vector store due to error")), + It.IsAny(), + It.IsAny>()), + Times.Once); + } + [Theory] [InlineData(false, false, false, 0)] [InlineData(false, false, true, 0)] @@ -793,6 +845,79 @@ public async Task InvokedAsync_CustomStorageInputFilter_OverridesDefaultAsync() Assert.Equal("Response", stored[2]["Content"]); } + [Fact] + public async Task InvokingAsync_WhenCallerCancels_PropagatesCancellationAsync() + { + // Arrange + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + this._vectorStoreCollectionMock + .Setup(c => c.SearchAsync( + It.IsAny(), + It.IsAny(), + It.IsAny>>(), + It.IsAny())) + .Throws(new OperationCanceledException(cts.Token)); + + var provider = new ChatHistoryMemoryProvider( + this._vectorStoreMock.Object, + TestCollectionName, + 1, + _ => new ChatHistoryMemoryProvider.State(new ChatHistoryMemoryProviderScope { UserId = "UID" }), + options: new ChatHistoryMemoryProviderOptions + { + SearchTime = ChatHistoryMemoryProviderOptions.SearchBehavior.BeforeAIInvoke + }, + loggerFactory: this._loggerFactoryMock.Object); + + var invokingContext = new AIContextProvider.InvokingContext( + s_mockAgent, + new TestAgentSession(), + new AIContext { Messages = [new ChatMessage(ChatRole.User, "What was discussed?")] }); + + // Act & Assert + await Assert.ThrowsAnyAsync( + () => provider.InvokingAsync(invokingContext, cts.Token).AsTask()); + } + + [Fact] + public async Task InvokingAsync_WhenProviderCancelsWithoutCallerCancellation_DoesNotThrowAsync() + { + // Arrange + this._vectorStoreCollectionMock + .Setup(c => c.SearchAsync( + It.IsAny(), + It.IsAny(), + It.IsAny>>(), + It.IsAny())) + .Throws(new OperationCanceledException("Provider cancelled")); + + var provider = new ChatHistoryMemoryProvider( + this._vectorStoreMock.Object, + TestCollectionName, + 1, + _ => new ChatHistoryMemoryProvider.State(new ChatHistoryMemoryProviderScope { UserId = "UID" }), + options: new ChatHistoryMemoryProviderOptions + { + SearchTime = ChatHistoryMemoryProviderOptions.SearchBehavior.BeforeAIInvoke + }, + loggerFactory: this._loggerFactoryMock.Object); + + var invokingContext = new AIContextProvider.InvokingContext( + s_mockAgent, + new TestAgentSession(), + new AIContext { Messages = [new ChatMessage(ChatRole.User, "What was discussed?")] }); + + // Act + var aiContext = await provider.InvokingAsync(invokingContext, CancellationToken.None); + + // Assert + Assert.NotNull(aiContext.Messages); + Assert.Single(aiContext.Messages); + Assert.Equal("What was discussed?", aiContext.Messages.Single().Text); + } + #endregion #region MessageAIContextProvider.InvokingAsync Tests From 92da63bdb8f9ba945d31f75c50633d405a42868a Mon Sep 17 00:00:00 2001 From: quifox <289420841+quifox@users.noreply.github.com> Date: Tue, 29 Sep 2026 19:52:19 +0800 Subject: [PATCH 2/3] .NET: Exercise storage caller cancellation path --- .../Memory/ChatHistoryMemoryProviderTests.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Memory/ChatHistoryMemoryProviderTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Memory/ChatHistoryMemoryProviderTests.cs index ca4ed8e64c0..567594846b8 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Memory/ChatHistoryMemoryProviderTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Memory/ChatHistoryMemoryProviderTests.cs @@ -274,7 +274,11 @@ public async Task InvokedAsync_WhenCallerCancels_PropagatesCancellationAsync() { // Arrange using var cts = new CancellationTokenSource(); - cts.Cancel(); + + this._vectorStoreCollectionMock + .Setup(c => c.UpsertAsync(It.IsAny>>(), cts.Token)) + .Callback(() => cts.Cancel()) + .ThrowsAsync(new OperationCanceledException(cts.Token)); var provider = new ChatHistoryMemoryProvider( this._vectorStoreMock.Object, @@ -286,8 +290,12 @@ public async Task InvokedAsync_WhenCallerCancels_PropagatesCancellationAsync() var invokedContext = new AIContextProvider.InvokedContext(s_mockAgent, new TestAgentSession(), [requestMsg], []); // Act & Assert - await Assert.ThrowsAnyAsync( + var exception = await Assert.ThrowsAnyAsync( () => provider.InvokedAsync(invokedContext, cts.Token).AsTask()); + Assert.Equal(cts.Token, exception.CancellationToken); + this._vectorStoreCollectionMock.Verify( + c => c.UpsertAsync(It.IsAny>>(), cts.Token), + Times.Once); } [Fact] From f9397a45ca71e53997ff6a296a67e900316e6b8a Mon Sep 17 00:00:00 2001 From: quifox <289420841+quifox@users.noreply.github.com> Date: Tue, 29 Sep 2026 22:35:11 +0800 Subject: [PATCH 3/3] .NET: Exercise search caller cancellation path --- .../Memory/ChatHistoryMemoryProviderTests.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Memory/ChatHistoryMemoryProviderTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Memory/ChatHistoryMemoryProviderTests.cs index 567594846b8..668a9d771f6 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Memory/ChatHistoryMemoryProviderTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Memory/ChatHistoryMemoryProviderTests.cs @@ -858,14 +858,14 @@ public async Task InvokingAsync_WhenCallerCancels_PropagatesCancellationAsync() { // Arrange using var cts = new CancellationTokenSource(); - cts.Cancel(); this._vectorStoreCollectionMock .Setup(c => c.SearchAsync( It.IsAny(), It.IsAny(), It.IsAny>>(), - It.IsAny())) + cts.Token)) + .Callback(() => cts.Cancel()) .Throws(new OperationCanceledException(cts.Token)); var provider = new ChatHistoryMemoryProvider( @@ -885,8 +885,16 @@ public async Task InvokingAsync_WhenCallerCancels_PropagatesCancellationAsync() new AIContext { Messages = [new ChatMessage(ChatRole.User, "What was discussed?")] }); // Act & Assert - await Assert.ThrowsAnyAsync( + var exception = await Assert.ThrowsAnyAsync( () => provider.InvokingAsync(invokingContext, cts.Token).AsTask()); + Assert.Equal(cts.Token, exception.CancellationToken); + this._vectorStoreCollectionMock.Verify( + c => c.SearchAsync( + It.IsAny(), + It.IsAny(), + It.IsAny>>(), + cts.Token), + Times.Once); } [Fact]