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 @@
+