diff --git a/dotnet/samples/02-agents/Agents/Agent_Step18_CompactionPipeline/Program.cs b/dotnet/samples/02-agents/Agents/Agent_Step18_CompactionPipeline/Program.cs
index 7849cb4656..9a3e935e01 100644
--- a/dotnet/samples/02-agents/Agents/Agent_Step18_CompactionPipeline/Program.cs
+++ b/dotnet/samples/02-agents/Agents/Agent_Step18_CompactionPipeline/Program.cs
@@ -13,6 +13,7 @@ using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Compaction;
using Microsoft.Extensions.AI;
+using static Microsoft.Agents.AI.Compaction.ChatHistoryCompactionPipeline;
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
@@ -39,33 +40,45 @@ static string LookupPrice([Description("The product name to look up.")] string p
};
// Configure the compaction pipeline with one of each strategy, ordered least to most aggressive.
-const int MaxTokens = 512;
-const int MaxTurns = 4;
+//const int MaxTokens = 512;
+//const int MaxTurns = 4;
ChatHistoryCompactionPipeline compactionPipeline =
- new(// 1. Gentle: collapse old tool-call groups into short summaries like "[Tool calls: LookupPrice]"
- new ToolResultCompactionStrategy(MaxTokens, preserveRecentGroups: 2),
+ //new(// 1. Gentle: collapse old tool-call groups into short summaries like "[Tool calls: LookupPrice]"
+ // new ToolResultCompactionStrategy(MaxTokens, preserveRecentGroups: 2),
- // 2. Moderate: use an LLM to summarize older conversation spans into a concise message
- new SummarizationCompactionStrategy(summarizerChatClient, MaxTokens, preserveRecentGroups: 2),
+ // // 2. Moderate: use an LLM to summarize older conversation spans into a concise message
+ // new SummarizationCompactionStrategy(summarizerChatClient, MaxTokens, preserveRecentGroups: 2),
- // 3. Aggressive: keep only the last N user turns and their responses
- new SlidingWindowCompactionStrategy(MaxTurns),
+ // // 3. Aggressive: keep only the last N user turns and their responses
+ // new SlidingWindowCompactionStrategy(MaxTurns),
- // 4. Emergency: drop oldest groups until under the token budget
- new TruncationCompactionStrategy(MaxTokens, preserveRecentGroups: 1));
+ // // 4. Emergency: drop oldest groups until under the token budget
+ // new TruncationCompactionStrategy(MaxTokens, preserveRecentGroups: 1));
+ Create(
+ Approach.Balanced,
+ Size.Compact,
+ summarizerChatClient);
// Create the agent with an in-memory chat history provider whose reducer is the compaction pipeline.
-AIAgent agent = agentChatClient.AsAIAgent(new ChatClientAgentOptions
-{
- Name = "ShoppingAssistant",
- ChatOptions = new()
- {
- Instructions = "You are a helpful shopping assistant. Help the user look up prices and compare products.",
- Tools = [AIFunctionFactory.Create(LookupPrice)],
- },
- ChatHistoryProvider = new InMemoryChatHistoryProvider(new() { ChatReducer = compactionPipeline }),
-});
+AIAgent agent =
+ agentChatClient.AsAIAgent(
+ new ChatClientAgentOptions
+ {
+ Name = "ShoppingAssistant",
+ ChatOptions = new()
+ {
+ Instructions =
+ """
+ You are a helpful, but long winded, shopping assistant.
+ Help the user look up prices and compare products.
+ When responding, Be sure to be extra descriptive and use as
+ many words as possible without sounding ridiculous.
+ """,
+ Tools = [AIFunctionFactory.Create(LookupPrice)],
+ },
+ ChatHistoryProvider = new InMemoryChatHistoryProvider(new() { ChatReducer = compactionPipeline }),
+ });
AgentSession session = await agent.CreateSessionAsync();
@@ -87,6 +100,7 @@ string[] prompts =
"Which product is the cheapest?",
"Can you compare the laptop and the keyboard for me?",
"What was the first product I asked about?",
+ "Thank you!",
];
foreach (string prompt in prompts)
diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/ChatHistoryCompactionPipeline.Factory.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/ChatHistoryCompactionPipeline.Factory.cs
new file mode 100644
index 0000000000..2e9cafd5a3
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/ChatHistoryCompactionPipeline.Factory.cs
@@ -0,0 +1,102 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using Microsoft.Extensions.AI;
+
+namespace Microsoft.Agents.AI.Compaction;
+
+public partial class ChatHistoryCompactionPipeline
+{
+ ///
+ /// %%% COMMENT
+ ///
+ public enum Size
+ {
+ ///
+ /// %%% COMMENT
+ ///
+ Compact,
+ ///
+ /// %%% COMMENT
+ ///
+ Adequate,
+ ///
+ /// %%% COMMENT
+ ///
+ Accomodating,
+ }
+
+ ///
+ /// %%% COMMENT
+ ///
+ public enum Approach
+ {
+ ///
+ /// %%% COMMENT
+ ///
+ Aggressive,
+ ///
+ /// %%% COMMENT
+ ///
+ Balanced,
+ ///
+ /// %%% COMMENT
+ ///
+ Gentle,
+ }
+
+ ///
+ /// %%% COMMENT
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static ChatHistoryCompactionPipeline Create(Approach approach, Size size, IChatClient chatClient) =>
+ approach switch
+ {
+ Approach.Aggressive => CreateAgressive(size, chatClient),
+ Approach.Balanced => CreateBalanced(size),
+ Approach.Gentle => CreateGentle(size),
+ _ => throw new NotImplementedException(), // %%% EXCEPTION
+ };
+
+ private static ChatHistoryCompactionPipeline CreateAgressive(Size size, IChatClient chatClient) =>
+ new(// 1. Gentle: collapse old tool-call groups into short summaries like "[Tool calls: LookupPrice]"
+ new ToolResultCompactionStrategy(MaxTokens(size), preserveRecentGroups: 2),
+ // 2. Moderate: use an LLM to summarize older conversation spans into a concise message
+ new SummarizationCompactionStrategy(chatClient, MaxTokens(size), preserveRecentGroups: 2),
+ // 3. Aggressive: keep only the last N user turns and their responses
+ new SlidingWindowCompactionStrategy(MaxTurns(size)),
+ // 4. Emergency: drop oldest groups until under the token budget
+ new TruncationCompactionStrategy(MaxTokens(size), preserveRecentGroups: 1));
+
+ private static ChatHistoryCompactionPipeline CreateBalanced(Size size) =>
+ new(// 1. Gentle: collapse old tool-call groups into short summaries like "[Tool calls: LookupPrice]"
+ new ToolResultCompactionStrategy(MaxTokens(size), preserveRecentGroups: 2),
+ // 2. Aggressive: keep only the last N user turns and their responses
+ new SlidingWindowCompactionStrategy(MaxTurns(size)));
+
+ private static ChatHistoryCompactionPipeline CreateGentle(Size size) =>
+ new(// 1. Gentle: collapse old tool-call groups into short summaries like "[Tool calls: LookupPrice]"
+ new ToolResultCompactionStrategy(MaxTokens(size), preserveRecentGroups: 2));
+
+ private static int MaxTokens(Size size) =>
+ size switch
+ {
+ Size.Compact => 500,
+ Size.Adequate => 1000,
+ Size.Accomodating => 2000,
+ _ => throw new NotImplementedException(), // %%% EXCEPTION
+ };
+
+ private static int MaxTurns(Size size) =>
+ size switch
+ {
+ Size.Compact => 10,
+ Size.Adequate => 50,
+ Size.Accomodating => 100,
+ _ => throw new NotImplementedException(), // %%% EXCEPTION
+ };
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/ChatHistoryCompactionPipeline.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/ChatHistoryCompactionPipeline.cs
index 8b678602a1..9dc4c4e507 100644
--- a/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/ChatHistoryCompactionPipeline.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/ChatHistoryCompactionPipeline.cs
@@ -1,7 +1,8 @@
// Copyright (c) Microsoft. All rights reserved.
+using System;
using System.Collections.Generic;
-using System.Linq;
+using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
@@ -24,7 +25,7 @@ namespace Microsoft.Agents.AI.Compaction;
/// accepted (e.g., ).
///
///
-public class ChatHistoryCompactionPipeline : IChatReducer
+public partial class ChatHistoryCompactionPipeline : IChatReducer
{
private readonly ChatHistoryCompactionStrategy[] _strategies;
private readonly IChatHistoryMetricsCalculator _metricsCalculator;
@@ -52,7 +53,7 @@ public class ChatHistoryCompactionPipeline : IChatReducer
IChatHistoryMetricsCalculator? metricsCalculator,
params IEnumerable strategies)
{
- this._strategies = Throw.IfNull(strategies).ToArray();
+ this._strategies = [.. Throw.IfNull(strategies)];
this._metricsCalculator = metricsCalculator ?? DefaultChatHistoryMetricsCalculator.Instance;
}
@@ -66,9 +67,9 @@ public class ChatHistoryCompactionPipeline : IChatReducer
IEnumerable messages,
CancellationToken cancellationToken = default)
{
- List messageList = messages.ToList(); // %%% HAXX
- await this.CompactAsync(messageList, cancellationToken).ConfigureAwait(false);
- return messageList;
+ List messageBuffer = messages is List messageList ? messageList : [.. messages];
+ await this.CompactAsync(messageBuffer, cancellationToken).ConfigureAwait(false);
+ return messageBuffer;
}
///
@@ -77,26 +78,37 @@ public class ChatHistoryCompactionPipeline : IChatReducer
/// The mutable message list to compact.
/// The to monitor for cancellation requests.
/// A with aggregate and per-strategy metrics.
- public async ValueTask CompactAsync( // %%% SCOPE
- IList messages,
+ public async ValueTask CompactAsync(
+ List messages,
CancellationToken cancellationToken = default)
{
Throw.IfNull(messages);
- IReadOnlyList readOnlyMessages = messages as IReadOnlyList ?? [.. messages]; // %%% TYPE CONSISTENCY
- CompactionMetric overallBefore = this._metricsCalculator.Calculate(readOnlyMessages);
+ ChatHistoryMetric overallBefore = this._metricsCalculator.Calculate(messages);
- List results = new(this._strategies.Length);
+ Debug.WriteLine($"COMPACTION: BEGIN x{overallBefore.MessageCount}/#{overallBefore.UserTurnCount} ({overallBefore.TokenCount} tokens)");
+ List compactionResults = new(this._strategies.Length);
+
+ Stopwatch timer = new();
+ TimeSpan startTime = TimeSpan.Zero;
+ ChatHistoryMetric overallAfter = overallBefore;
+ ChatHistoryMetric currentBefore = overallBefore;
foreach (ChatHistoryCompactionStrategy strategy in this._strategies)
{
- CompactionResult result = await strategy.CompactAsync(messages, this._metricsCalculator, cancellationToken).ConfigureAwait(false);
- results.Add(result);
+ // %%% VERBOSE - Debug.WriteLine($"COMPACTION: {strategy.Name} START");
+ timer.Start();
+ ChatHistoryCompactionStrategy.s_currentMetrics.Value = currentBefore;
+ CompactionResult strategyResult = await strategy.CompactAsync(messages, this._metricsCalculator, cancellationToken).ConfigureAwait(false);
+ timer.Stop();
+ TimeSpan elapsedTime = timer.Elapsed - startTime;
+ // %%% VERBOSE - Debug.WriteLine($"COMPACTION: {strategy.Name} FINISH [{elapsedTime}]");
+ compactionResults.Add(strategyResult);
+ overallAfter = currentBefore = strategyResult.After;
}
- readOnlyMessages = messages as IReadOnlyList ?? [.. messages];
- CompactionMetric overallAfter = this._metricsCalculator.Calculate(readOnlyMessages);
+ Debug.WriteLineIf(overallBefore.TokenCount != overallAfter.TokenCount, $"COMPACTION: TOTAL [{timer.Elapsed}] {overallBefore.TokenCount} => {overallAfter.TokenCount} tokens");
- return new(overallBefore, overallAfter, results);
+ return new(overallBefore, overallAfter, compactionResults);
}
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/ChatHistoryCompactionStrategy.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/ChatHistoryCompactionStrategy.cs
index 2bb8e01e40..5d78bfe6e5 100644
--- a/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/ChatHistoryCompactionStrategy.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/ChatHistoryCompactionStrategy.cs
@@ -2,8 +2,7 @@
using System;
using System.Collections.Generic;
-using System.Collections.ObjectModel;
-using System.Linq;
+using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
@@ -21,7 +20,7 @@ namespace Microsoft.Agents.AI.Compaction;
/// while the strategy adds:
///
/// - A conditional trigger via that decides whether compaction runs.
-/// - Before/after reporting via .
+/// - Before/after reporting via .
///
///
///
@@ -36,7 +35,7 @@ namespace Microsoft.Agents.AI.Compaction;
///
public abstract class ChatHistoryCompactionStrategy
{
- private static readonly AsyncLocal s_currentMetrics = new();
+ internal static readonly AsyncLocal s_currentMetrics = new();
///
/// Initializes a new instance of the class.
@@ -48,9 +47,9 @@ public abstract class ChatHistoryCompactionStrategy
}
///
- /// Exposes the current for the executing strategy, allowing to make informed decisions.
+ /// Exposes the current for the executing strategy, allowing to make informed decisions.
///
- protected static CompactionMetric CurrentMetrics => s_currentMetrics.Value ?? throw new InvalidOperationException($"No active {nameof(ChatHistoryCompactionStrategy)}.");
+ protected static ChatHistoryMetric CurrentMetrics => s_currentMetrics.Value ?? throw new InvalidOperationException($"No active {nameof(ChatHistoryCompactionStrategy)}.");
///
/// Gets the that performs the actual message compaction.
@@ -72,48 +71,50 @@ public abstract class ChatHistoryCompactionStrategy
///
/// to proceed with compaction; to skip.
///
- public abstract bool ShouldCompact(CompactionMetric metrics);
+ protected abstract bool ShouldCompact(ChatHistoryMetric metrics);
///
/// Execute this strategy: check the trigger, delegate to the , and report metrics.
///
- /// The mutable message list to compact.
+ /// The mutable message list to compact.
/// The calculator to use for metric snapshots.
/// The to monitor for cancellation requests.
/// A reporting the outcome.
- public async ValueTask CompactAsync(
- IList messages,
+ internal async ValueTask CompactAsync(
+ List history,
IChatHistoryMetricsCalculator metricsCalculator,
CancellationToken cancellationToken = default)
{
- messages = Throw.IfNull(messages);
Throw.IfNull(metricsCalculator);
+ Throw.IfNull(history);
- List? messageList = messages as List;
- ReadOnlyCollection snapshot = messageList is not null ? messageList.AsReadOnly() : new(messages);
- CompactionMetric before = metricsCalculator.Calculate(snapshot);
- s_currentMetrics.Value = before;
- if (!this.ShouldCompact(before))
+ ChatHistoryMetric beforeMetrics = CurrentMetrics;
+ if (!this.ShouldCompact(beforeMetrics))
{
- return CompactionResult.Skipped(this.Name, before);
+ // %%% VERBOSE - Debug.WriteLine($"COMPACTION: {this.Name} - Skipped");
+ return CompactionResult.Skipped(this.Name, beforeMetrics);
}
- ChatMessage[] reduced = (await this.Reducer.ReduceAsync(snapshot, cancellationToken).ConfigureAwait(false)).ToArray();
+ Debug.WriteLine($"COMPACTION: {this.Name} - Reducing");
- bool modified = reduced.Length != snapshot.Count;
+ IEnumerable reducerResult = await this.Reducer.ReduceAsync(history, cancellationToken).ConfigureAwait(false);
+
+ // Ensure we have a concrete collection to avoid multiple enumerations of the reducer result, which could be costly if it's an iterator.
+ ChatMessage[] reducedCopy = [.. reducerResult];
+
+ bool modified = reducedCopy.Length != history.Count;
if (modified)
{
- messages.Clear();
- foreach (ChatMessage message in reduced)
- {
- messages.Add(message);
- }
+ history.Clear();
+ history.AddRange(reducedCopy);
}
- CompactionMetric after = modified
- ? metricsCalculator.Calculate(reduced)
- : before;
+ ChatHistoryMetric afterMetrics = modified
+ ? metricsCalculator.Calculate(reducedCopy)
+ : beforeMetrics;
- return new(this.Name, applied: modified, before, after);
+ Debug.WriteLine($"COMPACTION: {this.Name} - Tokens {beforeMetrics.TokenCount} => {afterMetrics.TokenCount}");
+
+ return new(this.Name, applied: modified, beforeMetrics, afterMetrics);
}
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/CompactionMetric.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/ChatHistoryMetric.cs
similarity index 97%
rename from dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/CompactionMetric.cs
rename to dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/ChatHistoryMetric.cs
index e4d641623b..f2d9694dfe 100644
--- a/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/CompactionMetric.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/ChatHistoryMetric.cs
@@ -7,7 +7,7 @@ namespace Microsoft.Agents.AI.Compaction;
///
/// Immutable snapshot of conversation metrics used for compaction trigger evaluation and reporting.
///
-public sealed class CompactionMetric
+public sealed class ChatHistoryMetric
{
///
/// Gets the estimated token count across all messages.
diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/ChatReducerCompactionStrategy.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/ChatReducerCompactionStrategy.cs
new file mode 100644
index 0000000000..7305e86cd5
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/ChatReducerCompactionStrategy.cs
@@ -0,0 +1,35 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using Microsoft.Extensions.AI;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Agents.AI.Compaction;
+
+///
+/// Represents a chat history compaction strategy that uses a condition function to determine when compaction should
+/// occur.
+///
+///
+/// This strategy evaluates a user-provided condition against compaction metrics to decide whether to
+/// compact the chat history. It is useful for scenarios where compaction should be triggered based on custom thresholds
+/// or criteria. Inherits from ChatHistoryCompactionStrategy.
+///
+public class ChatReducerCompactionStrategy : ChatHistoryCompactionStrategy
+{
+ private readonly Func _condition;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ChatReducerCompactionStrategy(
+ IChatReducer reducer,
+ Func condition)
+ : base(reducer)
+ {
+ this._condition = Throw.IfNull(condition);
+ }
+
+ ///
+ protected override bool ShouldCompact(ChatHistoryMetric metrics) => this._condition(metrics);
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/CompactionPipelineResult.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/CompactionPipelineResult.cs
index ab7e6448ae..c416528a83 100644
--- a/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/CompactionPipelineResult.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/CompactionPipelineResult.cs
@@ -18,8 +18,8 @@ public sealed class CompactionPipelineResult
/// Metrics of the conversation after all strategies ran.
/// Per-strategy results in execution order.
internal CompactionPipelineResult(
- CompactionMetric before,
- CompactionMetric after,
+ ChatHistoryMetric before,
+ ChatHistoryMetric after,
IReadOnlyList strategyResults)
{
this.Before = Throw.IfNull(before);
@@ -30,12 +30,12 @@ public sealed class CompactionPipelineResult
///
/// Gets the conversation metrics before any compaction strategy ran.
///
- public CompactionMetric Before { get; }
+ public ChatHistoryMetric Before { get; }
///
/// Gets the conversation metrics after all compaction strategies ran.
///
- public CompactionMetric After { get; }
+ public ChatHistoryMetric After { get; }
///
/// Gets the per-strategy results in execution order.
diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/CompactionResult.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/CompactionResult.cs
index c6eca6625e..2c4b2ad13e 100644
--- a/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/CompactionResult.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/CompactionResult.cs
@@ -16,7 +16,7 @@ public sealed class CompactionResult
/// Whether the strategy modified the message list.
/// Metrics before the strategy ran.
/// Metrics after the strategy ran.
- public CompactionResult(string strategyName, bool applied, CompactionMetric before, CompactionMetric after)
+ public CompactionResult(string strategyName, bool applied, ChatHistoryMetric before, ChatHistoryMetric after)
{
this.StrategyName = Throw.IfNullOrWhitespace(strategyName);
this.Applied = applied;
@@ -37,12 +37,12 @@ public sealed class CompactionResult
///
/// Gets the conversation metrics before the strategy executed.
///
- public CompactionMetric Before { get; }
+ public ChatHistoryMetric Before { get; }
///
/// Gets the conversation metrics after the strategy executed.
///
- public CompactionMetric After { get; }
+ public ChatHistoryMetric After { get; }
///
/// Creates a representing a skipped strategy.
@@ -50,6 +50,6 @@ public sealed class CompactionResult
/// The name of the skipped strategy.
/// The current conversation metrics.
/// A result indicating no compaction was applied.
- internal static CompactionResult Skipped(string strategyName, CompactionMetric metrics)
+ internal static CompactionResult Skipped(string strategyName, ChatHistoryMetric metrics)
=> new(strategyName, applied: false, metrics, metrics);
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/DefaultChatHistoryMetricsCalculator.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/DefaultChatHistoryMetricsCalculator.cs
index 19b9258584..2491f99591 100644
--- a/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/DefaultChatHistoryMetricsCalculator.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/DefaultChatHistoryMetricsCalculator.cs
@@ -46,7 +46,7 @@ public sealed class DefaultChatHistoryMetricsCalculator : IChatHistoryMetricsCal
}
///
- public CompactionMetric Calculate(IReadOnlyList messages)
+ public ChatHistoryMetric Calculate(IReadOnlyList messages)
{
if (messages is null || messages.Count == 0)
{
diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/IChatHistoryMetricsCalculator.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/IChatHistoryMetricsCalculator.cs
index 830268dc76..3c4c124444 100644
--- a/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/IChatHistoryMetricsCalculator.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/IChatHistoryMetricsCalculator.cs
@@ -9,18 +9,18 @@ namespace Microsoft.Agents.AI.Compaction;
// and whether custom metrics calculators are a realistic extension point.
///
-/// Computes for a list of messages.
+/// Computes for a list of messages.
///
///
/// Token counting is model-specific. Implementations can provide precise tokenization
/// (e.g., using tiktoken or a model-specific tokenizer) or use estimation heuristics.
///
-public interface IChatHistoryMetricsCalculator // %%% NEEDED ???
+public interface IChatHistoryMetricsCalculator
{
///
/// Compute metrics for the given messages.
///
/// The messages to analyze.
- /// A snapshot.
- CompactionMetric Calculate(IReadOnlyList messages);
+ /// A snapshot.
+ ChatHistoryMetric Calculate(IReadOnlyList messages);
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/SlidingWindowCompactionStrategy.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/SlidingWindowCompactionStrategy.cs
index 9d8f12fc62..51b5b90a1d 100644
--- a/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/SlidingWindowCompactionStrategy.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/SlidingWindowCompactionStrategy.cs
@@ -1,6 +1,5 @@
// Copyright (c) Microsoft. All rights reserved.
-using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
@@ -44,7 +43,7 @@ public class SlidingWindowCompactionStrategy : ChatHistoryCompactionStrategy
}
///
- public override bool ShouldCompact(CompactionMetric metrics) =>
+ protected override bool ShouldCompact(ChatHistoryMetric metrics) =>
metrics.UserTurnCount > this._maxTurns;
///
@@ -57,25 +56,21 @@ public class SlidingWindowCompactionStrategy : ChatHistoryCompactionStrategy
IEnumerable messages,
CancellationToken cancellationToken = default)
{
- IReadOnlyList messageList = [.. messages];
+ IReadOnlyList messageList = [.. messages]; // %%% PERFORMANCE
IReadOnlyList groups = CurrentMetrics.Groups;
// Find the group-list indices where each user turn starts
- //int[] turnGroupIndices = groups.Where(group => group.Kind == ChatMessageGroupKind.UserTurn).Select(group => group.StartIndex).ToArray(); // %%% TODO
- List turnGroupIndices = [];
- for (int i = 0; i < groups.Count; i++)
- {
- if (groups[i].Kind == ChatMessageGroupKind.UserTurn)
- {
- turnGroupIndices.Add(i);
- }
- }
+ int[] turnGroupIndices =
+ [.. CurrentMetrics.Groups
+ .Select((group, index) => (group, index))
+ .Where(t => t.group.Kind == ChatMessageGroupKind.UserTurn)
+ .Select(t => t.index)];
// Keep the last maxTurns user turns and everything after the first kept turn
- int firstKeptTurnIndex = turnGroupIndices.Count - maxTurns;
+ int firstKeptTurnIndex = turnGroupIndices.Length - maxTurns;
int firstKeptGroupIndex = turnGroupIndices[firstKeptTurnIndex];
- List result = new(messageList.Count);
+ List result = new(messageList.Count); // %%% PERFORMANCE
for (int gi = 0; gi < groups.Count; gi++)
{
ChatMessageGroup group = groups[gi];
diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/SummarizationCompactionStrategy.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/SummarizationCompactionStrategy.cs
index 433b7c0f04..028077beae 100644
--- a/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/SummarizationCompactionStrategy.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/SummarizationCompactionStrategy.cs
@@ -69,7 +69,7 @@ public class SummarizationCompactionStrategy : ChatHistoryCompactionStrategy
}
///
- public override bool ShouldCompact(CompactionMetric metrics) =>
+ protected override bool ShouldCompact(ChatHistoryMetric metrics) =>
metrics.TokenCount > this._maxTokens;
///
@@ -96,7 +96,7 @@ public class SummarizationCompactionStrategy : ChatHistoryCompactionStrategy
IReadOnlyList messageList = [.. messages];
IReadOnlyList groups = CurrentMetrics.Groups;
- List nonSystemGroups = groups.Where(g => g.Kind != ChatMessageGroupKind.System).ToList();
+ List nonSystemGroups = [.. groups.Where(g => g.Kind != ChatMessageGroupKind.System)];
int protectedFromIndex = Math.Max(0, nonSystemGroups.Count - this._preserveRecentGroups);
if (protectedFromIndex == 0)
diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/ToolResultCompactionStrategy.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/ToolResultCompactionStrategy.cs
index 424943262b..26380d6d6d 100644
--- a/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/ToolResultCompactionStrategy.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/ToolResultCompactionStrategy.cs
@@ -29,6 +29,11 @@ namespace Microsoft.Agents.AI.Compaction;
///
public class ToolResultCompactionStrategy : ChatHistoryCompactionStrategy
{
+ ///
+ /// The default value for `preserveRecentGroups` used when constructing .
+ ///
+ public const int DefaultPreserveRecentGroups = 2;
+
private readonly int _maxTokens;
///
@@ -39,14 +44,14 @@ public class ToolResultCompactionStrategy : ChatHistoryCompactionStrategy
/// The number of most-recent non-system message groups to protect from collapsing.
/// Defaults to 2, ensuring the current turn's tool interactions remain visible.
///
- public ToolResultCompactionStrategy(int maxTokens, int preserveRecentGroups = 2)
+ public ToolResultCompactionStrategy(int maxTokens, int preserveRecentGroups = DefaultPreserveRecentGroups)
: base(new ToolResultClearingReducer(preserveRecentGroups))
{
this._maxTokens = maxTokens;
}
///
- public override bool ShouldCompact(CompactionMetric metrics) =>
+ protected override bool ShouldCompact(ChatHistoryMetric metrics) =>
metrics.TokenCount > this._maxTokens && metrics.ToolCallCount > 0;
///
@@ -62,7 +67,7 @@ public class ToolResultCompactionStrategy : ChatHistoryCompactionStrategy
IReadOnlyList messageList = [.. messages];
IReadOnlyList groups = CurrentMetrics.Groups;
- List nonSystemGroups = groups.Where(g => g.Kind != ChatMessageGroupKind.System).ToList();
+ List nonSystemGroups = [.. groups.Where(g => g.Kind != ChatMessageGroupKind.System)];
int protectedFromIndex = Math.Max(0, nonSystemGroups.Count - preserveRecentGroups);
HashSet protectedGroupStarts = [];
for (int i = protectedFromIndex; i < nonSystemGroups.Count; i++)
diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/TruncationCompactionStrategy.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/TruncationCompactionStrategy.cs
index 1390f463fc..a2a6b07acf 100644
--- a/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/TruncationCompactionStrategy.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/TruncationCompactionStrategy.cs
@@ -41,7 +41,7 @@ public class TruncationCompactionStrategy : ChatHistoryCompactionStrategy
}
///
- public override bool ShouldCompact(CompactionMetric metrics) =>
+ protected override bool ShouldCompact(ChatHistoryMetric metrics) =>
metrics.TokenCount > this._maxTokens;
///
@@ -56,15 +56,15 @@ public class TruncationCompactionStrategy : ChatHistoryCompactionStrategy
{
IReadOnlyList messageList = [.. messages];
- List removableGroups = CurrentMetrics.Groups.Where(g => g.Kind != ChatMessageGroupKind.System).ToList();
+ ChatMessageGroup[] removableGroups = [.. CurrentMetrics.Groups.Where(g => g.Kind != ChatMessageGroupKind.System)];
- if (removableGroups.Count == 0)
+ if (removableGroups.Length == 0)
{
return Task.FromResult>(messageList);
}
// Remove oldest non-system groups, keeping at least preserveRecentGroups.
- int maxRemovable = removableGroups.Count - preserveRecentGroups;
+ int maxRemovable = removableGroups.Length - preserveRecentGroups;
if (maxRemovable <= 0)
{
diff --git a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/ChatHistoryCompactionPipelineTests.cs b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/ChatHistoryCompactionPipelineTests.cs
index d5d58d7ba9..75658054d1 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/ChatHistoryCompactionPipelineTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/ChatHistoryCompactionPipelineTests.cs
@@ -14,11 +14,14 @@ public class ChatHistoryCompactionPipelineTests
[Fact]
public async Task EmptyStrategies_ReturnsUnmodifiedAsync()
{
+ // Arrange
ChatHistoryCompactionPipeline pipeline = new([]);
List messages = [new(ChatRole.User, "Hello")];
+ // Act
CompactionPipelineResult result = await pipeline.CompactAsync(messages);
+ // Assert
Assert.False(result.AnyApplied);
Assert.Equal(1, result.Before.MessageCount);
Assert.Equal(1, result.After.MessageCount);
@@ -28,6 +31,7 @@ public class ChatHistoryCompactionPipelineTests
[Fact]
public async Task ChainsStrategies_InOrderAsync()
{
+ // Arrange
ChatHistoryCompactionStrategy[] strategies =
[
new NeverCompactStrategy(),
@@ -40,8 +44,10 @@ public class ChatHistoryCompactionPipelineTests
new(ChatRole.User, "Second"),
];
+ // Act
CompactionPipelineResult result = await pipeline.CompactAsync(messages);
+ // Assert
Assert.True(result.AnyApplied);
Assert.Equal(2, result.StrategyResults.Count);
Assert.False(result.StrategyResults[0].Applied);
@@ -52,6 +58,7 @@ public class ChatHistoryCompactionPipelineTests
[Fact]
public async Task ReportsOverallMetricsAsync()
{
+ // Arrange
ChatHistoryCompactionPipeline pipeline = new([new RemoveFirstMessageStrategy()]);
List messages =
[
@@ -60,8 +67,10 @@ public class ChatHistoryCompactionPipelineTests
new(ChatRole.User, "Third"),
];
+ // Act
CompactionPipelineResult result = await pipeline.CompactAsync(messages);
+ // Assert
Assert.Equal(3, result.Before.MessageCount);
Assert.Equal(2, result.After.MessageCount);
}
@@ -69,51 +78,39 @@ public class ChatHistoryCompactionPipelineTests
[Fact]
public async Task CustomMetricsCalculator_IsUsedAsync()
{
+ // Arrange
Moq.Mock calcMock = new();
calcMock
.Setup(c => c.Calculate(Moq.It.IsAny>()))
- .Returns(new CompactionMetric { MessageCount = 42 });
-
+ .Returns(new ChatHistoryMetric { MessageCount = 42 });
ChatHistoryCompactionPipeline pipeline = new(calcMock.Object, []);
List messages = [new(ChatRole.User, "Hello")];
+ // Act
CompactionPipelineResult result = await pipeline.CompactAsync(messages);
+ // Assert
Assert.Equal(42, result.Before.MessageCount);
- calcMock.Verify(c => c.Calculate(Moq.It.IsAny>()), Moq.Times.AtLeast(2));
- }
-
- [Fact]
- public async Task CompactAsync_NonReadOnlyListMessages_WorksAsync()
- {
- ChatHistoryCompactionPipeline pipeline = new([new RemoveFirstMessageStrategy()]);
- NonReadOnlyList messages = new(
- [
- new(ChatRole.User, "First"),
- new(ChatRole.User, "Second"),
- ]);
-
- CompactionPipelineResult result = await pipeline.CompactAsync(messages);
-
- Assert.True(result.AnyApplied);
- Assert.Single(messages);
+ calcMock.Verify(c => c.Calculate(Moq.It.IsAny>()), Moq.Times.Once);
}
[Fact]
public async Task ReduceAsync_DelegatesCompactionAsync()
{
+ // Arrange
ChatHistoryCompactionPipeline pipeline = new([new RemoveFirstMessageStrategy()]);
-
- ChatMessage[] messages =
+ List messages =
[
new(ChatRole.User, "First"),
new(ChatRole.User, "Second"),
new(ChatRole.User, "Third"),
];
- IEnumerable result = await ((IChatReducer)pipeline).ReduceAsync(messages, default);
+ // Act
+ IEnumerable result = await pipeline.ReduceAsync(messages, default);
List resultList = result.ToList();
+ // Assert
Assert.Equal(2, resultList.Count);
Assert.Equal("Second", resultList[0].Text);
Assert.Equal("Third", resultList[1].Text);
@@ -122,16 +119,18 @@ public class ChatHistoryCompactionPipelineTests
[Fact]
public async Task ReduceAsync_EmptyStrategies_ReturnsAllMessagesAsync()
{
+ // Arrange
ChatHistoryCompactionPipeline pipeline = new([]);
-
ChatMessage[] messages =
[
new(ChatRole.User, "Hello"),
new(ChatRole.User, "World"),
];
- IEnumerable result = await ((IChatReducer)pipeline).ReduceAsync(messages, default);
+ // Act
+ IEnumerable result = await pipeline.ReduceAsync(messages, default);
+ // Assert
Assert.Equal(2, result.Count());
}
}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/ChatHistoryCompactionStrategyTests.cs b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/ChatHistoryCompactionStrategyTests.cs
index 5c2e006350..a0635de76e 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/ChatHistoryCompactionStrategyTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/ChatHistoryCompactionStrategyTests.cs
@@ -17,28 +17,32 @@ public class ChatHistoryCompactionStrategyTests
[Fact]
public async Task ShouldCompactReturnsFalse_SkipsAsync()
{
- NeverCompactStrategy strategy = new();
+ // Arrange
List messages = [new(ChatRole.User, "Hello")];
- DefaultChatHistoryMetricsCalculator calculator = new();
+ NeverCompactStrategy strategy = new();
- CompactionResult result = await strategy.CompactAsync(messages, calculator);
+ // Act
+ CompactionResult result = await RunCompactionStrategyAsync(strategy, messages);
+ // Assert
Assert.False(result.Applied);
}
[Fact]
public async Task ShouldCompactReturnsTrue_RunsCompactionAsync()
{
- RemoveFirstMessageStrategy strategy = new();
+ // Arrange
List messages =
[
new(ChatRole.User, "First"),
new(ChatRole.User, "Second"),
];
- DefaultChatHistoryMetricsCalculator calculator = new();
+ RemoveFirstMessageStrategy strategy = new();
- CompactionResult result = await strategy.CompactAsync(messages, calculator);
+ // Act
+ CompactionResult result = await RunCompactionStrategyAsync(strategy, messages);
+ // Assert
Assert.True(result.Applied);
Assert.Single(messages);
Assert.Equal("Second", messages[0].Text);
@@ -47,23 +51,24 @@ public class ChatHistoryCompactionStrategyTests
}
[Fact]
- public async Task DelegatesToIChatReducerAsync()
+ public async Task DelegatesToReducerAsync()
{
- Mock reducerMock = new();
- reducerMock
- .Setup(r => r.ReduceAsync(It.IsAny>(), It.IsAny()))
- .ReturnsAsync((IEnumerable msgs, CancellationToken _) => msgs.Skip(1));
-
- TestCompactionStrategy strategy = new(reducerMock.Object);
+ // Arrange
List messages =
[
new(ChatRole.User, "First"),
new(ChatRole.User, "Second"),
];
- DefaultChatHistoryMetricsCalculator calculator = new();
+ Mock reducerMock = new();
+ reducerMock
+ .Setup(r => r.ReduceAsync(It.IsAny>(), It.IsAny()))
+ .ReturnsAsync((IEnumerable messages, CancellationToken _) => messages.Skip(1));
+ TestCompactionStrategy strategy = new(reducerMock.Object);
- CompactionResult result = await strategy.CompactAsync(messages, calculator);
+ // Act
+ CompactionResult result = await RunCompactionStrategyAsync(strategy, messages);
+ // Assert
Assert.True(result.Applied);
Assert.Single(messages);
Assert.Equal("Second", messages[0].Text);
@@ -73,88 +78,55 @@ public class ChatHistoryCompactionStrategyTests
[Fact]
public async Task ReducerNoChange_ReturnsFalseAsync()
{
- Mock reducerMock = new();
- reducerMock
- .Setup(r => r.ReduceAsync(It.IsAny>(), It.IsAny()))
- .ReturnsAsync((IEnumerable msgs, CancellationToken _) => msgs);
-
- TestCompactionStrategy strategy = new(reducerMock.Object);
+ // Arrange
List messages =
[
new(ChatRole.User, "Hello"),
];
- DefaultChatHistoryMetricsCalculator calculator = new();
+ Mock reducerMock = new();
+ reducerMock
+ .Setup(r => r.ReduceAsync(It.IsAny>(), It.IsAny()))
+ .ReturnsAsync((IEnumerable msgs, CancellationToken _) => msgs);
+ TestCompactionStrategy strategy = new(reducerMock.Object, shouldCompact: false);
- CompactionResult result = await strategy.CompactAsync(messages, calculator);
+ // Act
+ CompactionResult result = await RunCompactionStrategyAsync(strategy, messages);
+ // Assert
Assert.False(result.Applied);
Assert.Single(messages);
}
[Fact]
- public void ExposesReducer()
+ public void ReducerLifecycle()
{
+ // Arrange
Mock reducerMock = new();
+
+ // Act
TestCompactionStrategy strategy = new(reducerMock.Object);
+ // Assert
Assert.Same(reducerMock.Object, strategy.Reducer);
- }
-
- [Fact]
- public void DefaultName_IsReducerTypeName()
- {
- Mock reducerMock = new();
- TestCompactionStrategy strategy = new(reducerMock.Object);
-
- // Moq proxy type name is used since we're using a mock
Assert.NotNull(strategy.Name);
Assert.NotEmpty(strategy.Name);
- }
-
- [Fact]
- public void ConditionDelegate_ReturnsTrue_ShouldCompactReturnsTrue()
- {
- Mock reducerMock = new();
- TestCompactionStrategy strategy = new(reducerMock.Object);
-
- CompactionMetric metrics = new() { TokenCount = 100 };
- Assert.True(strategy.ShouldCompact(metrics));
- }
-
- [Fact]
- public void ConditionDelegate_ReturnsFalse_ShouldCompactReturnsFalse()
- {
- Mock reducerMock = new();
- TestCompactionStrategy strategy = new(reducerMock.Object, shouldCompact: false);
-
- CompactionMetric metrics = new() { TokenCount = 100 };
- Assert.False(strategy.ShouldCompact(metrics));
- }
-
- [Fact]
- public async Task CompactAsync_NonReadOnlyListMessages_WorksAsync()
- {
- RemoveFirstMessageStrategy strategy = new();
- NonReadOnlyList messages = new(
- [
- new(ChatRole.User, "First"),
- new(ChatRole.User, "Second"),
- ]);
- DefaultChatHistoryMetricsCalculator calculator = new();
-
- CompactionResult result = await strategy.CompactAsync(messages, calculator);
-
- Assert.True(result.Applied);
- Assert.Single(messages);
- Assert.Equal("Second", messages[0].Text);
+ Assert.Equal(reducerMock.Object.GetType().Name, strategy.Name);
}
[Fact]
public void CurrentMetrics_OutsideStrategy_Throws()
{
+ // Act & Assert
Assert.Throws(() => TestCompactionStrategy.GetCurrentMetrics());
}
+ public static async ValueTask RunCompactionStrategyAsync(ChatHistoryCompactionStrategy strategy, List messages)
+ {
+ // Act
+ ChatHistoryCompactionStrategy.s_currentMetrics.Value = DefaultChatHistoryMetricsCalculator.Instance.Calculate(messages);
+ return await strategy.CompactAsync(messages, DefaultChatHistoryMetricsCalculator.Instance);
+ }
+
private sealed class TestCompactionStrategy : ChatHistoryCompactionStrategy
{
private readonly bool _shouldCompact;
@@ -165,8 +137,8 @@ public class ChatHistoryCompactionStrategyTests
this._shouldCompact = shouldCompact;
}
- public override bool ShouldCompact(CompactionMetric metrics) => this._shouldCompact;
+ protected override bool ShouldCompact(ChatHistoryMetric metrics) => this._shouldCompact;
- public static CompactionMetric GetCurrentMetrics() => CurrentMetrics;
+ public static ChatHistoryMetric GetCurrentMetrics() => CurrentMetrics;
}
}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/ChatReducerCompactionStrategyTests.cs b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/ChatReducerCompactionStrategyTests.cs
new file mode 100644
index 0000000000..844f7f55ea
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/ChatReducerCompactionStrategyTests.cs
@@ -0,0 +1,114 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Agents.AI.Compaction;
+using Microsoft.Extensions.AI;
+using Moq;
+
+namespace Microsoft.Agents.AI.Abstractions.UnitTests.Compaction;
+
+public class ChatReducerCompactionStrategyTests : CompactionStrategyTestBase
+{
+ [Fact]
+ public async Task ConditionFalse_SkipsAsync()
+ {
+ // Arrange
+ List messages = [new(ChatRole.User, "Hello")];
+ Mock reducerMock = new();
+ ChatReducerCompactionStrategy strategy = new(reducerMock.Object, _ => false);
+
+ // Act & Assert
+ await RunCompactionStrategySkippedAsync(strategy, messages);
+
+ // Assert
+ reducerMock.Verify(
+ r => r.ReduceAsync(It.IsAny>(), It.IsAny()),
+ Times.Never);
+ }
+
+ [Fact]
+ public async Task ConditionTrue_RunsReducerAsync()
+ {
+ // Arrange
+ List messages =
+ [
+ new(ChatRole.User, "First"),
+ new(ChatRole.User, "Second"),
+ ];
+ Mock reducerMock = new();
+ reducerMock
+ .Setup(r => r.ReduceAsync(It.IsAny>(), It.IsAny()))
+ .ReturnsAsync((IEnumerable msgs, CancellationToken _) => msgs.Skip(1));
+ ChatReducerCompactionStrategy strategy = new(reducerMock.Object, _ => true);
+
+ // Act & Assert
+ await RunCompactionStrategyReducedAsync(strategy, messages, expectedCount: 1);
+
+ // Assert
+ Assert.Equal("Second", messages[0].Text);
+ reducerMock.Verify(
+ r => r.ReduceAsync(It.IsAny>(), It.IsAny()),
+ Times.Once);
+ }
+
+ [Fact]
+ public async Task ConditionReceivesMetricsAsync()
+ {
+ // Arrange
+ List messages =
+ [
+ new(ChatRole.User, "Hello"),
+ new(ChatRole.Assistant, "Hi"),
+ ];
+ ChatHistoryMetric? capturedMetrics = null;
+ Mock reducerMock = new();
+ reducerMock
+ .Setup(r => r.ReduceAsync(It.IsAny>(), It.IsAny()))
+ .ReturnsAsync((IEnumerable msgs, CancellationToken _) => msgs);
+ ChatReducerCompactionStrategy strategy = new(
+ reducerMock.Object,
+ metrics =>
+ {
+ capturedMetrics = metrics;
+ return false;
+ });
+
+ // Act & Assert
+ await RunCompactionStrategySkippedAsync(strategy, messages);
+
+ // Assert
+ Assert.NotNull(capturedMetrics);
+ Assert.Equal(2, capturedMetrics!.MessageCount);
+ }
+
+ [Fact]
+ public async Task ReducerNoChange_AppliedFalseAsync()
+ {
+ // Arrange
+ List messages = [new(ChatRole.User, "Hello")];
+ Mock reducerMock = new();
+ reducerMock
+ .Setup(r => r.ReduceAsync(It.IsAny>(), It.IsAny()))
+ .ReturnsAsync((IEnumerable msgs, CancellationToken _) => msgs);
+ ChatReducerCompactionStrategy strategy = new(reducerMock.Object, _ => true);
+
+ // Act & Assert
+ await RunCompactionStrategySkippedAsync(strategy, messages);
+ }
+
+ [Fact]
+ public void Name_ReturnsReducerTypeName()
+ {
+ // Arrange
+ Mock reducerMock = new();
+
+ // Act
+ ChatReducerCompactionStrategy strategy = new(reducerMock.Object, _ => true);
+
+ // Assert
+ Assert.Equal(reducerMock.Object.GetType().Name, strategy.Name);
+ }
+}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/CompactionMetricTests.cs b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/CompactionMetricTests.cs
index c2287512cd..ace35e3607 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/CompactionMetricTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/CompactionMetricTests.cs
@@ -9,7 +9,10 @@ public class CompactionMetricTests
[Fact]
public void DefaultValues_AreZero()
{
- CompactionMetric metrics = new();
+ // Arrange & Act
+ ChatHistoryMetric metrics = new();
+
+ // Assert
Assert.Equal(0, metrics.TokenCount);
Assert.Equal(0L, metrics.ByteCount);
Assert.Equal(0, metrics.MessageCount);
@@ -21,7 +24,8 @@ public class CompactionMetricTests
[Fact]
public void InitProperties_SetCorrectly()
{
- CompactionMetric metrics = new()
+ // Arrange & Act
+ ChatHistoryMetric metrics = new()
{
TokenCount = 100,
ByteCount = 500,
@@ -30,6 +34,7 @@ public class CompactionMetricTests
UserTurnCount = 3
};
+ // Assert
Assert.Equal(100, metrics.TokenCount);
Assert.Equal(500L, metrics.ByteCount);
Assert.Equal(5, metrics.MessageCount);
diff --git a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/CompactionPipelineResultTests.cs b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/CompactionPipelineResultTests.cs
index 6921bc26ca..7edc8426f5 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/CompactionPipelineResultTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/CompactionPipelineResultTests.cs
@@ -11,13 +11,16 @@ public class CompactionPipelineResultTests
[Fact]
public void Properties_AreReadable()
{
- CompactionMetric before = new() { MessageCount = 10 };
- CompactionMetric after = new() { MessageCount = 5 };
+ // Arrange
+ ChatHistoryMetric before = new() { MessageCount = 10 };
+ ChatHistoryMetric after = new() { MessageCount = 5 };
CompactionResult strategyResult = new("Test", applied: true, before, after);
List results = [strategyResult];
+ // Act
CompactionPipelineResult pipelineResult = new(before, after, results);
+ // Assert
Assert.Same(before, pipelineResult.Before);
Assert.Same(after, pipelineResult.After);
Assert.Single(pipelineResult.StrategyResults);
@@ -26,21 +29,29 @@ public class CompactionPipelineResultTests
[Fact]
public void AnyApplied_AllFalse_ReturnsFalse()
{
- CompactionMetric metrics = new() { MessageCount = 5 };
+ // Arrange
+ ChatHistoryMetric metrics = new() { MessageCount = 5 };
CompactionResult skipped = CompactionResult.Skipped("Skip", metrics);
+
+ // Act
CompactionPipelineResult result = new(metrics, metrics, [skipped]);
+ // Assert
Assert.False(result.AnyApplied);
}
[Fact]
public void AnyApplied_SomeTrue_ReturnsTrue()
{
- CompactionMetric before = new() { MessageCount = 10 };
- CompactionMetric after = new() { MessageCount = 5 };
+ // Arrange
+ ChatHistoryMetric before = new() { MessageCount = 10 };
+ ChatHistoryMetric after = new() { MessageCount = 5 };
CompactionResult applied = new("Applied", applied: true, before, after);
+
+ // Act
CompactionPipelineResult result = new(before, after, [applied]);
+ // Assert
Assert.True(result.AnyApplied);
}
}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/CompactionResultTests.cs b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/CompactionResultTests.cs
index 1bb476786b..bc36898a4d 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/CompactionResultTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/CompactionResultTests.cs
@@ -9,9 +9,13 @@ public class CompactionResultTests
[Fact]
public void Skipped_HasSameBeforeAndAfter()
{
- CompactionMetric metrics = new() { MessageCount = 5, TokenCount = 100 };
+ // Arrange
+ ChatHistoryMetric metrics = new() { MessageCount = 5, TokenCount = 100 };
+
+ // Act
CompactionResult result = CompactionResult.Skipped("Test", metrics);
+ // Assert
Assert.Equal("Test", result.StrategyName);
Assert.False(result.Applied);
Assert.Same(metrics, result.Before);
diff --git a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/CompactionStrategyTestBase.cs b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/CompactionStrategyTestBase.cs
new file mode 100644
index 0000000000..851713cc83
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/CompactionStrategyTestBase.cs
@@ -0,0 +1,40 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Microsoft.Agents.AI.Compaction;
+using Microsoft.Extensions.AI;
+
+namespace Microsoft.Agents.AI.Abstractions.UnitTests.Compaction;
+
+public abstract class CompactionStrategyTestBase
+{
+ public static async ValueTask RunCompactionStrategyReducedAsync(ChatHistoryCompactionStrategy strategy, List messages, int expectedCount)
+ {
+ // Act
+ ChatHistoryCompactionStrategy.s_currentMetrics.Value = DefaultChatHistoryMetricsCalculator.Instance.Calculate(messages);
+ CompactionResult result = await strategy.CompactAsync(messages, DefaultChatHistoryMetricsCalculator.Instance);
+
+ // Assert
+ Assert.True(result.Applied);
+ Assert.NotEqual(result.Before, result.After);
+ Assert.Equal(expectedCount, messages.Count);
+
+ return result;
+ }
+
+ public static async ValueTask RunCompactionStrategySkippedAsync(ChatHistoryCompactionStrategy strategy, List messages)
+ {
+ // Act
+ int initialCount = messages.Count;
+ ChatHistoryCompactionStrategy.s_currentMetrics.Value = DefaultChatHistoryMetricsCalculator.Instance.Calculate(messages);
+ CompactionResult result = await strategy.CompactAsync(messages, DefaultChatHistoryMetricsCalculator.Instance);
+
+ // Assert
+ Assert.False(result.Applied);
+ Assert.Equal(result.Before, result.After);
+ Assert.Equal(initialCount, messages.Count);
+
+ return result;
+ }
+}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/DefaultChatHistoryMetricsCalculatorTests.cs b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/DefaultChatHistoryMetricsCalculatorTests.cs
index 01387f1eed..2edbe6a4ff 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/DefaultChatHistoryMetricsCalculatorTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/DefaultChatHistoryMetricsCalculatorTests.cs
@@ -11,9 +11,13 @@ public class DefaultChatHistoryMetricsCalculatorTests
[Fact]
public void EmptyList_ReturnsZeroMetrics()
{
+ // Arrange
DefaultChatHistoryMetricsCalculator calculator = new();
- CompactionMetric metrics = calculator.Calculate([]);
+ // Act
+ ChatHistoryMetric metrics = calculator.Calculate([]);
+
+ // Assert
Assert.Equal(0, metrics.TokenCount);
Assert.Equal(0L, metrics.ByteCount);
Assert.Equal(0, metrics.MessageCount);
@@ -24,6 +28,7 @@ public class DefaultChatHistoryMetricsCalculatorTests
[Fact]
public void CountsMessages()
{
+ // Arrange
DefaultChatHistoryMetricsCalculator calculator = new();
List messages =
[
@@ -31,14 +36,17 @@ public class DefaultChatHistoryMetricsCalculatorTests
new(ChatRole.Assistant, "Hi there"),
];
- CompactionMetric metrics = calculator.Calculate(messages);
+ // Act
+ ChatHistoryMetric metrics = calculator.Calculate(messages);
+ // Assert
Assert.Equal(2, metrics.MessageCount);
}
[Fact]
public void CountsUserTurns()
{
+ // Arrange
DefaultChatHistoryMetricsCalculator calculator = new();
List messages =
[
@@ -48,14 +56,17 @@ public class DefaultChatHistoryMetricsCalculatorTests
new(ChatRole.Assistant, "Good"),
];
- CompactionMetric metrics = calculator.Calculate(messages);
+ // Act
+ ChatHistoryMetric metrics = calculator.Calculate(messages);
+ // Assert
Assert.Equal(2, metrics.UserTurnCount);
}
[Fact]
public void CountsToolCalls()
{
+ // Arrange
DefaultChatHistoryMetricsCalculator calculator = new();
ChatMessage assistantMsg = new(ChatRole.Assistant, [
new FunctionCallContent("call1", "get_weather", new Dictionary { ["city"] = "NYC" }),
@@ -67,14 +78,17 @@ public class DefaultChatHistoryMetricsCalculatorTests
assistantMsg,
];
- CompactionMetric metrics = calculator.Calculate(messages);
+ // Act
+ ChatHistoryMetric metrics = calculator.Calculate(messages);
+ // Assert
Assert.Equal(2, metrics.ToolCallCount);
}
[Fact]
public void ConsecutiveUserMessages_CountAsOneTurn()
{
+ // Arrange
DefaultChatHistoryMetricsCalculator calculator = new();
List messages =
[
@@ -83,22 +97,27 @@ public class DefaultChatHistoryMetricsCalculatorTests
new(ChatRole.Assistant, "Reply"),
];
- CompactionMetric metrics = calculator.Calculate(messages);
+ // Act
+ ChatHistoryMetric metrics = calculator.Calculate(messages);
+ // Assert
Assert.Equal(1, metrics.UserTurnCount);
}
[Fact]
public void TokenCount_IsPositive()
{
+ // Arrange
DefaultChatHistoryMetricsCalculator calculator = new();
List messages =
[
new(ChatRole.User, "Hello world"),
];
- CompactionMetric metrics = calculator.Calculate(messages);
+ // Act
+ ChatHistoryMetric metrics = calculator.Calculate(messages);
+ // Assert
Assert.True(metrics.TokenCount > 0);
Assert.True(metrics.ByteCount > 0);
}
@@ -106,9 +125,13 @@ public class DefaultChatHistoryMetricsCalculatorTests
[Fact]
public void NullInput_ReturnsZeroMetrics()
{
+ // Arrange
DefaultChatHistoryMetricsCalculator calculator = new();
- CompactionMetric metrics = calculator.Calculate(null!);
+ // Act
+ ChatHistoryMetric metrics = calculator.Calculate(null!);
+
+ // Assert
Assert.Equal(0, metrics.TokenCount);
Assert.Equal(0L, metrics.ByteCount);
Assert.Equal(0, metrics.MessageCount);
@@ -118,45 +141,50 @@ public class DefaultChatHistoryMetricsCalculatorTests
[Fact]
public void InvalidCharsPerToken_UsesDefault()
{
- // A non-positive charsPerToken should fall back to the default (4)
+ // Arrange
DefaultChatHistoryMetricsCalculator calculator = new(charsPerToken: 0);
List messages =
[
new(ChatRole.User, "Hello world"),
];
- CompactionMetric metrics = calculator.Calculate(messages);
+ // Act
+ ChatHistoryMetric metrics = calculator.Calculate(messages);
- // With default 4 chars/token: "Hello world" = 11 chars → 11/4=2 + 4 overhead = 6 tokens
+ // Assert
Assert.True(metrics.TokenCount > 0);
}
[Fact]
public void NullMessageText_HandledGracefully()
{
+ // Arrange
DefaultChatHistoryMetricsCalculator calculator = new();
- // Message with no text content — Text returns null
ChatMessage msg = new() { Role = ChatRole.User };
List messages = [msg];
- CompactionMetric metrics = calculator.Calculate(messages);
+ // Act
+ ChatHistoryMetric metrics = calculator.Calculate(messages);
+ // Assert
Assert.Equal(1, metrics.MessageCount);
- // Null text → empty string → 0 bytes, only overhead tokens
- Assert.True(metrics.TokenCount > 0); // per-message overhead
+ Assert.True(metrics.TokenCount > 0);
Assert.Equal(0L, metrics.ByteCount);
}
[Fact]
public void NullContents_SkipsToolCounting()
{
+ // Arrange
DefaultChatHistoryMetricsCalculator calculator = new();
ChatMessage msg = new(ChatRole.User, "text");
msg.Contents = null!;
List messages = [msg];
- CompactionMetric metrics = calculator.Calculate(messages);
+ // Act
+ ChatHistoryMetric metrics = calculator.Calculate(messages);
+ // Assert
Assert.Equal(1, metrics.MessageCount);
Assert.Equal(0, metrics.ToolCallCount);
}
@@ -164,16 +192,18 @@ public class DefaultChatHistoryMetricsCalculatorTests
[Fact]
public void MessageWithOnlyNonTextContent_NullTextHandled()
{
+ // Arrange
DefaultChatHistoryMetricsCalculator calculator = new();
- // FunctionCallContent-only message has null Text
ChatMessage msg = new(ChatRole.Assistant,
[
new FunctionCallContent("c1", "func"),
]);
List messages = [msg];
- CompactionMetric metrics = calculator.Calculate(messages);
+ // Act
+ ChatHistoryMetric metrics = calculator.Calculate(messages);
+ // Assert
Assert.Equal(1, metrics.MessageCount);
Assert.Equal(1, metrics.ToolCallCount);
}
@@ -181,6 +211,7 @@ public class DefaultChatHistoryMetricsCalculatorTests
[Fact]
public void Calculate_PopulatesGroupIndex()
{
+ // Arrange
DefaultChatHistoryMetricsCalculator calculator = new();
List messages =
[
@@ -189,8 +220,10 @@ public class DefaultChatHistoryMetricsCalculatorTests
new(ChatRole.Assistant, "Hi there"),
];
- CompactionMetric metrics = calculator.Calculate(messages);
+ // Act
+ ChatHistoryMetric metrics = calculator.Calculate(messages);
+ // Assert
Assert.Equal(3, metrics.Groups.Count);
Assert.Equal(ChatMessageGroupKind.System, metrics.Groups[0].Kind);
Assert.Equal(ChatMessageGroupKind.UserTurn, metrics.Groups[1].Kind);
@@ -200,23 +233,30 @@ public class DefaultChatHistoryMetricsCalculatorTests
[Fact]
public void EmptyList_GroupIndexIsEmpty()
{
+ // Arrange
DefaultChatHistoryMetricsCalculator calculator = new();
- CompactionMetric metrics = calculator.Calculate([]);
+ // Act
+ ChatHistoryMetric metrics = calculator.Calculate([]);
+
+ // Assert
Assert.Empty(metrics.Groups);
}
[Fact]
public void GroupIndex_SystemMessage_IdentifiedCorrectly()
{
+ // Arrange
DefaultChatHistoryMetricsCalculator calculator = new();
List messages =
[
new(ChatRole.System, "You are a helpful assistant"),
];
+ // Act
IReadOnlyList groups = calculator.Calculate(messages).Groups;
+ // Assert
Assert.Single(groups);
Assert.Equal(ChatMessageGroupKind.System, groups[0].Kind);
Assert.Equal(0, groups[0].StartIndex);
@@ -226,6 +266,7 @@ public class DefaultChatHistoryMetricsCalculatorTests
[Fact]
public void GroupIndex_AssistantWithToolCalls_GroupedWithResults()
{
+ // Arrange
DefaultChatHistoryMetricsCalculator calculator = new();
ChatMessage assistantMsg = new(ChatRole.Assistant, [
new FunctionCallContent("call1", "get_weather", new Dictionary { ["city"] = "NYC" }),
@@ -240,18 +281,21 @@ public class DefaultChatHistoryMetricsCalculatorTests
toolResult,
];
+ // Act
IReadOnlyList groups = calculator.Calculate(messages).Groups;
+ // Assert
Assert.Equal(2, groups.Count);
Assert.Equal(ChatMessageGroupKind.UserTurn, groups[0].Kind);
Assert.Equal(ChatMessageGroupKind.AssistantToolGroup, groups[1].Kind);
Assert.Equal(1, groups[1].StartIndex);
- Assert.Equal(2, groups[1].Count); // assistant + tool result
+ Assert.Equal(2, groups[1].Count);
}
[Fact]
public void GroupIndex_MultipleToolResults_GroupedTogether()
{
+ // Arrange
DefaultChatHistoryMetricsCalculator calculator = new();
ChatMessage assistantMsg = new(ChatRole.Assistant, [
new FunctionCallContent("c1", "func1"),
@@ -261,8 +305,10 @@ public class DefaultChatHistoryMetricsCalculatorTests
ChatMessage tool2 = new(ChatRole.Tool, [new FunctionResultContent("c2", "result2")]);
List messages = [assistantMsg, tool1, tool2];
+ // Act
IReadOnlyList groups = calculator.Calculate(messages).Groups;
+ // Assert
Assert.Single(groups);
Assert.Equal(ChatMessageGroupKind.AssistantToolGroup, groups[0].Kind);
Assert.Equal(3, groups[0].Count);
@@ -271,6 +317,7 @@ public class DefaultChatHistoryMetricsCalculatorTests
[Fact]
public void GroupIndex_ComplexConversation_CorrectGrouping()
{
+ // Arrange
DefaultChatHistoryMetricsCalculator calculator = new();
List messages =
[
@@ -283,29 +330,34 @@ public class DefaultChatHistoryMetricsCalculatorTests
new(ChatRole.Assistant, "It's sunny!"),
];
+ // Act
IReadOnlyList groups = calculator.Calculate(messages).Groups;
+ // Assert
Assert.Equal(6, groups.Count);
Assert.Equal(ChatMessageGroupKind.System, groups[0].Kind);
Assert.Equal(ChatMessageGroupKind.UserTurn, groups[1].Kind);
Assert.Equal(ChatMessageGroupKind.AssistantPlain, groups[2].Kind);
Assert.Equal(ChatMessageGroupKind.UserTurn, groups[3].Kind);
Assert.Equal(ChatMessageGroupKind.AssistantToolGroup, groups[4].Kind);
- Assert.Equal(2, groups[4].Count); // assistant + tool
+ Assert.Equal(2, groups[4].Count);
Assert.Equal(ChatMessageGroupKind.AssistantPlain, groups[5].Kind);
}
[Fact]
public void GroupIndex_OrphanToolResult_IdentifiedCorrectly()
{
+ // Arrange
DefaultChatHistoryMetricsCalculator calculator = new();
List messages =
[
new ChatMessage(ChatRole.Tool, [new FunctionResultContent("c1", "orphan result")]),
];
+ // Act
IReadOnlyList groups = calculator.Calculate(messages).Groups;
+ // Assert
Assert.Single(groups);
Assert.Equal(ChatMessageGroupKind.ToolResult, groups[0].Kind);
}
@@ -313,14 +365,17 @@ public class DefaultChatHistoryMetricsCalculatorTests
[Fact]
public void GroupIndex_UnknownRole_IdentifiedAsOther()
{
+ // Arrange
DefaultChatHistoryMetricsCalculator calculator = new();
List messages =
[
new(new ChatRole("custom"), "custom message"),
];
+ // Act
IReadOnlyList groups = calculator.Calculate(messages).Groups;
+ // Assert
Assert.Single(groups);
Assert.Equal(ChatMessageGroupKind.Other, groups[0].Kind);
}
@@ -328,13 +383,16 @@ public class DefaultChatHistoryMetricsCalculatorTests
[Fact]
public void GroupIndex_AssistantWithNullContents_ClassifiedAsPlain()
{
+ // Arrange
DefaultChatHistoryMetricsCalculator calculator = new();
ChatMessage msg = new(ChatRole.Assistant, "reply");
msg.Contents = null!;
List messages = [msg];
+ // Act
IReadOnlyList groups = calculator.Calculate(messages).Groups;
+ // Assert
Assert.Single(groups);
Assert.Equal(ChatMessageGroupKind.AssistantPlain, groups[0].Kind);
}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/Internal/NeverCompactStrategy.cs b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/Internal/NeverCompactStrategy.cs
index 9a4429e08a..f126ed5db2 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/Internal/NeverCompactStrategy.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/Internal/NeverCompactStrategy.cs
@@ -16,7 +16,8 @@ internal sealed class NeverCompactStrategy : ChatHistoryCompactionStrategy
}
public override string Name => "NeverCompact";
- public override bool ShouldCompact(CompactionMetric metrics) => false;
+
+ protected override bool ShouldCompact(ChatHistoryMetric metrics) => false;
private sealed class NoOpReducer : IChatReducer
{
diff --git a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/Internal/NonReadOnlyList.cs b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/Internal/NonReadOnlyList.cs
deleted file mode 100644
index 101388c921..0000000000
--- a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/Internal/NonReadOnlyList.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System.Collections;
-using System.Collections.Generic;
-using System.Linq;
-
-namespace Microsoft.Agents.AI.Abstractions.UnitTests.Compaction.Internal;
-
-///
-/// An IList<T> that does NOT implement IReadOnlyList<T>,
-/// used to test the defensive as IReadOnlyList<T> ?? fallback patterns.
-///
-internal sealed class NonReadOnlyList : IList
-{
- private readonly List _inner;
-
- public NonReadOnlyList(IEnumerable items)
- {
- this._inner = items.ToList();
- }
-
- public T this[int index]
- {
- get => this._inner[index];
- set => this._inner[index] = value;
- }
-
- public int Count => this._inner.Count;
- public bool IsReadOnly => false;
- public void Add(T item) => this._inner.Add(item);
- public void Clear() => this._inner.Clear();
- public bool Contains(T item) => this._inner.Contains(item);
- public void CopyTo(T[] array, int arrayIndex) => this._inner.CopyTo(array, arrayIndex);
- public IEnumerator GetEnumerator() => this._inner.GetEnumerator();
- public int IndexOf(T item) => this._inner.IndexOf(item);
- public void Insert(int index, T item) => this._inner.Insert(index, item);
- public bool Remove(T item) => this._inner.Remove(item);
- public void RemoveAt(int index) => this._inner.RemoveAt(index);
- IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
-}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/Internal/RemoveFirstMessageStrategy.cs b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/Internal/RemoveFirstMessageStrategy.cs
index f2ca36bc10..477a7815a0 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/Internal/RemoveFirstMessageStrategy.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/Internal/RemoveFirstMessageStrategy.cs
@@ -18,7 +18,7 @@ internal sealed class RemoveFirstMessageStrategy : ChatHistoryCompactionStrategy
public override string Name => "RemoveFirst";
- public override bool ShouldCompact(CompactionMetric metrics) => metrics.MessageCount > 0;
+ protected override bool ShouldCompact(ChatHistoryMetric metrics) => metrics.MessageCount > 0;
private sealed class RemoveFirstReducer : IChatReducer
{
diff --git a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/MessageGroupTests.cs b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/MessageGroupTests.cs
index 5449b4bbf9..c84ebbb1ac 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/MessageGroupTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/MessageGroupTests.cs
@@ -9,10 +9,12 @@ public class MessageGroupTests
[Fact]
public void Equality_Works()
{
+ // Arrange
ChatMessageGroup a = new(0, 2, ChatMessageGroupKind.AssistantToolGroup);
ChatMessageGroup b = new(0, 2, ChatMessageGroupKind.AssistantToolGroup);
ChatMessageGroup c = new(1, 2, ChatMessageGroupKind.AssistantToolGroup);
+ // Act & Assert
Assert.Equal(a, b);
Assert.True(a == b);
Assert.NotEqual(a, c);
@@ -22,34 +24,42 @@ public class MessageGroupTests
[Fact]
public void Equals_Object_NullReturnsFalse()
{
+ // Arrange
ChatMessageGroup group = new(0, 1, ChatMessageGroupKind.System);
+ // Act & Assert
Assert.False(group.Equals(null));
}
[Fact]
public void Equals_Object_BoxedMessageGroupReturnsTrue()
{
+ // Arrange
ChatMessageGroup group = new(0, 2, ChatMessageGroupKind.AssistantToolGroup);
object boxed = new ChatMessageGroup(0, 2, ChatMessageGroupKind.AssistantToolGroup);
+ // Act & Assert
Assert.True(group.Equals(boxed));
}
[Fact]
public void Equals_Object_WrongTypeReturnsFalse()
{
+ // Arrange
ChatMessageGroup group = new(0, 1, ChatMessageGroupKind.System);
+ // Act & Assert
Assert.False(group.Equals("not a MessageGroup"));
}
[Fact]
public void GetHashCode_ConsistentForEqualInstances()
{
+ // Arrange
ChatMessageGroup a = new(0, 2, ChatMessageGroupKind.AssistantToolGroup);
ChatMessageGroup b = new(0, 2, ChatMessageGroupKind.AssistantToolGroup);
+ // Act & Assert
Assert.Equal(a.GetHashCode(), b.GetHashCode());
}
}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/SlidingWindowCompactionStrategyTests.cs b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/SlidingWindowCompactionStrategyTests.cs
index dee020130b..b3e75920ba 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/SlidingWindowCompactionStrategyTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Abstractions.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;
@@ -7,47 +7,27 @@ using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.Abstractions.UnitTests.Compaction;
-public class SlidingWindowCompactionStrategyTests
+public class SlidingWindowCompactionStrategyTests : CompactionStrategyTestBase
{
- [Fact]
- public void ShouldCompact_UnderLimit_ReturnsFalse()
- {
- SlidingWindowCompactionStrategy strategy = new(maxTurns: 10);
- CompactionMetric metrics = new() { UserTurnCount = 3 };
-
- Assert.False(strategy.ShouldCompact(metrics));
- }
-
- [Fact]
- public void ShouldCompact_OverLimit_ReturnsTrue()
- {
- SlidingWindowCompactionStrategy strategy = new(maxTurns: 2);
- CompactionMetric metrics = new() { UserTurnCount = 5 };
-
- Assert.True(strategy.ShouldCompact(metrics));
- }
-
[Fact]
public async Task UnderLimit_NoChangeAsync()
{
- SlidingWindowCompactionStrategy strategy = new(maxTurns: 10);
+ // Arrange
List messages =
[
new(ChatRole.User, "Hello"),
new(ChatRole.Assistant, "Hi"),
];
- DefaultChatHistoryMetricsCalculator calculator = new();
+ SlidingWindowCompactionStrategy strategy = new(maxTurns: 10);
- CompactionResult result = await strategy.CompactAsync(messages, calculator);
-
- Assert.False(result.Applied);
- Assert.Equal(2, messages.Count);
+ // Act & Assert
+ await RunCompactionStrategySkippedAsync(strategy, messages);
}
[Fact]
public async Task KeepsLastNTurnsAsync()
{
- SlidingWindowCompactionStrategy strategy = new(maxTurns: 2);
+ // Arrange
List messages =
[
new(ChatRole.User, "Turn 1"),
@@ -57,12 +37,12 @@ public class SlidingWindowCompactionStrategyTests
new(ChatRole.User, "Turn 3"),
new(ChatRole.Assistant, "Reply 3"),
];
- DefaultChatHistoryMetricsCalculator calculator = new();
+ SlidingWindowCompactionStrategy strategy = new(maxTurns: 2);
- CompactionResult result = await strategy.CompactAsync(messages, calculator);
+ // Act & Assert
+ await RunCompactionStrategyReducedAsync(strategy, messages, expectedCount: 4);
- Assert.True(result.Applied);
- Assert.Equal(4, messages.Count);
+ // Assert
Assert.Equal("Turn 2", messages[0].Text);
Assert.Equal("Reply 2", messages[1].Text);
Assert.Equal("Turn 3", messages[2].Text);
@@ -72,7 +52,7 @@ public class SlidingWindowCompactionStrategyTests
[Fact]
public async Task PreservesSystemMessagesAsync()
{
- SlidingWindowCompactionStrategy strategy = new(maxTurns: 1);
+ // Arrange
List messages =
[
new(ChatRole.System, "You are a helper"),
@@ -81,12 +61,12 @@ public class SlidingWindowCompactionStrategyTests
new(ChatRole.User, "Turn 2"),
new(ChatRole.Assistant, "Reply 2"),
];
- DefaultChatHistoryMetricsCalculator calculator = new();
+ SlidingWindowCompactionStrategy strategy = new(maxTurns: 1);
- CompactionResult result = await strategy.CompactAsync(messages, calculator);
+ // Act & Assert
+ await RunCompactionStrategyReducedAsync(strategy, messages, expectedCount: 3);
- Assert.True(result.Applied);
- Assert.Equal(3, messages.Count);
+ // Assert
Assert.Equal(ChatRole.System, messages[0].Role);
Assert.Equal("You are a helper", messages[0].Text);
Assert.Equal("Turn 2", messages[1].Text);
@@ -96,7 +76,7 @@ public class SlidingWindowCompactionStrategyTests
[Fact]
public async Task PreservesToolGroupsWithinKeptTurnsAsync()
{
- SlidingWindowCompactionStrategy strategy = new(maxTurns: 1);
+ // Arrange
List messages =
[
new(ChatRole.User, "Turn 1"),
@@ -106,37 +86,34 @@ public class SlidingWindowCompactionStrategyTests
new ChatMessage(ChatRole.Tool, [new FunctionResultContent("c1", "Sunny")]),
new(ChatRole.Assistant, "It's sunny!"),
];
- DefaultChatHistoryMetricsCalculator calculator = new();
+ SlidingWindowCompactionStrategy strategy = new(maxTurns: 1);
- CompactionResult result = await strategy.CompactAsync(messages, calculator);
+ // Act & Assert
+ await RunCompactionStrategyReducedAsync(strategy, messages, expectedCount: 4);
- Assert.True(result.Applied);
- // Turn 1 dropped, Turn 2 kept (user + assistant-tool-group + plain assistant)
- Assert.Equal(4, messages.Count);
+ // Assert
Assert.Equal("Get weather", messages[0].Text);
}
[Fact]
public async Task SingleTurn_AtLimit_NoChangeAsync()
{
- SlidingWindowCompactionStrategy strategy = new(maxTurns: 1);
+ // Arrange
List messages =
[
new(ChatRole.User, "Hello"),
new(ChatRole.Assistant, "Hi"),
];
- DefaultChatHistoryMetricsCalculator calculator = new();
+ SlidingWindowCompactionStrategy strategy = new(maxTurns: 1);
- CompactionResult result = await strategy.CompactAsync(messages, calculator);
-
- Assert.False(result.Applied);
- Assert.Equal(2, messages.Count);
+ // Act & Assert
+ await RunCompactionStrategySkippedAsync(strategy, messages);
}
[Fact]
public async Task DropsResponseGroupsFromOldTurnsAsync()
{
- SlidingWindowCompactionStrategy strategy = new(maxTurns: 1);
+ // Arrange
List messages =
[
new(ChatRole.User, "Turn 1"),
@@ -146,12 +123,12 @@ public class SlidingWindowCompactionStrategyTests
new(ChatRole.User, "Turn 2"),
new(ChatRole.Assistant, "Reply 2"),
];
- DefaultChatHistoryMetricsCalculator calculator = new();
+ SlidingWindowCompactionStrategy strategy = new(maxTurns: 1);
- CompactionResult result = await strategy.CompactAsync(messages, calculator);
+ // Act & Assert
+ await RunCompactionStrategyReducedAsync(strategy, messages, expectedCount: 2);
- Assert.True(result.Applied);
- Assert.Equal(2, messages.Count);
+ // Assert
Assert.Equal("Turn 2", messages[0].Text);
Assert.Equal("Reply 2", messages[1].Text);
}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/SummarizationCompactionStrategyTests.cs b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/SummarizationCompactionStrategyTests.cs
index 02782ad754..018d35ce48 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/SummarizationCompactionStrategyTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/SummarizationCompactionStrategyTests.cs
@@ -9,44 +9,24 @@ using Moq;
namespace Microsoft.Agents.AI.Abstractions.UnitTests.Compaction;
-public class SummarizationCompactionStrategyTests
+public class SummarizationCompactionStrategyTests : CompactionStrategyTestBase
{
- [Fact]
- public void ShouldCompact_UnderLimit_ReturnsFalse()
- {
- Mock chatClientMock = new();
- SummarizationCompactionStrategy strategy = new(chatClientMock.Object, maxTokens: 100000);
- CompactionMetric metrics = new() { TokenCount = 500 };
-
- Assert.False(strategy.ShouldCompact(metrics));
- }
-
- [Fact]
- public void ShouldCompact_OverLimit_ReturnsTrue()
- {
- Mock chatClientMock = new();
- SummarizationCompactionStrategy strategy = new(chatClientMock.Object, maxTokens: 100);
- CompactionMetric metrics = new() { TokenCount = 500 };
-
- Assert.True(strategy.ShouldCompact(metrics));
- }
-
[Fact]
public async Task UnderLimit_NoChangeAsync()
{
- Mock chatClientMock = new();
- SummarizationCompactionStrategy strategy = new(chatClientMock.Object, maxTokens: 100000);
+ // Arrange
List messages =
[
new(ChatRole.User, "Hello"),
new(ChatRole.Assistant, "Hi"),
];
- DefaultChatHistoryMetricsCalculator calculator = new();
+ Mock chatClientMock = new();
+ SummarizationCompactionStrategy strategy = new(chatClientMock.Object, maxTokens: 100000);
- CompactionResult result = await strategy.CompactAsync(messages, calculator);
+ // Act & Assert
+ await RunCompactionStrategySkippedAsync(strategy, messages);
- Assert.False(result.Applied);
- Assert.Equal(2, messages.Count);
+ // Assert
chatClientMock.Verify(
c => c.GetResponseAsync(It.IsAny>(), It.IsAny(), It.IsAny()),
Times.Never);
@@ -55,13 +35,7 @@ public class SummarizationCompactionStrategyTests
[Fact]
public async Task SummarizesOldGroupsAsync()
{
- Mock chatClientMock = new();
- chatClientMock
- .Setup(c => c.GetResponseAsync(It.IsAny>(), It.IsAny(), It.IsAny()))
- .ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, "User asked about weather. It was sunny.")));
-
- // preserveRecentGroups=2 means keep last 2 non-system groups
- SummarizationCompactionStrategy strategy = new(chatClientMock.Object, maxTokens: 1, preserveRecentGroups: 2);
+ // Arrange
List messages =
[
new(ChatRole.User, "What's the weather?"),
@@ -71,14 +45,16 @@ public class SummarizationCompactionStrategyTests
new(ChatRole.User, "Thanks!"),
new(ChatRole.Assistant, "You're welcome!"),
];
- DefaultChatHistoryMetricsCalculator calculator = new();
+ Mock chatClientMock = new();
+ chatClientMock
+ .Setup(c => c.GetResponseAsync(It.IsAny>(), It.IsAny(), It.IsAny()))
+ .ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, "User asked about weather. It was sunny.")));
+ SummarizationCompactionStrategy strategy = new(chatClientMock.Object, maxTokens: 1, preserveRecentGroups: 2);
- CompactionResult result = await strategy.CompactAsync(messages, calculator);
+ // Act & Assert
+ await RunCompactionStrategyReducedAsync(strategy, messages, expectedCount: 3);
- Assert.True(result.Applied);
- // 6 groups (3 user + 3 assistant), protect last 2 → summarize first 4 groups
- // Result: summary + 2 protected groups = 3 messages
- Assert.Equal(3, messages.Count);
+ // Assert
Assert.Contains("[Summary]", messages[0].Text);
Assert.Contains("sunny", messages[0].Text);
Assert.Equal("Thanks!", messages[1].Text);
@@ -88,12 +64,7 @@ public class SummarizationCompactionStrategyTests
[Fact]
public async Task PreservesSystemMessagesAsync()
{
- Mock chatClientMock = new();
- chatClientMock
- .Setup(c => c.GetResponseAsync(It.IsAny>(), It.IsAny(), It.IsAny()))
- .ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, "Summary of earlier discussion.")));
-
- SummarizationCompactionStrategy strategy = new(chatClientMock.Object, maxTokens: 1, preserveRecentGroups: 1);
+ // Arrange
List messages =
[
new(ChatRole.System, "You are a helper"),
@@ -102,11 +73,16 @@ public class SummarizationCompactionStrategyTests
new(ChatRole.User, "Turn 2"),
new(ChatRole.Assistant, "Reply 2"),
];
- DefaultChatHistoryMetricsCalculator calculator = new();
+ Mock chatClientMock = new();
+ chatClientMock
+ .Setup(c => c.GetResponseAsync(It.IsAny>(), It.IsAny(), It.IsAny()))
+ .ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, "Summary of earlier discussion.")));
+ SummarizationCompactionStrategy strategy = new(chatClientMock.Object, maxTokens: 1, preserveRecentGroups: 1);
- CompactionResult result = await strategy.CompactAsync(messages, calculator);
+ // Act & Assert
+ await RunCompactionStrategyReducedAsync(strategy, messages, expectedCount: 3);
- Assert.True(result.Applied);
+ // Assert
Assert.Equal(ChatRole.System, messages[0].Role);
Assert.Equal("You are a helper", messages[0].Text);
Assert.Contains("[Summary]", messages[1].Text);
@@ -115,20 +91,19 @@ public class SummarizationCompactionStrategyTests
[Fact]
public async Task AllGroupsProtected_NoChangeAsync()
{
- Mock chatClientMock = new();
- SummarizationCompactionStrategy strategy = new(chatClientMock.Object, maxTokens: 1, preserveRecentGroups: 10);
+ // Arrange
List messages =
[
new(ChatRole.User, "Hello"),
new(ChatRole.Assistant, "Hi"),
];
- DefaultChatHistoryMetricsCalculator calculator = new();
+ Mock chatClientMock = new();
+ SummarizationCompactionStrategy strategy = new(chatClientMock.Object, maxTokens: 1, preserveRecentGroups: 10);
- CompactionResult result = await strategy.CompactAsync(messages, calculator);
+ // Act & Assert
+ await RunCompactionStrategySkippedAsync(strategy, messages);
- // All groups protected → nothing to summarize → no change
- Assert.False(result.Applied);
- Assert.Equal(2, messages.Count);
+ // Assert
chatClientMock.Verify(
c => c.GetResponseAsync(It.IsAny>(), It.IsAny(), It.IsAny()),
Times.Never);
@@ -137,16 +112,8 @@ public class SummarizationCompactionStrategyTests
[Fact]
public async Task CustomPrompt_UsedInRequestAsync()
{
+ // Arrange
const string CustomPrompt = "Summarize briefly.";
- List? capturedMessages = null;
-
- Mock chatClientMock = new();
- chatClientMock
- .Setup(c => c.GetResponseAsync(It.IsAny>(), It.IsAny(), It.IsAny()))
- .Callback, ChatOptions, CancellationToken>((msgs, _, _) => capturedMessages = [.. msgs])
- .ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, "Brief summary.")));
-
- SummarizationCompactionStrategy strategy = new(chatClientMock.Object, maxTokens: 1, preserveRecentGroups: 1, summarizationPrompt: CustomPrompt);
List messages =
[
new(ChatRole.User, "First"),
@@ -154,12 +121,19 @@ public class SummarizationCompactionStrategyTests
new(ChatRole.User, "Second"),
new(ChatRole.Assistant, "Reply 2"),
];
- DefaultChatHistoryMetricsCalculator calculator = new();
+ List? capturedMessages = null;
+ Mock chatClientMock = new();
+ chatClientMock
+ .Setup(c => c.GetResponseAsync(It.IsAny>(), It.IsAny(), It.IsAny()))
+ .Callback, ChatOptions, CancellationToken>((msgs, _, _) => capturedMessages = [.. msgs])
+ .ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, "Brief summary.")));
+ SummarizationCompactionStrategy strategy = new(chatClientMock.Object, maxTokens: 1, preserveRecentGroups: 1, summarizationPrompt: CustomPrompt);
- await strategy.CompactAsync(messages, calculator);
+ // Act & Assert
+ await RunCompactionStrategyReducedAsync(strategy, messages, expectedCount: 2);
+ // Assert
Assert.NotNull(capturedMessages);
- // First message in request should be the custom system prompt
Assert.Equal(ChatRole.System, capturedMessages![0].Role);
Assert.Equal(CustomPrompt, capturedMessages[0].Text);
}
@@ -167,12 +141,7 @@ public class SummarizationCompactionStrategyTests
[Fact]
public async Task NullResponseText_UsesFallbackAsync()
{
- Mock chatClientMock = new();
- chatClientMock
- .Setup(c => c.GetResponseAsync(It.IsAny>(), It.IsAny(), It.IsAny()))
- .ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, (string?)null)));
-
- SummarizationCompactionStrategy strategy = new(chatClientMock.Object, maxTokens: 1, preserveRecentGroups: 1);
+ // Arrange
List messages =
[
new(ChatRole.User, "First"),
@@ -180,11 +149,16 @@ public class SummarizationCompactionStrategyTests
new(ChatRole.User, "Second"),
new(ChatRole.Assistant, "Reply 2"),
];
- DefaultChatHistoryMetricsCalculator calculator = new();
+ Mock chatClientMock = new();
+ chatClientMock
+ .Setup(c => c.GetResponseAsync(It.IsAny>(), It.IsAny(), It.IsAny()))
+ .ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, (string?)null)));
+ SummarizationCompactionStrategy strategy = new(chatClientMock.Object, maxTokens: 1, preserveRecentGroups: 1);
- CompactionResult result = await strategy.CompactAsync(messages, calculator);
+ // Act & Assert
+ await RunCompactionStrategyReducedAsync(strategy, messages, expectedCount: 2);
- Assert.True(result.Applied);
+ // Assert
Assert.Contains("[Summary unavailable]", messages[0].Text);
}
}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/ToolResultCompactionStrategyTests.cs b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/ToolResultCompactionStrategyTests.cs
index 8a6065a940..d2b3113ec6 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/ToolResultCompactionStrategyTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/ToolResultCompactionStrategyTests.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;
@@ -7,57 +7,28 @@ using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.Abstractions.UnitTests.Compaction;
-public class ToolResultCompactionStrategyTests
+public class ToolResultCompactionStrategyTests : CompactionStrategyTestBase
{
- [Fact]
- public void ShouldCompact_UnderLimit_ReturnsFalse()
- {
- ToolResultCompactionStrategy strategy = new(maxTokens: 100000);
- CompactionMetric metrics = new() { TokenCount = 500, ToolCallCount = 3 };
-
- Assert.False(strategy.ShouldCompact(metrics));
- }
-
- [Fact]
- public void ShouldCompact_OverLimitNoToolCalls_ReturnsFalse()
- {
- ToolResultCompactionStrategy strategy = new(maxTokens: 100);
- CompactionMetric metrics = new() { TokenCount = 500, ToolCallCount = 0 };
-
- Assert.False(strategy.ShouldCompact(metrics));
- }
-
- [Fact]
- public void ShouldCompact_OverLimitWithToolCalls_ReturnsTrue()
- {
- ToolResultCompactionStrategy strategy = new(maxTokens: 100);
- CompactionMetric metrics = new() { TokenCount = 500, ToolCallCount = 2 };
-
- Assert.True(strategy.ShouldCompact(metrics));
- }
-
[Fact]
public async Task UnderLimit_NoChangeAsync()
{
- ToolResultCompactionStrategy strategy = new(maxTokens: 100000);
+ // Arrange
List messages =
[
new(ChatRole.User, "Hello"),
new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("c1", "get_weather")]),
new ChatMessage(ChatRole.Tool, [new FunctionResultContent("c1", "Sunny")]),
];
- DefaultChatHistoryMetricsCalculator calculator = new();
+ ToolResultCompactionStrategy strategy = new(maxTokens: 100000);
- CompactionResult result = await strategy.CompactAsync(messages, calculator);
-
- Assert.False(result.Applied);
- Assert.Equal(3, messages.Count);
+ // Act & Assert
+ await RunCompactionStrategySkippedAsync(strategy, messages);
}
[Fact]
public async Task CollapsesOldToolGroupAsync()
{
- ToolResultCompactionStrategy strategy = new(maxTokens: 1, preserveRecentGroups: 1);
+ // Arrange
List messages =
[
new(ChatRole.User, "Check weather"),
@@ -66,13 +37,12 @@ public class ToolResultCompactionStrategyTests
new(ChatRole.User, "Thanks"),
new(ChatRole.Assistant, "You're welcome!"),
];
- DefaultChatHistoryMetricsCalculator calculator = new();
+ ToolResultCompactionStrategy strategy = new(maxTokens: 1, preserveRecentGroups: 1);
- CompactionResult result = await strategy.CompactAsync(messages, calculator);
+ // Act & Assert
+ await RunCompactionStrategyReducedAsync(strategy, messages, expectedCount: 4);
- Assert.True(result.Applied);
- // The old tool group (assistant+tool = 2 messages) should be collapsed to 1
- Assert.Equal(4, messages.Count); // user + [collapsed] + user + assistant
+ // Assert
Assert.Contains("[Tool calls: get_weather]", messages[1].Text);
Assert.Equal(ChatRole.Assistant, messages[1].Role);
}
@@ -80,8 +50,7 @@ public class ToolResultCompactionStrategyTests
[Fact]
public async Task ProtectsRecentGroupsAsync()
{
- // With preserveRecentGroups=4, all groups are protected (5 groups, protect 4 non-system)
- ToolResultCompactionStrategy strategy = new(maxTokens: 1, preserveRecentGroups: 10);
+ // Arrange
List messages =
[
new(ChatRole.User, "Check weather"),
@@ -90,19 +59,16 @@ public class ToolResultCompactionStrategyTests
new(ChatRole.User, "Thanks"),
new(ChatRole.Assistant, "You're welcome!"),
];
- DefaultChatHistoryMetricsCalculator calculator = new();
+ ToolResultCompactionStrategy strategy = new(maxTokens: 1, preserveRecentGroups: 10);
- CompactionResult result = await strategy.CompactAsync(messages, calculator);
-
- // All groups protected, so no collapse
- Assert.False(result.Applied);
- Assert.Equal(5, messages.Count);
+ // Act & Assert
+ await RunCompactionStrategySkippedAsync(strategy, messages);
}
[Fact]
public async Task PreservesSystemMessagesAsync()
{
- ToolResultCompactionStrategy strategy = new(maxTokens: 1, preserveRecentGroups: 1);
+ // Arrange
List messages =
[
new(ChatRole.System, "You are a helper"),
@@ -112,11 +78,12 @@ public class ToolResultCompactionStrategyTests
new(ChatRole.User, "Thanks"),
new(ChatRole.Assistant, "You're welcome!"),
];
- DefaultChatHistoryMetricsCalculator calculator = new();
+ ToolResultCompactionStrategy strategy = new(maxTokens: 1, preserveRecentGroups: 1);
- CompactionResult result = await strategy.CompactAsync(messages, calculator);
+ // Act & Assert
+ await RunCompactionStrategyReducedAsync(strategy, messages, expectedCount: 5);
- Assert.True(result.Applied);
+ // Assert
Assert.Equal(ChatRole.System, messages[0].Role);
Assert.Equal("You are a helper", messages[0].Text);
}
@@ -124,7 +91,7 @@ public class ToolResultCompactionStrategyTests
[Fact]
public async Task MultipleToolCalls_ListedInSummaryAsync()
{
- ToolResultCompactionStrategy strategy = new(maxTokens: 1, preserveRecentGroups: 1);
+ // Arrange
List messages =
[
new(ChatRole.User, "Do research"),
@@ -137,13 +104,12 @@ public class ToolResultCompactionStrategyTests
new(ChatRole.User, "Summarize"),
new(ChatRole.Assistant, "Here's the summary."),
];
- DefaultChatHistoryMetricsCalculator calculator = new();
+ ToolResultCompactionStrategy strategy = new(maxTokens: 1, preserveRecentGroups: 1);
- CompactionResult result = await strategy.CompactAsync(messages, calculator);
+ // Act & Assert
+ await RunCompactionStrategyReducedAsync(strategy, messages, expectedCount: 4);
- Assert.True(result.Applied);
- // Old tool group (1 assistant + 2 tools = 3 messages) collapsed to 1
- Assert.Equal(4, messages.Count);
+ // Assert
Assert.Contains("search", messages[1].Text);
Assert.Contains("fetch_page", messages[1].Text);
}
@@ -151,18 +117,15 @@ public class ToolResultCompactionStrategyTests
[Fact]
public async Task NoToolGroups_NoChangeAsync()
{
- ToolResultCompactionStrategy strategy = new(maxTokens: 1);
+ // Arrange
List messages =
[
new(ChatRole.User, "Hello"),
new(ChatRole.Assistant, "Hi there"),
];
- DefaultChatHistoryMetricsCalculator calculator = new();
+ ToolResultCompactionStrategy strategy = new(maxTokens: 1);
- // ToolCallCount is 0, so ShouldCompact returns false
- CompactionResult result = await strategy.CompactAsync(messages, calculator);
-
- Assert.False(result.Applied);
- Assert.Equal(2, messages.Count);
+ // Act & Assert
+ await RunCompactionStrategySkippedAsync(strategy, messages);
}
}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/TruncationCompactionStrategyTests.cs b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/TruncationCompactionStrategyTests.cs
index 47929d23e5..b4be4422a5 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/TruncationCompactionStrategyTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/TruncationCompactionStrategyTests.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;
@@ -7,29 +7,26 @@ using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.Abstractions.UnitTests.Compaction;
-public class TruncationCompactionStrategyTests
+public class TruncationCompactionStrategyTests : CompactionStrategyTestBase
{
[Fact]
public async Task UnderLimit_NoChangeAsync()
{
- TruncationCompactionStrategy strategy = new(maxTokens: 100000);
+ // Arrange
List messages =
[
new(ChatRole.User, "Hello"),
];
- DefaultChatHistoryMetricsCalculator calculator = new();
+ TruncationCompactionStrategy strategy = new(maxTokens: 100000);
- CompactionResult result = await strategy.CompactAsync(messages, calculator);
-
- Assert.False(result.Applied);
- Assert.Single(messages);
+ // Act & Assert
+ await RunCompactionStrategySkippedAsync(strategy, messages);
}
[Fact]
public async Task OverLimit_RemovesOldestGroupsAsync()
{
- // Use a very low max to trigger compaction
- TruncationCompactionStrategy strategy = new(maxTokens: 1);
+ // Arrange
List messages =
[
new(ChatRole.User, "First message"),
@@ -37,77 +34,45 @@ public class TruncationCompactionStrategyTests
new(ChatRole.User, "Second message"),
new(ChatRole.Assistant, "Second reply"),
];
- DefaultChatHistoryMetricsCalculator calculator = new();
+ TruncationCompactionStrategy strategy = new(maxTokens: 1);
- CompactionResult result = await strategy.CompactAsync(messages, calculator);
-
- Assert.True(result.Applied);
- // Should have removed old groups, keeping at least the last one
- Assert.True(messages.Count < 4);
- Assert.True(messages.Count > 0);
- }
-
- [Fact]
- public void ShouldCompact_ReturnsFalseWhenUnderLimit()
- {
- TruncationCompactionStrategy strategy = new(maxTokens: 10000);
- CompactionMetric metrics = new() { TokenCount = 500 };
-
- Assert.False(strategy.ShouldCompact(metrics));
- }
-
- [Fact]
- public void ShouldCompact_ReturnsTrueWhenOverLimit()
- {
- TruncationCompactionStrategy strategy = new(maxTokens: 100);
- CompactionMetric metrics = new() { TokenCount = 500 };
-
- Assert.True(strategy.ShouldCompact(metrics));
+ // Act & Assert
+ await RunCompactionStrategyReducedAsync(strategy, messages, expectedCount: 1);
}
[Fact]
public async Task SystemOnlyMessages_NoChangeAsync()
{
- // Only system messages → removableGroups.Count == 0 → no change
- TruncationCompactionStrategy strategy = new(maxTokens: 1);
+ // Arrange
List messages =
[
new(ChatRole.System, "You are a helper"),
];
- DefaultChatHistoryMetricsCalculator calculator = new();
+ TruncationCompactionStrategy strategy = new(maxTokens: 1);
- CompactionResult result = await strategy.CompactAsync(messages, calculator);
-
- // ShouldCompact triggers but reducer finds nothing removable
- Assert.Single(messages);
- Assert.Equal(result.After.MessageCount, result.Before.MessageCount);
- Assert.Equal(result.After.TokenCount, result.Before.TokenCount);
+ // Act & Assert
+ await RunCompactionStrategySkippedAsync(strategy, messages);
}
[Fact]
public async Task SingleNonSystemGroup_NoChangeAsync()
{
- // Only one non-system group → maxRemovable <= 0 → no change
- TruncationCompactionStrategy strategy = new(maxTokens: 1);
+ // Arrange
List messages =
[
new(ChatRole.System, "System prompt"),
new(ChatRole.User, "Only user message"),
];
- DefaultChatHistoryMetricsCalculator calculator = new();
+ TruncationCompactionStrategy strategy = new(maxTokens: 1);
- CompactionResult result = await strategy.CompactAsync(messages, calculator);
-
- Assert.Equal(2, messages.Count);
- Assert.Equal(result.After.MessageCount, result.Before.MessageCount);
- Assert.Equal(result.After.TokenCount, result.Before.TokenCount);
+ // Act & Assert
+ await RunCompactionStrategySkippedAsync(strategy, messages);
}
[Fact]
public async Task PreserveRecentGroups_KeepsMultipleGroupsAsync()
{
- // preserveRecentGroups=2 means keep at least the last 2 non-system groups
- TruncationCompactionStrategy strategy = new(maxTokens: 1, preserveRecentGroups: 2);
+ // Arrange
List messages =
[
new(ChatRole.User, "Turn 1"),
@@ -117,14 +82,12 @@ public class TruncationCompactionStrategyTests
new(ChatRole.User, "Turn 3"),
new(ChatRole.Assistant, "Reply 3"),
];
- DefaultChatHistoryMetricsCalculator calculator = new();
+ TruncationCompactionStrategy strategy = new(maxTokens: 1, preserveRecentGroups: 2);
- CompactionResult result = await strategy.CompactAsync(messages, calculator);
+ // Act & Assert
+ await RunCompactionStrategyReducedAsync(strategy, messages, expectedCount: 2);
- Assert.True(result.Applied);
- // 6 groups (3 user + 3 assistant), protect last 2 → 4 removable
- // Should keep at least: Turn 3 user + Reply 3 assistant (last 2 groups)
- Assert.True(messages.Count >= 2);
+ // Assert
Assert.Equal("Reply 3", messages[^1].Text);
}
}