diff --git a/dotnet/src/Microsoft.Agents.AI/Compaction/CompactionApproach.cs b/dotnet/src/Microsoft.Agents.AI/Compaction/CompactionApproach.cs
index 1b9a23cc55..cfbab7bd41 100644
--- a/dotnet/src/Microsoft.Agents.AI/Compaction/CompactionApproach.cs
+++ b/dotnet/src/Microsoft.Agents.AI/Compaction/CompactionApproach.cs
@@ -17,21 +17,18 @@ public enum CompactionApproach
///
/// Applies the lightest available compaction techniques.
/// Collapses old tool call groups into concise summaries and uses truncation as an emergency backstop.
- /// Does not require a summarization .
///
Gentle,
///
/// Balances context preservation with compaction efficiency.
/// Applies tool result collapsing, LLM-based summarization, and truncation as an emergency backstop.
- /// Requires a summarization .
///
Balanced,
///
/// Applies the most aggressive available compaction techniques.
/// Applies tool result collapsing, LLM-based summarization, turn-based sliding window, and truncation.
- /// Requires a summarization .
///
Aggressive,
}
diff --git a/dotnet/src/Microsoft.Agents.AI/Compaction/CompactionStrategy.Factory.cs b/dotnet/src/Microsoft.Agents.AI/Compaction/CompactionStrategy.Factory.cs
index 788cfd39ad..2a63e0e19f 100644
--- a/dotnet/src/Microsoft.Agents.AI/Compaction/CompactionStrategy.Factory.cs
+++ b/dotnet/src/Microsoft.Agents.AI/Compaction/CompactionStrategy.Factory.cs
@@ -2,7 +2,6 @@
using System;
using Microsoft.Extensions.AI;
-using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI.Compaction;
@@ -62,16 +61,12 @@ public abstract partial class CompactionStrategy
///
///
/// The compaction approach that controls which strategy or pipeline to use.
- /// does not require a ;
- /// and require one.
///
///
/// The context-size profile that controls token and message thresholds.
///
///
/// The used for LLM-based summarization.
- /// Required when is or
- /// ; ignored for .
///
/// A configured for the specified approach and size.
///
@@ -80,21 +75,16 @@ public abstract partial class CompactionStrategy
///
/// or is not a defined enum value.
///
- public static CompactionStrategy Create(CompactionApproach approach, CompactionSize size, IChatClient? chatClient = null)
+ public static CompactionStrategy Create(CompactionApproach approach, CompactionSize size, IChatClient chatClient)
{
- if (approach is CompactionApproach.Balanced or CompactionApproach.Aggressive)
- {
- _ = Throw.IfNull(chatClient);
- }
-
int tokenLimit = GetTokenLimit(size);
int messageLimit = GetMessageLimit(size);
return approach switch
{
- CompactionApproach.Gentle => CreateGentlePipeline(tokenLimit, messageLimit),
- CompactionApproach.Balanced => CreateBalancedPipeline(tokenLimit, messageLimit, chatClient!),
- CompactionApproach.Aggressive => CreateAggressivePipeline(tokenLimit, messageLimit, GetTurnLimit(size), chatClient!),
+ CompactionApproach.Gentle => CreateGentlePipeline(tokenLimit, messageLimit, chatClient),
+ CompactionApproach.Balanced => CreateBalancedPipeline(tokenLimit, messageLimit, chatClient),
+ CompactionApproach.Aggressive => CreateAggressivePipeline(tokenLimit, messageLimit, GetTurnLimit(size), chatClient),
_ => throw new ArgumentOutOfRangeException(nameof(approach), approach, null),
};
}
@@ -123,35 +113,59 @@ public abstract partial class CompactionStrategy
_ => throw new ArgumentOutOfRangeException(nameof(size), size, null),
};
- private static PipelineCompactionStrategy CreateGentlePipeline(int tokenLimit, int messageLimit) =>
- new(
- new ToolResultCompactionStrategy(CompactionTriggers.MessagesExceed(messageLimit)),
- new TruncationCompactionStrategy(CompactionTriggers.TokensExceed(tokenLimit)));
+ private static PipelineCompactionStrategy CreateGentlePipeline(int tokenLimit, int messageLimit, IChatClient chatClient)
+ {
+ int messageTarget = messageLimit * 4 / 5;
+ int tokenTarget = tokenLimit * 4 / 5;
+
+ return new(
+ new ToolResultCompactionStrategy(
+ trigger: CompactionTriggers.MessagesExceed(messageLimit),
+ target: CompactionTriggers.MessagesBelow(messageTarget)),
+ new SummarizationCompactionStrategy(
+ chatClient,
+ trigger: CompactionTriggers.TokensExceed(tokenLimit),
+ target: CompactionTriggers.TokensBelow(tokenTarget)));
+ }
private static PipelineCompactionStrategy CreateBalancedPipeline(int tokenLimit, int messageLimit, IChatClient chatClient)
{
- // Early stages trigger at two-thirds of the limit so the pipeline has room to compact
- // incrementally before reaching the emergency truncation backstop at the full limit.
- int earlyMessageTrigger = messageLimit * 2 / 3;
- int earlyTokenTrigger = tokenLimit * 2 / 3;
+ int messageTarget = messageLimit * 3 / 4;
+ int tokenTarget = tokenLimit * 4 / 5;
return new(
- new ToolResultCompactionStrategy(CompactionTriggers.MessagesExceed(earlyMessageTrigger)),
- new SummarizationCompactionStrategy(chatClient, CompactionTriggers.TokensExceed(earlyTokenTrigger)),
- new TruncationCompactionStrategy(CompactionTriggers.TokensExceed(tokenLimit)));
+ new ToolResultCompactionStrategy(
+ trigger: CompactionTriggers.MessagesExceed(messageLimit),
+ target: CompactionTriggers.MessagesBelow(messageTarget)),
+ new SummarizationCompactionStrategy(
+ chatClient,
+ trigger: CompactionTriggers.TokensExceed(tokenLimit),
+ target: CompactionTriggers.TokensBelow(tokenTarget)),
+ new TruncationCompactionStrategy(
+ trigger: CompactionTriggers.TokensExceed(tokenLimit),
+ target: CompactionTriggers.TokensBelow(tokenTarget)));
}
private static PipelineCompactionStrategy CreateAggressivePipeline(int tokenLimit, int messageLimit, int turnLimit, IChatClient chatClient)
{
// Early stages trigger at half the limit so compaction kicks in sooner and
// the sliding window and truncation backstop are reached less often.
- int earlyMessageTrigger = messageLimit / 2;
- int earlyTokenTrigger = tokenLimit / 2;
+ int messageTarget = messageLimit * 3 / 4;
+ int tokenTarget = tokenLimit * 3 / 4;
return new(
- new ToolResultCompactionStrategy(CompactionTriggers.MessagesExceed(earlyMessageTrigger)),
- new SummarizationCompactionStrategy(chatClient, CompactionTriggers.TokensExceed(earlyTokenTrigger)),
- new SlidingWindowCompactionStrategy(CompactionTriggers.TurnsExceed(turnLimit)),
- new TruncationCompactionStrategy(CompactionTriggers.TokensExceed(tokenLimit)));
+ new ToolResultCompactionStrategy(
+ trigger: CompactionTriggers.MessagesExceed(messageLimit),
+ target: CompactionTriggers.MessagesBelow(messageTarget)),
+ new SummarizationCompactionStrategy(
+ chatClient,
+ trigger: CompactionTriggers.TokensExceed(tokenLimit),
+ target: CompactionTriggers.TokensBelow(tokenTarget)),
+ new SlidingWindowCompactionStrategy(
+ trigger: CompactionTriggers.TokensExceed(tokenLimit),
+ target: CompactionTriggers.TokensBelow(tokenTarget)),
+ new TruncationCompactionStrategy(
+ trigger: CompactionTriggers.TokensExceed(tokenLimit),
+ target: CompactionTriggers.TokensBelow(tokenTarget)));
}
}
diff --git a/dotnet/src/Microsoft.Agents.AI/Compaction/CompactionTriggers.cs b/dotnet/src/Microsoft.Agents.AI/Compaction/CompactionTriggers.cs
index a2bc398ac3..9350b581af 100644
--- a/dotnet/src/Microsoft.Agents.AI/Compaction/CompactionTriggers.cs
+++ b/dotnet/src/Microsoft.Agents.AI/Compaction/CompactionTriggers.cs
@@ -50,6 +50,14 @@ public static class CompactionTriggers
public static CompactionTrigger TokensExceed(int maxTokens) =>
index => index.IncludedTokenCount > maxTokens;
+ ///
+ /// Creates a trigger that fires when the included message count is below the specified maximum.
+ ///
+ /// The message threshold.
+ /// A that evaluates included message count.
+ public static CompactionTrigger MessagesBelow(int maxMessages) =>
+ index => index.IncludedMessageCount < maxMessages;
+
///
/// Creates a trigger that fires when the included message count exceeds the specified maximum.
///
diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Compaction/CompactionStrategyCreateTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Compaction/CompactionStrategyCreateTests.cs
index 3ef88173e3..992b4ac2c0 100644
--- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Compaction/CompactionStrategyCreateTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Compaction/CompactionStrategyCreateTests.cs
@@ -33,11 +33,11 @@ public class CompactionStrategyCreateTests
{
PipelineCompactionStrategy pipeline =
Assert.IsType(
- CompactionStrategy.Create(CompactionApproach.Gentle, CompactionSize.Compact));
+ CompactionStrategy.Create(CompactionApproach.Gentle, CompactionSize.Compact, CreateMockChatClient()));
Assert.Equal(2, pipeline.Strategies.Count);
Assert.IsType(pipeline.Strategies[0]);
- Assert.IsType(pipeline.Strategies[1]);
+ Assert.IsType(pipeline.Strategies[1]);
}
[Fact]
@@ -45,11 +45,11 @@ public class CompactionStrategyCreateTests
{
PipelineCompactionStrategy pipeline =
Assert.IsType(
- CompactionStrategy.Create(CompactionApproach.Gentle, CompactionSize.Moderate));
+ CompactionStrategy.Create(CompactionApproach.Gentle, CompactionSize.Moderate, CreateMockChatClient()));
Assert.Equal(2, pipeline.Strategies.Count);
Assert.IsType(pipeline.Strategies[0]);
- Assert.IsType(pipeline.Strategies[1]);
+ Assert.IsType(pipeline.Strategies[1]);
}
[Fact]
@@ -57,18 +57,18 @@ public class CompactionStrategyCreateTests
{
PipelineCompactionStrategy pipeline =
Assert.IsType(
- CompactionStrategy.Create(CompactionApproach.Gentle, CompactionSize.Generous));
+ CompactionStrategy.Create(CompactionApproach.Gentle, CompactionSize.Generous, CreateMockChatClient()));
Assert.Equal(2, pipeline.Strategies.Count);
Assert.IsType(pipeline.Strategies[0]);
- Assert.IsType(pipeline.Strategies[1]);
+ Assert.IsType(pipeline.Strategies[1]);
}
[Fact]
public void CreateGentleDoesNotRequireChatClient()
{
// No chatClient supplied — should succeed without throwing.
- CompactionStrategy strategy = CompactionStrategy.Create(CompactionApproach.Gentle, CompactionSize.Moderate);
+ CompactionStrategy strategy = CompactionStrategy.Create(CompactionApproach.Gentle, CompactionSize.Moderate, CreateMockChatClient());
Assert.NotNull(strategy);
}
@@ -117,7 +117,7 @@ public class CompactionStrategyCreateTests
public void CreateBalancedNullChatClientThrows()
{
Assert.Throws(
- () => CompactionStrategy.Create(CompactionApproach.Balanced, CompactionSize.Moderate, null));
+ () => CompactionStrategy.Create(CompactionApproach.Balanced, CompactionSize.Moderate, null!));
}
// ── Aggressive ────────────────────────────────────────────────────────────
@@ -164,27 +164,20 @@ public class CompactionStrategyCreateTests
Assert.IsType(pipeline.Strategies[3]);
}
- [Fact]
- public void CreateAggressiveNullChatClientThrows()
- {
- Assert.Throws(
- () => CompactionStrategy.Create(CompactionApproach.Aggressive, CompactionSize.Moderate, null));
- }
-
// ── Invalid enum values ───────────────────────────────────────────────────
[Fact]
public void CreateInvalidApproachThrows()
{
Assert.Throws(
- () => CompactionStrategy.Create((CompactionApproach)99, CompactionSize.Moderate));
+ () => CompactionStrategy.Create((CompactionApproach)99, CompactionSize.Moderate, CreateMockChatClient()));
}
[Fact]
public void CreateInvalidSizeThrows()
{
Assert.Throws(
- () => CompactionStrategy.Create(CompactionApproach.Gentle, (CompactionSize)99));
+ () => CompactionStrategy.Create(CompactionApproach.Gentle, (CompactionSize)99, CreateMockChatClient()));
}
// ── Size-threshold behavioral verification ────────────────────────────────
@@ -203,8 +196,8 @@ public class CompactionStrategyCreateTests
// The configuration preserves a number of most-recent groups, leaving the oldest
// tool-call group eligible for collapsing, which makes the behavioral difference
// between Compact and Moderate sizes observable in this test.
- CompactionStrategy compactPipeline = CompactionStrategy.Create(CompactionApproach.Gentle, CompactionSize.Compact);
- CompactionStrategy moderatePipeline = CompactionStrategy.Create(CompactionApproach.Gentle, CompactionSize.Moderate);
+ CompactionStrategy compactPipeline = CompactionStrategy.Create(CompactionApproach.Gentle, CompactionSize.Compact, CreateMockChatClient());
+ CompactionStrategy moderatePipeline = CompactionStrategy.Create(CompactionApproach.Gentle, CompactionSize.Moderate, CreateMockChatClient());
List messages =
[