diff --git a/dotnet/src/Microsoft.Agents.AI/Compaction/MessageIndex.cs b/dotnet/src/Microsoft.Agents.AI/Compaction/MessageIndex.cs index f8467eb3ce..dff5b4764f 100644 --- a/dotnet/src/Microsoft.Agents.AI/Compaction/MessageIndex.cs +++ b/dotnet/src/Microsoft.Agents.AI/Compaction/MessageIndex.cs @@ -67,11 +67,12 @@ public sealed class MessageIndex this.Groups = groups; this.Tokenizer = tokenizer; + // Restore turn counter from the last group that has a TurnIndex for (int index = groups.Count - 1; index >= 0; --index) { - if (this.Groups[0].TurnIndex.HasValue) + if (this.Groups[index].TurnIndex.HasValue) { - this._currentTurn = this.Groups[0].TurnIndex!.Value; + this._currentTurn = this.Groups[index].TurnIndex!.Value; break; } } @@ -353,11 +354,6 @@ public sealed class MessageIndex private static bool HasToolCalls(ChatMessage message) { - if (message.Contents is null) - { - return false; - } - foreach (AIContent content in message.Contents) { if (content is FunctionCallContent) diff --git a/dotnet/src/Microsoft.Agents.AI/Compaction/SummarizationCompactionStrategy.cs b/dotnet/src/Microsoft.Agents.AI/Compaction/SummarizationCompactionStrategy.cs index 0e7c01719c..54c7ab4cdd 100644 --- a/dotnet/src/Microsoft.Agents.AI/Compaction/SummarizationCompactionStrategy.cs +++ b/dotnet/src/Microsoft.Agents.AI/Compaction/SummarizationCompactionStrategy.cs @@ -157,11 +157,6 @@ public sealed class SummarizationCompactionStrategy : CompactionStrategy } } - if (summarized == 0) - { - return false; - } - // Generate summary using the chat client (single LLM call for all marked groups) ChatResponse response = await this.ChatClient.GetResponseAsync( [ @@ -180,14 +175,7 @@ public sealed class SummarizationCompactionStrategy : CompactionStrategy ChatMessage summaryMessage = new(ChatRole.Assistant, $"[Summary]\n{summaryText}"); (summaryMessage.AdditionalProperties ??= [])[MessageGroup.SummaryPropertyKey] = true; - if (insertIndex >= 0) - { - index.InsertGroup(insertIndex, MessageGroupKind.Summary, [summaryMessage]); - } - else - { - index.AddGroup(MessageGroupKind.Summary, [summaryMessage]); - } + index.InsertGroup(insertIndex, MessageGroupKind.Summary, [summaryMessage]); return true; } diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Compaction/CompactingChatClientTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Compaction/CompactingChatClientTests.cs new file mode 100644 index 0000000000..80292c7ed8 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Compaction/CompactingChatClientTests.cs @@ -0,0 +1,400 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Agents.AI.Compaction; +using Microsoft.Extensions.AI; +using Moq; + +namespace Microsoft.Agents.AI.UnitTests.Compaction; + +/// +/// Contains tests for the class. +/// +public sealed class CompactingChatClientTests : IDisposable +{ + /// + /// Restores the static after each test. + /// + public void Dispose() + { + SetCurrentRunContext(null); + } + + [Fact] + public void ConstructorThrowsOnNullStrategyAsync() + { + Mock mockInner = new(); + Assert.Throws(() => new CompactingChatClient(mockInner.Object, null!)); + } + + [Fact] + public async Task GetResponseAsyncNoContextPassesThroughAsync() + { + // Arrange — no CurrentRunContext set → passthrough + ChatResponse expectedResponse = new([new ChatMessage(ChatRole.Assistant, "Hi")]); + Mock mockInner = new(); + mockInner.Setup(c => c.GetResponseAsync( + It.IsAny>(), + It.IsAny(), + It.IsAny())) + .ReturnsAsync(expectedResponse); + + TruncationCompactionStrategy strategy = new(CompactionTriggers.TokensExceed(100000)); + CompactingChatClient client = new(mockInner.Object, strategy); + + List messages = + [ + new ChatMessage(ChatRole.User, "Hello"), + ]; + + // Act + ChatResponse response = await client.GetResponseAsync(messages); + + // Assert + Assert.Same(expectedResponse, response); + mockInner.Verify(c => c.GetResponseAsync( + messages, + It.IsAny(), + It.IsAny()), Times.Once); + } + + [Fact] + public async Task GetResponseAsyncWithContextAppliesCompactionAsync() + { + // Arrange — set CurrentRunContext so compaction runs + ChatResponse expectedResponse = new([new ChatMessage(ChatRole.Assistant, "Done")]); + List? capturedMessages = null; + Mock mockInner = new(); + mockInner.Setup(c => c.GetResponseAsync( + It.IsAny>(), + It.IsAny(), + It.IsAny())) + .Callback, ChatOptions?, CancellationToken>((msgs, _, _) => + capturedMessages = [.. msgs]) + .ReturnsAsync(expectedResponse); + + // Strategy that always triggers and keeps only 1 group + TruncationCompactionStrategy strategy = new(_ => true, minimumPreserved: 1); + CompactingChatClient client = new(mockInner.Object, strategy); + + TestAgentSession session = new(); + SetRunContext(session); + + List messages = + [ + new ChatMessage(ChatRole.User, "Q1"), + new ChatMessage(ChatRole.Assistant, "A1"), + new ChatMessage(ChatRole.User, "Q2"), + ]; + + // Act + ChatResponse response = await client.GetResponseAsync(messages); + + // Assert — compaction should have removed oldest groups + Assert.Same(expectedResponse, response); + Assert.NotNull(capturedMessages); + Assert.True(capturedMessages!.Count < messages.Count); + } + + [Fact] + public async Task GetResponseAsyncNoCompactionNeededReturnsOriginalMessagesAsync() + { + // Arrange — trigger never fires → no compaction + ChatResponse expectedResponse = new([new ChatMessage(ChatRole.Assistant, "Hi")]); + List? capturedMessages = null; + Mock mockInner = new(); + mockInner.Setup(c => c.GetResponseAsync( + It.IsAny>(), + It.IsAny(), + It.IsAny())) + .Callback, ChatOptions?, CancellationToken>((msgs, _, _) => + capturedMessages = [.. msgs]) + .ReturnsAsync(expectedResponse); + + TruncationCompactionStrategy strategy = new(CompactionTriggers.TokensExceed(100000)); + CompactingChatClient client = new(mockInner.Object, strategy); + + TestAgentSession session = new(); + SetRunContext(session); + + List messages = + [ + new ChatMessage(ChatRole.User, "Hello"), + ]; + + // Act + await client.GetResponseAsync(messages); + + // Assert — original messages passed through + Assert.NotNull(capturedMessages); + Assert.Single(capturedMessages!); + Assert.Equal("Hello", capturedMessages[0].Text); + } + + [Fact] + public async Task GetResponseAsyncWithExistingIndexUpdatesAsync() + { + // Arrange — call twice to exercise the "existing index" path (state.MessageIndex.Count > 0) + Mock mockInner = new(); + mockInner.Setup(c => c.GetResponseAsync( + It.IsAny>(), + It.IsAny(), + It.IsAny())) + .ReturnsAsync(new ChatResponse([new ChatMessage(ChatRole.Assistant, "OK")])); + + // Strategy that always triggers, keeping 1 group + TruncationCompactionStrategy strategy = new(_ => true, minimumPreserved: 1); + CompactingChatClient client = new(mockInner.Object, strategy); + + TestAgentSession session = new(); + SetRunContext(session); + + List messages1 = + [ + new ChatMessage(ChatRole.User, "Q1"), + new ChatMessage(ChatRole.Assistant, "A1"), + new ChatMessage(ChatRole.User, "Q2"), + ]; + + // First call — initializes state + await client.GetResponseAsync(messages1); + + List messages2 = + [ + new ChatMessage(ChatRole.User, "Q1"), + new ChatMessage(ChatRole.Assistant, "A1"), + new ChatMessage(ChatRole.User, "Q2"), + new ChatMessage(ChatRole.Assistant, "A2"), + new ChatMessage(ChatRole.User, "Q3"), + ]; + + // Act — second call exercises the update path + ChatResponse response = await client.GetResponseAsync(messages2); + + // Assert + Assert.NotNull(response); + } + + [Fact] + public async Task GetResponseAsyncNullSessionReturnsOriginalAsync() + { + // Arrange — CurrentRunContext exists but Session is null + ChatResponse expectedResponse = new([new ChatMessage(ChatRole.Assistant, "Hi")]); + Mock mockInner = new(); + mockInner.Setup(c => c.GetResponseAsync( + It.IsAny>(), + It.IsAny(), + It.IsAny())) + .ReturnsAsync(expectedResponse); + + TruncationCompactionStrategy strategy = new(CompactionTriggers.TokensExceed(100000)); + CompactingChatClient client = new(mockInner.Object, strategy); + + // Set context with null session + SetRunContext(null); + + List messages = [new ChatMessage(ChatRole.User, "Hello")]; + + // Act + ChatResponse response = await client.GetResponseAsync(messages); + + // Assert + Assert.Same(expectedResponse, response); + } + + [Fact] + public async Task GetStreamingResponseAsyncNoContextPassesThroughAsync() + { + // Arrange — no CurrentRunContext + Mock mockInner = new(); + ChatResponseUpdate[] updates = [new(ChatRole.Assistant, "Hi")]; + mockInner.Setup(c => c.GetStreamingResponseAsync( + It.IsAny>(), + It.IsAny(), + It.IsAny())) + .Returns(ToAsyncEnumerableAsync(updates)); + + TruncationCompactionStrategy strategy = new(CompactionTriggers.TokensExceed(100000)); + CompactingChatClient client = new(mockInner.Object, strategy); + + List messages = [new ChatMessage(ChatRole.User, "Hello")]; + + // Act + List results = []; + await foreach (ChatResponseUpdate update in client.GetStreamingResponseAsync(messages)) + { + results.Add(update); + } + + // Assert + Assert.Single(results); + Assert.Equal("Hi", results[0].Text); + } + + [Fact] + public async Task GetStreamingResponseAsyncWithContextAppliesCompactionAsync() + { + // Arrange + Mock mockInner = new(); + ChatResponseUpdate[] updates = [new(ChatRole.Assistant, "Done")]; + mockInner.Setup(c => c.GetStreamingResponseAsync( + It.IsAny>(), + It.IsAny(), + It.IsAny())) + .Returns(ToAsyncEnumerableAsync(updates)); + + TruncationCompactionStrategy strategy = new(_ => true, minimumPreserved: 1); + CompactingChatClient client = new(mockInner.Object, strategy); + + TestAgentSession session = new(); + SetRunContext(session); + + List messages = + [ + new ChatMessage(ChatRole.User, "Q1"), + new ChatMessage(ChatRole.Assistant, "A1"), + new ChatMessage(ChatRole.User, "Q2"), + ]; + + // Act + List results = []; + await foreach (ChatResponseUpdate update in client.GetStreamingResponseAsync(messages)) + { + results.Add(update); + } + + // Assert + Assert.Single(results); + Assert.Equal("Done", results[0].Text); + } + + [Fact] + public void GetServiceReturnsStrategyForMatchingType() + { + // Arrange + Mock mockInner = new(); + TruncationCompactionStrategy strategy = new(CompactionTriggers.TokensExceed(1000)); + CompactingChatClient client = new(mockInner.Object, strategy); + + // Act — typeof(Type).IsInstanceOfType(typeof(CompactionStrategy)) is true + object? result = client.GetService(typeof(Type)); + + // Assert + Assert.Same(strategy, result); + } + + [Fact] + public void GetServiceDelegatesToBaseForNonMatchingType() + { + // Arrange + Mock mockInner = new(); + TruncationCompactionStrategy strategy = new(CompactionTriggers.TokensExceed(1000)); + CompactingChatClient client = new(mockInner.Object, strategy); + + // Act — typeof(string) doesn't match + object? result = client.GetService(typeof(string)); + + // Assert — delegates to base (which returns null for unregistered types) + Assert.Null(result); + } + + [Fact] + public void GetServiceThrowsOnNullType() + { + Mock mockInner = new(); + TruncationCompactionStrategy strategy = new(CompactionTriggers.TokensExceed(1000)); + CompactingChatClient client = new(mockInner.Object, strategy); + + Assert.Throws(() => client.GetService(null!)); + } + + [Fact] + public void GetServiceWithServiceKeyDelegatesToBase() + { + // Arrange — non-null serviceKey always delegates + Mock mockInner = new(); + TruncationCompactionStrategy strategy = new(CompactionTriggers.TokensExceed(1000)); + CompactingChatClient client = new(mockInner.Object, strategy); + + // Act + object? result = client.GetService(typeof(Type), serviceKey: "mykey"); + + // Assert — delegates to base because serviceKey is non-null + Assert.Null(result); + } + + [Fact] + public async Task GetResponseAsyncMessagesNotListCreatesListCopyAsync() + { + // Arrange — pass IEnumerable (not List) to exercise the list copy branch + ChatResponse expectedResponse = new([new ChatMessage(ChatRole.Assistant, "Hi")]); + Mock mockInner = new(); + mockInner.Setup(c => c.GetResponseAsync( + It.IsAny>(), + It.IsAny(), + It.IsAny())) + .ReturnsAsync(expectedResponse); + + TruncationCompactionStrategy strategy = new(CompactionTriggers.TokensExceed(100000)); + CompactingChatClient client = new(mockInner.Object, strategy); + + TestAgentSession session = new(); + SetRunContext(session); + + // Use an IEnumerable (not a List) to trigger the copy path + IEnumerable messages = new ChatMessage[] { new(ChatRole.User, "Hello") }; + + // Act + ChatResponse response = await client.GetResponseAsync(messages); + + // Assert + Assert.Same(expectedResponse, response); + } + + /// + /// Sets via reflection. + /// + private static void SetCurrentRunContext(AgentRunContext? context) + { + FieldInfo? field = typeof(AIAgent).GetField("s_currentContext", BindingFlags.NonPublic | BindingFlags.Static); + Assert.NotNull(field); + object? asyncLocal = field!.GetValue(null); + Assert.NotNull(asyncLocal); + PropertyInfo? valueProp = asyncLocal!.GetType().GetProperty("Value"); + Assert.NotNull(valueProp); + valueProp!.SetValue(asyncLocal, context); + } + + /// + /// Creates an with the given session and sets it as the current context. + /// + private static void SetRunContext(AgentSession? session) + { + Mock mockAgent = new() { CallBase = true }; + AgentRunContext context = new( + mockAgent.Object, + session, + new List { new(ChatRole.User, "test") }, + null); + SetCurrentRunContext(context); + } + + private static async IAsyncEnumerable ToAsyncEnumerableAsync( + ChatResponseUpdate[] updates, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + foreach (ChatResponseUpdate update in updates) + { + cancellationToken.ThrowIfCancellationRequested(); + yield return update; + await Task.CompletedTask; + } + } + + private sealed class TestAgentSession : AgentSession; +} diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Compaction/MessageIndexTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Compaction/MessageIndexTests.cs index 9f0eabf9d8..b1fa5c59cb 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Compaction/MessageIndexTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Compaction/MessageIndexTests.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Buffers; using System.Collections.Generic; using Microsoft.Agents.AI.Compaction; using Microsoft.Extensions.AI; @@ -675,4 +676,174 @@ public class MessageIndexTests Assert.Equal(5, inserted.ByteCount); Assert.Equal(1, inserted.TokenCount); // 5 / 4 = 1 (integer division) } + + [Fact] + public void ConstructorWithGroupsRestoresTurnIndex() + { + // Arrange — pre-existing groups with turn indices + MessageGroup group1 = new(MessageGroupKind.User, [new ChatMessage(ChatRole.User, "Q1")], 2, 1, turnIndex: 1); + MessageGroup group2 = new(MessageGroupKind.AssistantText, [new ChatMessage(ChatRole.Assistant, "A1")], 2, 1, turnIndex: 1); + MessageGroup group3 = new(MessageGroupKind.User, [new ChatMessage(ChatRole.User, "Q2")], 2, 1, turnIndex: 2); + List groups = [group1, group2, group3]; + + // Act — constructor should restore _currentTurn from the last group's TurnIndex + MessageIndex index = new(groups); + + // Assert — adding a new user message should get turn 3 (restored 2 + 1) + index.Update( + [ + new ChatMessage(ChatRole.User, "Q1"), + new ChatMessage(ChatRole.Assistant, "A1"), + new ChatMessage(ChatRole.User, "Q2"), + new ChatMessage(ChatRole.User, "Q3"), + ]); + + // The new user group should have TurnIndex 3 + MessageGroup lastGroup = index.Groups[index.Groups.Count - 1]; + Assert.Equal(MessageGroupKind.User, lastGroup.Kind); + Assert.NotNull(lastGroup.TurnIndex); + } + + [Fact] + public void ConstructorWithEmptyGroupsHandlesGracefully() + { + // Arrange & Act — constructor with empty list + MessageIndex index = new([]); + + // Assert + Assert.Empty(index.Groups); + } + + [Fact] + public void ConstructorWithGroupsWithoutTurnIndexSkipsRestore() + { + // Arrange — groups without turn indices (system messages) + MessageGroup systemGroup = new(MessageGroupKind.System, [new ChatMessage(ChatRole.System, "Be helpful")], 10, 3, turnIndex: null); + List groups = [systemGroup]; + + // Act — constructor won't find a TurnIndex to restore + MessageIndex index = new(groups); + + // Assert + Assert.Single(index.Groups); + } + + [Fact] + public void ComputeTokenCountReturnsTokenCount() + { + // Arrange — call the public static method directly + List messages = + [ + new ChatMessage(ChatRole.User, "Hello world"), + new ChatMessage(ChatRole.Assistant, "Greetings"), + ]; + + // Act — use a simple tokenizer that counts words (each word = 1 token) + SimpleWordTokenizer tokenizer = new(); + int tokenCount = MessageIndex.ComputeTokenCount(messages, tokenizer); + + // Assert — "Hello world" = 2, "Greetings" = 1 → 3 total + Assert.Equal(3, tokenCount); + } + + [Fact] + public void ComputeTokenCountEmptyTextReturnsZero() + { + // Arrange — message with no text content + List messages = + [ + new ChatMessage(ChatRole.User, [new FunctionCallContent("c1", "fn")]), + ]; + + SimpleWordTokenizer tokenizer = new(); + int tokenCount = MessageIndex.ComputeTokenCount(messages, tokenizer); + + // Assert — no text content → 0 tokens + Assert.Equal(0, tokenCount); + } + + [Fact] + public void CreateWithTokenizerUsesTokenizerForCounts() + { + // Arrange + SimpleWordTokenizer tokenizer = new(); + + List messages = + [ + new ChatMessage(ChatRole.User, "Hello world test"), + ]; + + // Act + MessageIndex index = MessageIndex.Create(messages, tokenizer); + + // Assert — tokenizer counts words: "Hello world test" = 3 tokens + Assert.Single(index.Groups); + Assert.Equal(3, index.Groups[0].TokenCount); + Assert.NotNull(index.Tokenizer); + } + + [Fact] + public void InsertGroupWithTokenizerUsesTokenizer() + { + // Arrange + SimpleWordTokenizer tokenizer = new(); + MessageIndex index = MessageIndex.Create( + [ + new ChatMessage(ChatRole.User, "Hello"), + ], tokenizer); + + // Act + ChatMessage msg = new(ChatRole.Assistant, "Hello world test message"); + MessageGroup inserted = index.InsertGroup(0, MessageGroupKind.AssistantText, [msg]); + + // Assert — tokenizer counts words: "Hello world test message" = 4 tokens + Assert.Equal(4, inserted.TokenCount); + } + + /// + /// A simple tokenizer that counts whitespace-separated words as tokens. + /// + private sealed class SimpleWordTokenizer : Microsoft.ML.Tokenizers.Tokenizer + { + public override Microsoft.ML.Tokenizers.PreTokenizer? PreTokenizer => null; + public override Microsoft.ML.Tokenizers.Normalizer? Normalizer => null; + + protected override Microsoft.ML.Tokenizers.EncodeResults EncodeToTokens(string? text, System.ReadOnlySpan textSpan, Microsoft.ML.Tokenizers.EncodeSettings settings) + { + // Simple word-based encoding + string input = text ?? textSpan.ToString(); + if (string.IsNullOrWhiteSpace(input)) + { + return new Microsoft.ML.Tokenizers.EncodeResults + { + Tokens = System.Array.Empty(), + CharsConsumed = 0, + NormalizedText = null, + }; + } + + string[] words = input.Split(' '); + List tokens = []; + int offset = 0; + for (int i = 0; i < words.Length; i++) + { + tokens.Add(new Microsoft.ML.Tokenizers.EncodedToken(i, words[i], new System.Range(offset, offset + words[i].Length))); + offset += words[i].Length + 1; + } + + return new Microsoft.ML.Tokenizers.EncodeResults + { + Tokens = tokens, + CharsConsumed = input.Length, + NormalizedText = null, + }; + } + + public override OperationStatus Decode(System.Collections.Generic.IEnumerable ids, System.Span destination, out int idsConsumed, out int charsWritten) + { + idsConsumed = 0; + charsWritten = 0; + return OperationStatus.Done; + } + } } diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Compaction/SlidingWindowCompactionStrategyTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Compaction/SlidingWindowCompactionStrategyTests.cs index 2d78c2e720..08c3b50ad3 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Compaction/SlidingWindowCompactionStrategyTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Compaction/SlidingWindowCompactionStrategyTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; using System.Threading.Tasks; @@ -163,12 +163,12 @@ public class SlidingWindowCompactionStrategyTests { // Arrange — trigger on > 1 turn, custom target stops after removing 1 turn int removeCount = 0; - CompactionTrigger targetAfterOne = _ => ++removeCount >= 1; + bool TargetAfterOne(MessageIndex _) => ++removeCount >= 1; SlidingWindowCompactionStrategy strategy = new( CompactionTriggers.TurnsExceed(1), minimumPreserved: 0, - target: targetAfterOne); + target: TargetAfterOne); MessageIndex index = MessageIndex.Create( [ diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Compaction/SummarizationCompactionStrategyTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Compaction/SummarizationCompactionStrategyTests.cs index d12d490dd4..9d6bfdb05b 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Compaction/SummarizationCompactionStrategyTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Compaction/SummarizationCompactionStrategyTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; using System.Linq; @@ -15,8 +15,6 @@ namespace Microsoft.Agents.AI.UnitTests.Compaction; /// public class SummarizationCompactionStrategyTests { - private static readonly CompactionTrigger AlwaysTrigger = _ => true; - /// /// Creates a mock that returns the specified summary text. /// @@ -60,7 +58,7 @@ public class SummarizationCompactionStrategyTests // Arrange — always trigger, preserve 1 recent group SummarizationCompactionStrategy strategy = new( CreateMockChatClient("Key facts from earlier."), - AlwaysTrigger, + CompactionTriggers.Always, minimumPreserved: 1); MessageIndex index = MessageIndex.Create( @@ -91,7 +89,7 @@ public class SummarizationCompactionStrategyTests // Arrange SummarizationCompactionStrategy strategy = new( CreateMockChatClient(), - AlwaysTrigger, + CompactionTriggers.Always, minimumPreserved: 1); MessageIndex index = MessageIndex.Create( @@ -118,7 +116,7 @@ public class SummarizationCompactionStrategyTests // Arrange SummarizationCompactionStrategy strategy = new( CreateMockChatClient("Summary text."), - AlwaysTrigger, + CompactionTriggers.Always, minimumPreserved: 1); MessageIndex index = MessageIndex.Create( @@ -145,7 +143,7 @@ public class SummarizationCompactionStrategyTests // Arrange — LLM returns whitespace SummarizationCompactionStrategy strategy = new( CreateMockChatClient(" "), - AlwaysTrigger, + CompactionTriggers.Always, minimumPreserved: 1); MessageIndex index = MessageIndex.Create( @@ -168,7 +166,7 @@ public class SummarizationCompactionStrategyTests // Arrange — preserve 5 but only 2 non-system groups SummarizationCompactionStrategy strategy = new( CreateMockChatClient(), - AlwaysTrigger, + CompactionTriggers.Always, minimumPreserved: 5); MessageIndex index = MessageIndex.Create( @@ -198,12 +196,12 @@ public class SummarizationCompactionStrategyTests capturedMessages = [.. msgs]) .ReturnsAsync(new ChatResponse([new ChatMessage(ChatRole.Assistant, "Custom summary.")])); - const string customPrompt = "Summarize in bullet points only."; + const string CustomPrompt = "Summarize in bullet points only."; SummarizationCompactionStrategy strategy = new( mockClient.Object, - AlwaysTrigger, + CompactionTriggers.Always, minimumPreserved: 1, - summarizationPrompt: customPrompt); + summarizationPrompt: CustomPrompt); MessageIndex index = MessageIndex.Create( [ @@ -216,7 +214,7 @@ public class SummarizationCompactionStrategyTests // Assert — the custom prompt should be the first message sent to the LLM Assert.NotNull(capturedMessages); - Assert.Equal(customPrompt, capturedMessages![0].Text); + Assert.Equal(CustomPrompt, capturedMessages![0].Text); } [Fact] @@ -225,7 +223,7 @@ public class SummarizationCompactionStrategyTests // Arrange SummarizationCompactionStrategy strategy = new( CreateMockChatClient(), - AlwaysTrigger, + CompactionTriggers.Always, minimumPreserved: 1); MessageIndex index = MessageIndex.Create( @@ -248,13 +246,13 @@ public class SummarizationCompactionStrategyTests { // Arrange — 4 non-system groups, preserve 1, target met after 1 exclusion int exclusionCount = 0; - CompactionTrigger targetAfterOne = _ => ++exclusionCount >= 1; + CompactionTrigger TargetAfterOne = _ => ++exclusionCount >= 1; SummarizationCompactionStrategy strategy = new( CreateMockChatClient("Partial summary."), - AlwaysTrigger, + CompactionTriggers.Always, minimumPreserved: 1, - target: targetAfterOne); + target: TargetAfterOne); MessageIndex index = MessageIndex.Create( [ @@ -278,7 +276,7 @@ public class SummarizationCompactionStrategyTests // Arrange — preserve 2 SummarizationCompactionStrategy strategy = new( CreateMockChatClient("Summary."), - AlwaysTrigger, + CompactionTriggers.Always, minimumPreserved: 2); MessageIndex index = MessageIndex.Create( diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Compaction/TruncationCompactionStrategyTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Compaction/TruncationCompactionStrategyTests.cs index ee08611653..ceb40b7495 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Compaction/TruncationCompactionStrategyTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Compaction/TruncationCompactionStrategyTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System.Linq; using System.Threading.Tasks; @@ -12,13 +12,11 @@ namespace Microsoft.Agents.AI.UnitTests.Compaction; /// public class TruncationCompactionStrategyTests { - private static readonly CompactionTrigger s_alwaysTrigger = _ => true; - [Fact] public async Task CompactAsyncAlwaysTriggerCompactsToPreserveRecentAsync() { // Arrange — always-trigger means always compact - TruncationCompactionStrategy strategy = new(s_alwaysTrigger, minimumPreserved: 1); + TruncationCompactionStrategy strategy = new(CompactionTriggers.Always, minimumPreserved: 1); MessageIndex groups = MessageIndex.Create( [ new ChatMessage(ChatRole.User, "First"), @@ -89,7 +87,7 @@ public class TruncationCompactionStrategyTests public async Task CompactAsyncPreservesSystemMessagesAsync() { // Arrange - TruncationCompactionStrategy strategy = new(s_alwaysTrigger, minimumPreserved: 1); + TruncationCompactionStrategy strategy = new(CompactionTriggers.Always, minimumPreserved: 1); MessageIndex groups = MessageIndex.Create( [ new ChatMessage(ChatRole.System, "You are helpful."), @@ -117,9 +115,9 @@ public class TruncationCompactionStrategyTests public async Task CompactAsyncPreservesToolCallGroupAtomicityAsync() { // Arrange - TruncationCompactionStrategy strategy = new(s_alwaysTrigger, minimumPreserved: 1); + TruncationCompactionStrategy strategy = new(CompactionTriggers.Always, minimumPreserved: 1); - ChatMessage assistantToolCall= new(ChatRole.Assistant, [new FunctionCallContent("call1", "get_weather")]); + ChatMessage assistantToolCall = new(ChatRole.Assistant, [new FunctionCallContent("call1", "get_weather")]); ChatMessage toolResult = new(ChatRole.Tool, "Sunny"); ChatMessage finalResponse = new(ChatRole.User, "Thanks!"); @@ -141,7 +139,7 @@ public class TruncationCompactionStrategyTests public async Task CompactAsyncSetsExcludeReasonAsync() { // Arrange - TruncationCompactionStrategy strategy = new(s_alwaysTrigger, minimumPreserved: 1); + TruncationCompactionStrategy strategy = new(CompactionTriggers.Always, minimumPreserved: 1); MessageIndex groups = MessageIndex.Create( [ new ChatMessage(ChatRole.User, "Old"), @@ -160,7 +158,7 @@ public class TruncationCompactionStrategyTests public async Task CompactAsyncSkipsAlreadyExcludedGroupsAsync() { // Arrange - TruncationCompactionStrategy strategy = new(s_alwaysTrigger, minimumPreserved: 1); + TruncationCompactionStrategy strategy = new(CompactionTriggers.Always, minimumPreserved: 1); MessageIndex groups = MessageIndex.Create( [ new ChatMessage(ChatRole.User, "Already excluded"), @@ -183,7 +181,7 @@ public class TruncationCompactionStrategyTests public async Task CompactAsyncMinimumPreservedKeepsMultipleAsync() { // Arrange — keep 2 most recent - TruncationCompactionStrategy strategy = new(s_alwaysTrigger, minimumPreserved: 2); + TruncationCompactionStrategy strategy = new(CompactionTriggers.Always, minimumPreserved: 2); MessageIndex groups = MessageIndex.Create( [ new ChatMessage(ChatRole.User, "Q1"), @@ -207,7 +205,7 @@ public class TruncationCompactionStrategyTests public async Task CompactAsyncNothingToRemoveReturnsFalseAsync() { // Arrange — preserve 5 but only 2 groups - TruncationCompactionStrategy strategy = new(s_alwaysTrigger, minimumPreserved: 5); + TruncationCompactionStrategy strategy = new(CompactionTriggers.Always, minimumPreserved: 5); MessageIndex groups = MessageIndex.Create( [ new ChatMessage(ChatRole.User, "Hello"), @@ -226,12 +224,12 @@ public class TruncationCompactionStrategyTests { // Arrange — always trigger, custom target stops after 1 exclusion int targetChecks = 0; - CompactionTrigger targetAfterOne = _ => ++targetChecks >= 1; + bool TargetAfterOne(MessageIndex _) => ++targetChecks >= 1; TruncationCompactionStrategy strategy = new( - s_alwaysTrigger, + CompactionTriggers.Always, minimumPreserved: 1, - target: targetAfterOne); + target: TargetAfterOne); MessageIndex groups = MessageIndex.Create( [ diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Microsoft.Agents.AI.UnitTests.csproj b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Microsoft.Agents.AI.UnitTests.csproj index 7fa417b184..ffa4417f34 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Microsoft.Agents.AI.UnitTests.csproj +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Microsoft.Agents.AI.UnitTests.csproj @@ -16,6 +16,7 @@ +