mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Tuning
This commit is contained in:
@@ -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);
|
||||
|
||||
+47
-17
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+10
-10
@@ -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(
|
||||
[
|
||||
|
||||
+8
-8
@@ -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(
|
||||
|
||||
+13
-13
@@ -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(
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user