From dd217cda042ef195ccc2e2b22ded974d440ee876 Mon Sep 17 00:00:00 2001 From: John Karam <168653085+johnkaram02@users.noreply.github.com> Date: Fri, 23 Oct 2026 20:33:15 +0000 Subject: [PATCH] .Net: Fix chat history reducers crashing when target count is one and a system message is present --- .../ChatHistoryReducerExtensions.cs | 3 ++- .../ChatHistorySummarizationReducerTests.cs | 21 +++++++++++++++++++ .../ChatHistoryTruncationReducerTests.cs | 19 +++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/dotnet/src/SemanticKernel.Core/AI/ChatCompletion/ChatHistoryReducerExtensions.cs b/dotnet/src/SemanticKernel.Core/AI/ChatCompletion/ChatHistoryReducerExtensions.cs index 2a01909a2c5c..4b8fa61b4511 100644 --- a/dotnet/src/SemanticKernel.Core/AI/ChatCompletion/ChatHistoryReducerExtensions.cs +++ b/dotnet/src/SemanticKernel.Core/AI/ChatCompletion/ChatHistoryReducerExtensions.cs @@ -104,7 +104,8 @@ public static int LocateSafeReductionIndex( int offsetCount = 0, bool hasSystemMessage = false) { - targetCount -= hasSystemMessage ? 1 : 0; + // The system message counts toward the target, but at least one other message is always retained + targetCount = Math.Max(1, targetCount - (hasSystemMessage ? 1 : 0)); // Compute the index of the truncation threshold int thresholdIndex = chatHistory.Count - (thresholdCount ?? 0) - targetCount; diff --git a/dotnet/src/SemanticKernel.UnitTests/AI/ChatCompletion/ChatHistorySummarizationReducerTests.cs b/dotnet/src/SemanticKernel.UnitTests/AI/ChatCompletion/ChatHistorySummarizationReducerTests.cs index 0588055efc19..5db41bc01dd1 100644 --- a/dotnet/src/SemanticKernel.UnitTests/AI/ChatCompletion/ChatHistorySummarizationReducerTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/AI/ChatCompletion/ChatHistorySummarizationReducerTests.cs @@ -245,6 +245,27 @@ public async Task VerifySystemMessageIsNotReducedAsync() Assert.Contains(messages, m => m.Role == AuthorRole.System); } + /// + /// Validate history summarized and the latest message retained when the target count is one and a system message is present. + /// + [Fact] + public async Task VerifySystemMessageWithSingleTargetCountAsync() + { + // Arrange + Mock mockCompletionService = this.CreateMockCompletionService(); + IReadOnlyList sourceHistory = MockChatHistoryGenerator.CreateSimpleHistory(20, includeSystemMessage: true).ToArray(); + ChatHistorySummarizationReducer reducer = new(mockCompletionService.Object, 1); + + // Act + IEnumerable? reducedHistory = await reducer.ReduceAsync(sourceHistory); + + // Assert + ChatMessageContent[] messages = VerifyReducedHistory(reducedHistory, 3); + Assert.Equal(AuthorRole.System, messages[0].Role); + VerifySummarization(messages[1]); + Assert.Same(sourceHistory[^1], messages[2]); + } + private static ChatMessageContent[] VerifyReducedHistory(IEnumerable? reducedHistory, int expectedCount) { Assert.NotNull(reducedHistory); diff --git a/dotnet/src/SemanticKernel.UnitTests/AI/ChatCompletion/ChatHistoryTruncationReducerTests.cs b/dotnet/src/SemanticKernel.UnitTests/AI/ChatCompletion/ChatHistoryTruncationReducerTests.cs index 650648dadc17..73fe57d688d7 100644 --- a/dotnet/src/SemanticKernel.UnitTests/AI/ChatCompletion/ChatHistoryTruncationReducerTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/AI/ChatCompletion/ChatHistoryTruncationReducerTests.cs @@ -155,6 +155,25 @@ public async Task VerifySystemMessageIsNotReducedAsync() Assert.Contains(reducedHistory!, m => m.Role == AuthorRole.System); } + /// + /// Validate history reduced to the system message and the latest message when the target count is one. + /// + [Fact] + public async Task VerifySystemMessageWithSingleTargetCountAsync() + { + // Arrange + IReadOnlyList sourceHistory = MockChatHistoryGenerator.CreateSimpleHistory(20, includeSystemMessage: true).ToArray(); + ChatHistoryTruncationReducer reducer = new(1); + + // Act + IEnumerable? reducedHistory = await reducer.ReduceAsync(sourceHistory); + + // Assert + VerifyReducedHistory(reducedHistory, 2); + Assert.Equal(AuthorRole.System, reducedHistory!.First().Role); + Assert.Same(sourceHistory[^1], reducedHistory!.Last()); + } + private static void VerifyReducedHistory(IEnumerable? reducedHistory, int expectedCount) { Assert.NotNull(reducedHistory);