This commit is contained in:
Chris Rickman
2026-03-05 03:10:40 -08:00
Unverified
parent 6ce0447ff6
commit 7e2c5ad4e6
9 changed files with 181 additions and 108 deletions
@@ -47,7 +47,7 @@ PipelineCompactionStrategy compactionPipeline =
new SummarizationCompactionStrategy(summarizerChatClient, CompactionTriggers.TokensExceed(0x500)),
// 3. Aggressive: keep only the last N user turns and their responses
new SlidingWindowCompactionStrategy(maximumTurns: 4),
new SlidingWindowCompactionStrategy(CompactionTriggers.TurnsExceed(4)),
// 4. Emergency: drop oldest groups until under the token budget
new TruncationCompactionStrategy(CompactionTriggers.TokensExceed(0x8000)));
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
@@ -7,20 +8,18 @@ using System.Threading.Tasks;
namespace Microsoft.Agents.AI.Compaction;
/// <summary>
/// A compaction strategy that keeps only the most recent user turns and their
/// associated response groups, removing older turns to bound conversation length.
/// A compaction strategy that removes the oldest user turns and their associated response groups
/// to bound conversation length.
/// </summary>
/// <remarks>
/// <para>
/// This strategy always preserves system messages. It identifies user turns in the
/// conversation (via <see cref="MessageGroup.TurnIndex"/>) and keeps the last
/// <see cref="MaxTurns"/> turns along with all response groups (assistant replies,
/// tool call groups) that belong to each kept turn.
/// conversation (via <see cref="MessageGroup.TurnIndex"/>) and excludes the oldest turns
/// one at a time until the <see cref="CompactionStrategy.Target"/> condition is met.
/// </para>
/// <para>
/// The <see cref="CompactionTrigger"/> predicate controls when compaction proceeds.
/// When <see langword="null"/>, a default trigger of <see cref="CompactionTriggers.TurnsExceed"/>
/// with <see cref="MaxTurns"/> is used.
/// <see cref="MinimumPreserved"/> is a hard floor: even if the <see cref="CompactionStrategy.Target"/>
/// has not been reached, compaction will not touch the last <see cref="MinimumPreserved"/> non-system groups.
/// </para>
/// <para>
/// This strategy is more predictable than token-based truncation for bounding conversation
@@ -30,62 +29,87 @@ namespace Microsoft.Agents.AI.Compaction;
public sealed class SlidingWindowCompactionStrategy : CompactionStrategy
{
/// <summary>
/// The default maximum number of user turns to retain before compaction occurs. This default is a reasonable starting point
/// for many conversations, but should be tuned based on the expected conversation length and token budget.
/// The default minimum number of most-recent non-system groups to preserve.
/// </summary>
public const int DefaultMaximumTurns = 32;
public const int DefaultMinimumPreserved = 1;
/// <summary>
/// Initializes a new instance of the <see cref="SlidingWindowCompactionStrategy"/> class.
/// </summary>
/// <param name="maximumTurns">
/// The maximum number of user turns to keep. Older turns and their associated responses are removed.
/// <param name="trigger">
/// The <see cref="CompactionTrigger"/> that controls when compaction proceeds.
/// Use <see cref="CompactionTriggers.TurnsExceed"/> for turn-based thresholds.
/// </param>
/// <param name="minimumPreserved">
/// The minimum number of most-recent non-system message groups to preserve.
/// This is a hard floor — compaction will not exclude groups beyond this limit,
/// regardless of the target condition.
/// </param>
/// <param name="target">
/// An optional target condition that controls when compaction stops. When <see langword="null"/>,
/// defaults to the inverse of the auto-derived trigger — compaction stops as soon as the turn count is within bounds.
/// defaults to the inverse of the <paramref name="trigger"/> — compaction stops as soon as the trigger would no longer fire.
/// </param>
public SlidingWindowCompactionStrategy(int maximumTurns = DefaultMaximumTurns, CompactionTrigger? target = null)
: base(CompactionTriggers.TurnsExceed(maximumTurns), target)
public SlidingWindowCompactionStrategy(CompactionTrigger trigger, int minimumPreserved = DefaultMinimumPreserved, CompactionTrigger? target = null)
: base(trigger, target)
{
this.MaxTurns = maximumTurns;
this.MinimumPreserved = minimumPreserved;
}
/// <summary>
/// Gets the maximum number of user turns to retain after compaction.
/// Gets the minimum number of most-recent non-system groups that are always preserved.
/// This is a hard floor that compaction cannot exceed, regardless of the target condition.
/// </summary>
public int MaxTurns { get; }
public int MinimumPreserved { get; }
/// <inheritdoc/>
protected override Task<bool> ApplyCompactionAsync(MessageIndex index, CancellationToken cancellationToken)
{
// Collect distinct included turn indices in order
List<int> includedTurns = [];
// Identify protected groups: the N most-recent non-system, non-excluded groups
List<int> nonSystemIncludedIndices = [];
foreach (MessageGroup group in index.Groups)
{
if (!group.IsExcluded && group.TurnIndex is int turnIndex && !includedTurns.Contains(turnIndex))
if (!group.IsExcluded && group.Kind != MessageGroupKind.System)
{
includedTurns.Add(turnIndex);
nonSystemIncludedIndices.Add(index.Groups.IndexOf(group));
}
}
if (includedTurns.Count <= this.MaxTurns)
int protectedStart = Math.Max(0, nonSystemIncludedIndices.Count - this.MinimumPreserved);
HashSet<int> protectedGroupIndices = [];
for (int i = protectedStart; i < nonSystemIncludedIndices.Count; i++)
{
return Task.FromResult(false);
protectedGroupIndices.Add(nonSystemIncludedIndices[i]);
}
// Collect distinct included turn indices in order (oldest first), excluding protected groups
List<int> excludableTurns = [];
for (int i = 0; i < index.Groups.Count; i++)
{
MessageGroup group = index.Groups[i];
if (!group.IsExcluded
&& group.Kind != MessageGroupKind.System
&& !protectedGroupIndices.Contains(i)
&& group.TurnIndex is int turnIndex
&& !excludableTurns.Contains(turnIndex))
{
excludableTurns.Add(turnIndex);
}
}
// Exclude one turn at a time from oldest, re-checking target after each
int turnsToRemove = includedTurns.Count - this.MaxTurns;
bool compacted = false;
for (int t = 0; t < turnsToRemove; t++)
for (int t = 0; t < excludableTurns.Count; t++)
{
int turnToExclude = includedTurns[t];
int turnToExclude = excludableTurns[t];
for (int i = 0; i < index.Groups.Count; i++)
{
MessageGroup group = index.Groups[i];
if (!group.IsExcluded && group.Kind != MessageGroupKind.System && group.TurnIndex == turnToExclude)
if (!group.IsExcluded
&& group.Kind != MessageGroupKind.System
&& !protectedGroupIndices.Contains(i)
&& group.TurnIndex == turnToExclude)
{
group.IsExcluded = true;
group.ExcludeReason = $"Excluded by {nameof(SlidingWindowCompactionStrategy)}";
@@ -16,12 +16,16 @@ namespace Microsoft.Agents.AI.Compaction;
/// </summary>
/// <remarks>
/// <para>
/// This strategy protects system messages and the most recent <see cref="PreserveRecentGroups"/>
/// This strategy protects system messages and the most recent <see cref="MinimumPreserved"/>
/// non-system groups. All older groups are collected and sent to the <see cref="IChatClient"/>
/// for summarization. The resulting summary replaces those messages as a single assistant message
/// with <see cref="MessageGroupKind.Summary"/>.
/// </para>
/// <para>
/// <see cref="MinimumPreserved"/> is a hard floor: even if the <see cref="CompactionStrategy.Target"/>
/// has not been reached, compaction will not touch the last <see cref="MinimumPreserved"/> non-system groups.
/// </para>
/// <para>
/// The <see cref="CompactionTrigger"/> predicate controls when compaction proceeds.
/// When <see langword="null"/>, the strategy compacts whenever there are groups older than the preserve window.
/// Use <see cref="CompactionTriggers"/> for common trigger conditions such as token thresholds.
@@ -50,9 +54,10 @@ public sealed class SummarizationCompactionStrategy : CompactionStrategy
/// <param name="trigger">
/// The <see cref="CompactionTrigger"/> that controls when compaction proceeds.
/// </param>
/// <param name="preserveRecentGroups">
/// The number of most-recent non-system message groups to protect from summarization.
/// Defaults to 4, preserving the current and recent exchanges.
/// <param name="minimumPreserved">
/// The minimum number of most-recent non-system message groups to preserve.
/// This is a hard floor — compaction will not summarize groups beyond this limit,
/// regardless of the target condition. Defaults to 4, preserving the current and recent exchanges.
/// </param>
/// <param name="summarizationPrompt">
/// An optional custom system prompt for the summarization LLM call. When <see langword="null"/>,
@@ -65,13 +70,13 @@ public sealed class SummarizationCompactionStrategy : CompactionStrategy
public SummarizationCompactionStrategy(
IChatClient chatClient,
CompactionTrigger trigger,
int preserveRecentGroups = 4,
int minimumPreserved = 4,
string? summarizationPrompt = null,
CompactionTrigger? target = null)
: base(trigger, target)
{
this.ChatClient = Throw.IfNull(chatClient);
this.PreserveRecentGroups = preserveRecentGroups;
this.MinimumPreserved = minimumPreserved;
this.SummarizationPrompt = summarizationPrompt ?? DefaultSummarizationPrompt;
}
@@ -81,9 +86,10 @@ public sealed class SummarizationCompactionStrategy : CompactionStrategy
public IChatClient ChatClient { get; }
/// <summary>
/// Gets the number of most-recent non-system groups to protect from summarization.
/// Gets the minimum number of most-recent non-system groups that are always preserved.
/// This is a hard floor that compaction cannot exceed, regardless of the target condition.
/// </summary>
public int PreserveRecentGroups { get; }
public int MinimumPreserved { get; }
/// <summary>
/// Gets the prompt used when requesting summaries from the chat client.
@@ -104,7 +110,7 @@ public sealed class SummarizationCompactionStrategy : CompactionStrategy
}
}
int protectedFromEnd = Math.Min(this.PreserveRecentGroups, nonSystemIncludedCount);
int protectedFromEnd = Math.Min(this.MinimumPreserved, nonSystemIncludedCount);
int maxSummarizable = nonSystemIncludedCount - protectedFromEnd;
if (maxSummarizable <= 0)
@@ -21,6 +21,10 @@ namespace Microsoft.Agents.AI.Compaction;
/// <c>[Tool calls: get_weather, search_docs]</c>.
/// </para>
/// <para>
/// <see cref="MinimumPreserved"/> is a hard floor: even if the <see cref="CompactionStrategy.Target"/>
/// has not been reached, compaction will not touch the last <see cref="MinimumPreserved"/> non-system groups.
/// </para>
/// <para>
/// The <see cref="CompactionTrigger"/> predicate controls when compaction proceeds.
/// When <see langword="null"/>, a default compound trigger of
/// <see cref="CompactionTriggers.TokensExceed"/> AND <see cref="CompactionTriggers.HasToolCalls"/>
@@ -30,9 +34,9 @@ namespace Microsoft.Agents.AI.Compaction;
public sealed class ToolResultCompactionStrategy : CompactionStrategy
{
/// <summary>
/// The default number of most-recent non-system groups to protect from collapsing.
/// The default minimum number of most-recent non-system groups to preserve.
/// </summary>
public const int DefaultPreserveRecentGroups = 2;
public const int DefaultMinimumPreserved = 2;
/// <summary>
/// Initializes a new instance of the <see cref="ToolResultCompactionStrategy"/> class.
@@ -40,24 +44,27 @@ public sealed class ToolResultCompactionStrategy : CompactionStrategy
/// <param name="trigger">
/// The <see cref="CompactionTrigger"/> that controls when compaction proceeds.
/// </param>
/// <param name="preserveRecentGroups">
/// The number of most-recent non-system message groups to protect from collapsing.
/// Defaults to <see cref="DefaultPreserveRecentGroups"/>, ensuring the current turn's tool interactions remain visible.
/// <param name="minimumPreserved">
/// The minimum number of most-recent non-system message groups to preserve.
/// This is a hard floor — compaction will not collapse groups beyond this limit,
/// regardless of the target condition.
/// Defaults to <see cref="DefaultMinimumPreserved"/>, ensuring the current turn's tool interactions remain visible.
/// </param>
/// <param name="target">
/// An optional target condition that controls when compaction stops. When <see langword="null"/>,
/// defaults to the inverse of the <paramref name="trigger"/> — compaction stops as soon as the trigger would no longer fire.
/// </param>
public ToolResultCompactionStrategy(CompactionTrigger trigger, int preserveRecentGroups = DefaultPreserveRecentGroups, CompactionTrigger? target = null)
public ToolResultCompactionStrategy(CompactionTrigger trigger, int minimumPreserved = DefaultMinimumPreserved, CompactionTrigger? target = null)
: base(trigger, target)
{
this.PreserveRecentGroups = preserveRecentGroups;
this.MinimumPreserved = minimumPreserved;
}
/// <summary>
/// Gets the number of most-recent non-system groups to protect from collapsing.
/// Gets the minimum number of most-recent non-system groups that are always preserved.
/// This is a hard floor that compaction cannot exceed, regardless of the target condition.
/// </summary>
public int PreserveRecentGroups { get; }
public int MinimumPreserved { get; }
/// <inheritdoc/>
protected override Task<bool> ApplyCompactionAsync(MessageIndex index, CancellationToken cancellationToken)
@@ -73,7 +80,7 @@ public sealed class ToolResultCompactionStrategy : CompactionStrategy
}
}
int protectedStart = Math.Max(0, nonSystemIncludedIndices.Count - this.PreserveRecentGroups);
int protectedStart = Math.Max(0, nonSystemIncludedIndices.Count - this.MinimumPreserved);
HashSet<int> protectedGroupIndices = [];
for (int i = protectedStart; i < nonSystemIncludedIndices.Count; i++)
{
@@ -7,7 +7,7 @@ namespace Microsoft.Agents.AI.Compaction;
/// <summary>
/// A compaction strategy that removes the oldest non-system message groups,
/// keeping the most recent groups up to <see cref="PreserveRecentGroups"/>.
/// keeping at least <see cref="MinimumPreserved"/> most-recent groups intact.
/// </summary>
/// <remarks>
/// <para>
@@ -16,6 +16,10 @@ namespace Microsoft.Agents.AI.Compaction;
/// corresponding tool result messages are always removed together.
/// </para>
/// <para>
/// <see cref="MinimumPreserved"/> is a hard floor: even if the <see cref="CompactionStrategy.Target"/>
/// has not been reached, compaction will not touch the last <see cref="MinimumPreserved"/> non-system groups.
/// </para>
/// <para>
/// The <see cref="CompactionTrigger"/> controls when compaction proceeds.
/// Use <see cref="CompactionTriggers"/> for common trigger conditions such as token or group thresholds.
/// </para>
@@ -23,9 +27,9 @@ namespace Microsoft.Agents.AI.Compaction;
public sealed class TruncationCompactionStrategy : CompactionStrategy
{
/// <summary>
/// The default number of most-recent non-system groups to protect from collapsing.
/// The default minimum number of most-recent non-system groups to preserve.
/// </summary>
public const int DefaultPreserveRecentGroups = 32;
public const int DefaultMinimumPreserved = 32;
/// <summary>
/// Initializes a new instance of the <see cref="TruncationCompactionStrategy"/> class.
@@ -33,24 +37,26 @@ public sealed class TruncationCompactionStrategy : CompactionStrategy
/// <param name="trigger">
/// The <see cref="CompactionTrigger"/> that controls when compaction proceeds.
/// </param>
/// <param name="preserveRecentGroups">
/// The minimum number of most-recent non-system message groups to keep.
/// Defaults to 1 so that at least the latest exchange is always preserved.
/// <param name="minimumPreserved">
/// The minimum number of most-recent non-system message groups to preserve.
/// This is a hard floor — compaction will not remove groups beyond this limit,
/// regardless of the target condition.
/// </param>
/// <param name="target">
/// An optional target condition that controls when compaction stops. When <see langword="null"/>,
/// defaults to the inverse of the <paramref name="trigger"/> — compaction stops as soon as the trigger would no longer fire.
/// </param>
public TruncationCompactionStrategy(CompactionTrigger trigger, int preserveRecentGroups = DefaultPreserveRecentGroups, CompactionTrigger? target = null)
public TruncationCompactionStrategy(CompactionTrigger trigger, int minimumPreserved = DefaultMinimumPreserved, CompactionTrigger? target = null)
: base(trigger, target)
{
this.PreserveRecentGroups = preserveRecentGroups;
this.MinimumPreserved = minimumPreserved;
}
/// <summary>
/// Gets the minimum number of most-recent non-system message groups to retain after compaction.
/// Gets the minimum number of most-recent non-system message groups that are always preserved.
/// This is a hard floor that compaction cannot exceed, regardless of the target condition.
/// </summary>
public int PreserveRecentGroups { get; }
public int MinimumPreserved { get; }
/// <inheritdoc/>
protected override Task<bool> ApplyCompactionAsync(MessageIndex index, CancellationToken cancellationToken)
@@ -66,7 +72,7 @@ public sealed class TruncationCompactionStrategy : CompactionStrategy
}
}
int maxRemovable = removableCount - this.PreserveRecentGroups;
int maxRemovable = removableCount - this.MinimumPreserved;
if (maxRemovable <= 0)
{
return Task.FromResult(false);
@@ -15,8 +15,8 @@ public class SlidingWindowCompactionStrategyTests
[Fact]
public async Task CompactAsyncBelowMaxTurnsReturnsFalseAsync()
{
// Arrange
SlidingWindowCompactionStrategy strategy = new(maximumTurns: 3);
// Arrange — trigger requires > 3 turns, conversation has 2
SlidingWindowCompactionStrategy strategy = new(CompactionTriggers.TurnsExceed(3));
MessageIndex groups = MessageIndex.Create(
[
new ChatMessage(ChatRole.User, "Q1"),
@@ -35,8 +35,8 @@ public class SlidingWindowCompactionStrategyTests
[Fact]
public async Task CompactAsyncExceedsMaxTurnsExcludesOldestTurnsAsync()
{
// Arrange — keep 2 turns, conversation has 3
SlidingWindowCompactionStrategy strategy = new(maximumTurns: 2);
// Arrange — trigger on > 2 turns, conversation has 3
SlidingWindowCompactionStrategy strategy = new(CompactionTriggers.TurnsExceed(2));
MessageIndex groups = MessageIndex.Create(
[
new ChatMessage(ChatRole.User, "Q1"),
@@ -65,8 +65,8 @@ public class SlidingWindowCompactionStrategyTests
[Fact]
public async Task CompactAsyncPreservesSystemMessagesAsync()
{
// Arrange
SlidingWindowCompactionStrategy strategy = new(maximumTurns: 1);
// Arrange — trigger on > 1 turn
SlidingWindowCompactionStrategy strategy = new(CompactionTriggers.TurnsExceed(1));
MessageIndex groups = MessageIndex.Create(
[
new ChatMessage(ChatRole.System, "You are helpful."),
@@ -89,8 +89,8 @@ public class SlidingWindowCompactionStrategyTests
[Fact]
public async Task CompactAsyncPreservesToolCallGroupsInKeptTurnsAsync()
{
// Arrange
SlidingWindowCompactionStrategy strategy = new(maximumTurns: 1);
// Arrange — trigger on > 1 turn
SlidingWindowCompactionStrategy strategy = new(CompactionTriggers.TurnsExceed(1));
MessageIndex groups = MessageIndex.Create(
[
new ChatMessage(ChatRole.User, "Q1"),
@@ -114,10 +114,10 @@ public class SlidingWindowCompactionStrategyTests
}
[Fact]
public async Task CompactAsyncCustomTriggerOverridesDefaultAsync()
public async Task CompactAsyncTriggerNotMetReturnsFalseAsync()
{
// Arrange — custom trigger: only compact when tokens exceed threshold
SlidingWindowCompactionStrategy strategy = new(maximumTurns: 99);
// Arrange — trigger requires > 99 turns
SlidingWindowCompactionStrategy strategy = new(CompactionTriggers.TurnsExceed(99));
MessageIndex groups = MessageIndex.Create(
[
@@ -126,7 +126,7 @@ public class SlidingWindowCompactionStrategyTests
new ChatMessage(ChatRole.User, "Q3"),
]);
// Act — tokens are tiny, trigger not met
// Act
bool result = await strategy.CompactAsync(groups);
// Assert
@@ -136,8 +136,8 @@ public class SlidingWindowCompactionStrategyTests
[Fact]
public async Task CompactAsyncIncludedMessagesContainOnlyKeptTurnsAsync()
{
// Arrange
SlidingWindowCompactionStrategy strategy = new(maximumTurns: 1);
// Arrange — trigger on > 1 turn
SlidingWindowCompactionStrategy strategy = new(CompactionTriggers.TurnsExceed(1));
MessageIndex groups = MessageIndex.Create(
[
new ChatMessage(ChatRole.System, "System"),
@@ -161,13 +161,13 @@ public class SlidingWindowCompactionStrategyTests
[Fact]
public async Task CompactAsyncCustomTargetStopsExcludingEarlyAsync()
{
// Arrange — 4 turns, maxTurns=1 means 3 should be excluded
// But custom target stops after removing 1 turn
// Arrange — trigger on > 1 turn, custom target stops after removing 1 turn
int removeCount = 0;
CompactionTrigger targetAfterOne = _ => ++removeCount >= 1;
SlidingWindowCompactionStrategy strategy = new(
maximumTurns: 1,
CompactionTriggers.TurnsExceed(1),
minimumPreserved: 0,
target: targetAfterOne);
MessageIndex index = MessageIndex.Create(
@@ -191,4 +191,34 @@ public class SlidingWindowCompactionStrategyTests
Assert.False(index.Groups[2].IsExcluded); // Q2 (turn 2) — kept
Assert.False(index.Groups[3].IsExcluded); // A2 (turn 2)
}
[Fact]
public async Task CompactAsyncMinimumPreservedStopsCompactionAsync()
{
// Arrange — always trigger with never-satisfied target, but MinimumPreserved = 2 is hard floor
SlidingWindowCompactionStrategy strategy = new(
CompactionTriggers.TurnsExceed(1),
minimumPreserved: 2,
target: _ => false);
MessageIndex index = MessageIndex.Create(
[
new ChatMessage(ChatRole.User, "Q1"),
new ChatMessage(ChatRole.Assistant, "A1"),
new ChatMessage(ChatRole.User, "Q2"),
new ChatMessage(ChatRole.Assistant, "A2"),
new ChatMessage(ChatRole.User, "Q3"),
new ChatMessage(ChatRole.Assistant, "A3"),
]);
// Act
bool result = await strategy.CompactAsync(index);
// Assert — target never says stop, but MinimumPreserved=2 prevents removing the last 2 groups
Assert.True(result);
Assert.Equal(2, index.IncludedGroupCount);
// Last 2 non-system groups must be preserved
Assert.False(index.Groups[4].IsExcluded); // Q3
Assert.False(index.Groups[5].IsExcluded); // A3
}
}
@@ -38,7 +38,7 @@ public class SummarizationCompactionStrategyTests
SummarizationCompactionStrategy strategy = new(
CreateMockChatClient(),
CompactionTriggers.TokensExceed(100000),
preserveRecentGroups: 1);
minimumPreserved: 1);
MessageIndex index = MessageIndex.Create(
[
@@ -61,7 +61,7 @@ public class SummarizationCompactionStrategyTests
SummarizationCompactionStrategy strategy = new(
CreateMockChatClient("Key facts from earlier."),
AlwaysTrigger,
preserveRecentGroups: 1);
minimumPreserved: 1);
MessageIndex index = MessageIndex.Create(
[
@@ -92,7 +92,7 @@ public class SummarizationCompactionStrategyTests
SummarizationCompactionStrategy strategy = new(
CreateMockChatClient(),
AlwaysTrigger,
preserveRecentGroups: 1);
minimumPreserved: 1);
MessageIndex index = MessageIndex.Create(
[
@@ -119,7 +119,7 @@ public class SummarizationCompactionStrategyTests
SummarizationCompactionStrategy strategy = new(
CreateMockChatClient("Summary text."),
AlwaysTrigger,
preserveRecentGroups: 1);
minimumPreserved: 1);
MessageIndex index = MessageIndex.Create(
[
@@ -146,7 +146,7 @@ public class SummarizationCompactionStrategyTests
SummarizationCompactionStrategy strategy = new(
CreateMockChatClient(" "),
AlwaysTrigger,
preserveRecentGroups: 1);
minimumPreserved: 1);
MessageIndex index = MessageIndex.Create(
[
@@ -169,7 +169,7 @@ public class SummarizationCompactionStrategyTests
SummarizationCompactionStrategy strategy = new(
CreateMockChatClient(),
AlwaysTrigger,
preserveRecentGroups: 5);
minimumPreserved: 5);
MessageIndex index = MessageIndex.Create(
[
@@ -202,7 +202,7 @@ public class SummarizationCompactionStrategyTests
SummarizationCompactionStrategy strategy = new(
mockClient.Object,
AlwaysTrigger,
preserveRecentGroups: 1,
minimumPreserved: 1,
summarizationPrompt: customPrompt);
MessageIndex index = MessageIndex.Create(
@@ -226,7 +226,7 @@ public class SummarizationCompactionStrategyTests
SummarizationCompactionStrategy strategy = new(
CreateMockChatClient(),
AlwaysTrigger,
preserveRecentGroups: 1);
minimumPreserved: 1);
MessageIndex index = MessageIndex.Create(
[
@@ -253,7 +253,7 @@ public class SummarizationCompactionStrategyTests
SummarizationCompactionStrategy strategy = new(
CreateMockChatClient("Partial summary."),
AlwaysTrigger,
preserveRecentGroups: 1,
minimumPreserved: 1,
target: targetAfterOne);
MessageIndex index = MessageIndex.Create(
@@ -279,7 +279,7 @@ public class SummarizationCompactionStrategyTests
SummarizationCompactionStrategy strategy = new(
CreateMockChatClient("Summary."),
AlwaysTrigger,
preserveRecentGroups: 2);
minimumPreserved: 2);
MessageIndex index = MessageIndex.Create(
[
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft. All rights reserved.
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Threading.Tasks;
@@ -41,7 +41,7 @@ public class ToolResultCompactionStrategyTests
// Arrange — always trigger
ToolResultCompactionStrategy strategy = new(
trigger: _ => true,
preserveRecentGroups: 1);
minimumPreserved: 1);
MessageIndex groups = MessageIndex.Create(
[
@@ -71,7 +71,7 @@ public class ToolResultCompactionStrategyTests
// Arrange — protect 2 recent non-system groups (the tool group + Q2)
ToolResultCompactionStrategy strategy = new(
trigger: _ => true,
preserveRecentGroups: 3);
minimumPreserved: 3);
MessageIndex groups = MessageIndex.Create(
[
@@ -94,7 +94,7 @@ public class ToolResultCompactionStrategyTests
// Arrange
ToolResultCompactionStrategy strategy = new(
trigger: _ => true,
preserveRecentGroups: 1);
minimumPreserved: 1);
MessageIndex groups = MessageIndex.Create(
[
@@ -119,7 +119,7 @@ public class ToolResultCompactionStrategyTests
// Arrange — assistant calls two tools
ToolResultCompactionStrategy strategy = new(
trigger: _ => true,
preserveRecentGroups: 1);
minimumPreserved: 1);
ChatMessage multiToolCall = new(ChatRole.Assistant,
[
@@ -152,7 +152,7 @@ public class ToolResultCompactionStrategyTests
// Arrange — trigger fires but no tool groups to collapse
ToolResultCompactionStrategy strategy = new(
trigger: _ => true,
preserveRecentGroups: 0);
minimumPreserved: 0);
MessageIndex groups = MessageIndex.Create(
[
@@ -175,7 +175,7 @@ public class ToolResultCompactionStrategyTests
CompactionTriggers.All(
CompactionTriggers.TokensExceed(0),
CompactionTriggers.HasToolCalls()),
preserveRecentGroups: 1);
minimumPreserved: 1);
MessageIndex groups = MessageIndex.Create(
[
@@ -201,7 +201,7 @@ public class ToolResultCompactionStrategyTests
ToolResultCompactionStrategy strategy = new(
trigger: _ => true,
preserveRecentGroups: 1,
minimumPreserved: 1,
target: targetAfterOne);
MessageIndex index = MessageIndex.Create(
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft. All rights reserved.
// Copyright (c) Microsoft. All rights reserved.
using System.Linq;
using System.Threading.Tasks;
@@ -18,7 +18,7 @@ public class TruncationCompactionStrategyTests
public async Task CompactAsyncAlwaysTriggerCompactsToPreserveRecentAsync()
{
// Arrange — always-trigger means always compact
TruncationCompactionStrategy strategy = new(s_alwaysTrigger, preserveRecentGroups: 1);
TruncationCompactionStrategy strategy = new(s_alwaysTrigger, minimumPreserved: 1);
MessageIndex groups = MessageIndex.Create(
[
new ChatMessage(ChatRole.User, "First"),
@@ -39,7 +39,7 @@ public class TruncationCompactionStrategyTests
{
// Arrange — trigger requires > 1000 tokens, conversation is tiny
TruncationCompactionStrategy strategy = new(
preserveRecentGroups: 1,
minimumPreserved: 1,
trigger: CompactionTriggers.TokensExceed(1000));
MessageIndex groups = MessageIndex.Create(
@@ -61,7 +61,7 @@ public class TruncationCompactionStrategyTests
{
// Arrange — trigger on groups > 2
TruncationCompactionStrategy strategy = new(
preserveRecentGroups: 1,
minimumPreserved: 1,
trigger: CompactionTriggers.GroupsExceed(2));
MessageIndex groups = MessageIndex.Create(
@@ -89,7 +89,7 @@ public class TruncationCompactionStrategyTests
public async Task CompactAsyncPreservesSystemMessagesAsync()
{
// Arrange
TruncationCompactionStrategy strategy = new(s_alwaysTrigger, preserveRecentGroups: 1);
TruncationCompactionStrategy strategy = new(s_alwaysTrigger, minimumPreserved: 1);
MessageIndex groups = MessageIndex.Create(
[
new ChatMessage(ChatRole.System, "You are helpful."),
@@ -117,7 +117,7 @@ public class TruncationCompactionStrategyTests
public async Task CompactAsyncPreservesToolCallGroupAtomicityAsync()
{
// Arrange
TruncationCompactionStrategy strategy = new(s_alwaysTrigger, preserveRecentGroups: 1);
TruncationCompactionStrategy strategy = new(s_alwaysTrigger, minimumPreserved: 1);
ChatMessage assistantToolCall= new(ChatRole.Assistant, [new FunctionCallContent("call1", "get_weather")]);
ChatMessage toolResult = new(ChatRole.Tool, "Sunny");
@@ -141,7 +141,7 @@ public class TruncationCompactionStrategyTests
public async Task CompactAsyncSetsExcludeReasonAsync()
{
// Arrange
TruncationCompactionStrategy strategy = new(s_alwaysTrigger, preserveRecentGroups: 1);
TruncationCompactionStrategy strategy = new(s_alwaysTrigger, minimumPreserved: 1);
MessageIndex groups = MessageIndex.Create(
[
new ChatMessage(ChatRole.User, "Old"),
@@ -160,7 +160,7 @@ public class TruncationCompactionStrategyTests
public async Task CompactAsyncSkipsAlreadyExcludedGroupsAsync()
{
// Arrange
TruncationCompactionStrategy strategy = new(s_alwaysTrigger, preserveRecentGroups: 1);
TruncationCompactionStrategy strategy = new(s_alwaysTrigger, minimumPreserved: 1);
MessageIndex groups = MessageIndex.Create(
[
new ChatMessage(ChatRole.User, "Already excluded"),
@@ -180,10 +180,10 @@ public class TruncationCompactionStrategyTests
}
[Fact]
public async Task CompactAsyncPreserveRecentGroupsKeepsMultipleAsync()
public async Task CompactAsyncMinimumPreservedKeepsMultipleAsync()
{
// Arrange — keep 2 most recent
TruncationCompactionStrategy strategy = new(s_alwaysTrigger, preserveRecentGroups: 2);
TruncationCompactionStrategy strategy = new(s_alwaysTrigger, minimumPreserved: 2);
MessageIndex groups = MessageIndex.Create(
[
new ChatMessage(ChatRole.User, "Q1"),
@@ -207,7 +207,7 @@ public class TruncationCompactionStrategyTests
public async Task CompactAsyncNothingToRemoveReturnsFalseAsync()
{
// Arrange — preserve 5 but only 2 groups
TruncationCompactionStrategy strategy = new(s_alwaysTrigger, preserveRecentGroups: 5);
TruncationCompactionStrategy strategy = new(s_alwaysTrigger, minimumPreserved: 5);
MessageIndex groups = MessageIndex.Create(
[
new ChatMessage(ChatRole.User, "Hello"),
@@ -230,7 +230,7 @@ public class TruncationCompactionStrategyTests
TruncationCompactionStrategy strategy = new(
s_alwaysTrigger,
preserveRecentGroups: 1,
minimumPreserved: 1,
target: targetAfterOne);
MessageIndex groups = MessageIndex.Create(
@@ -258,7 +258,7 @@ public class TruncationCompactionStrategyTests
// Arrange — trigger on groups > 2, target is default (inverse of trigger: groups <= 2)
TruncationCompactionStrategy strategy = new(
CompactionTriggers.GroupsExceed(2),
preserveRecentGroups: 1);
minimumPreserved: 1);
MessageIndex groups = MessageIndex.Create(
[