From 74e75268bc3cffd7ca42b9722459ac99204f7954 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 12 Mar 2026 23:42:13 +0000
Subject: [PATCH] Add CompactionApproach/CompactionSize enums and
CompactionStrategy.Create() factory
Co-authored-by: crickman <66376200+crickman@users.noreply.github.com>
---
.../Compaction/CompactionApproach.cs | 35 +++
.../Compaction/CompactionSize.cs | 38 +++
.../Compaction/CompactionStrategy.cs | 114 +++++++++
.../CompactionStrategyCreateTests.cs | 237 ++++++++++++++++++
4 files changed, 424 insertions(+)
create mode 100644 dotnet/src/Microsoft.Agents.AI/Compaction/CompactionApproach.cs
create mode 100644 dotnet/src/Microsoft.Agents.AI/Compaction/CompactionSize.cs
create mode 100644 dotnet/tests/Microsoft.Agents.AI.UnitTests/Compaction/CompactionStrategyCreateTests.cs
diff --git a/dotnet/src/Microsoft.Agents.AI/Compaction/CompactionApproach.cs b/dotnet/src/Microsoft.Agents.AI/Compaction/CompactionApproach.cs
new file mode 100644
index 0000000000..4204a32c58
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI/Compaction/CompactionApproach.cs
@@ -0,0 +1,35 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.Shared.DiagnosticIds;
+
+namespace Microsoft.Agents.AI.Compaction;
+
+///
+/// Describes the compaction approach used by a pre-configured .
+///
+///
+[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
+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/CompactionSize.cs b/dotnet/src/Microsoft.Agents.AI/Compaction/CompactionSize.cs
new file mode 100644
index 0000000000..a71ff63cd2
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI/Compaction/CompactionSize.cs
@@ -0,0 +1,38 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.Shared.DiagnosticIds;
+
+namespace Microsoft.Agents.AI.Compaction;
+
+///
+/// Describes the context-size profile used by a pre-configured .
+///
+///
+/// The size profile controls the token and message thresholds at which compaction triggers.
+/// Choose a size that matches the input token limit of your model:
+/// for smaller context windows, for common mid-range models,
+/// and for models with large context windows.
+///
+///
+[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
+public enum CompactionSize
+{
+ ///
+ /// Targets models with smaller context windows (approximately 4,000 tokens).
+ /// Compaction triggers earlier and keeps less history in context.
+ ///
+ Compact,
+
+ ///
+ /// Targets models with a medium-sized context window (approximately 8,000 tokens).
+ /// This is a reasonable default for most common models.
+ ///
+ Moderate,
+
+ ///
+ /// Targets models with large context windows (approximately 16,000 tokens or more).
+ /// Compaction triggers later and retains more history in context.
+ ///
+ Generous,
+}
diff --git a/dotnet/src/Microsoft.Agents.AI/Compaction/CompactionStrategy.cs b/dotnet/src/Microsoft.Agents.AI/Compaction/CompactionStrategy.cs
index e6f7485438..818908cd8e 100644
--- a/dotnet/src/Microsoft.Agents.AI/Compaction/CompactionStrategy.cs
+++ b/dotnet/src/Microsoft.Agents.AI/Compaction/CompactionStrategy.cs
@@ -5,6 +5,7 @@ using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
+using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Shared.DiagnosticIds;
@@ -161,4 +162,117 @@ public abstract class CompactionStrategy
/// The target value.
/// 0 if negative; otherwise the value
protected static int EnsureNonNegative(int value) => Math.Max(0, value);
+
+ ///
+ /// Creates a pre-configured from a combination of
+ /// and .
+ ///
+ ///
+ ///
+ /// The controls which strategies are included in the pipeline:
+ ///
+ /// - : tool result collapsing + truncation backstop. No required.
+ /// - : tool result collapsing + LLM summarization + truncation backstop.
+ /// - : tool result collapsing + LLM summarization + sliding window + truncation backstop.
+ ///
+ ///
+ ///
+ /// The controls the token and message thresholds at which each stage triggers.
+ /// Choose a size that matches the input token limit of your model.
+ ///
+ ///
+ ///
+ /// 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.
+ ///
+ /// is and requires one.
+ ///
+ ///
+ /// or is not a defined enum value.
+ ///
+ public static CompactionStrategy Create(CompactionApproach approach, CompactionSize size, IChatClient? chatClient = null)
+ {
+ 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!),
+ _ => throw new ArgumentOutOfRangeException(nameof(approach), approach, null),
+ };
+ }
+
+ private static int GetTokenLimit(CompactionSize size) => size switch
+ {
+ CompactionSize.Compact => 4_000,
+ CompactionSize.Moderate => 8_000,
+ CompactionSize.Generous => 16_000,
+ _ => throw new ArgumentOutOfRangeException(nameof(size), size, null),
+ };
+
+ private static int GetMessageLimit(CompactionSize size) => size switch
+ {
+ CompactionSize.Compact => 10,
+ CompactionSize.Moderate => 20,
+ CompactionSize.Generous => 40,
+ _ => throw new ArgumentOutOfRangeException(nameof(size), size, null),
+ };
+
+ private static int GetTurnLimit(CompactionSize size) => size switch
+ {
+ CompactionSize.Compact => 3,
+ CompactionSize.Moderate => 6,
+ CompactionSize.Generous => 12,
+ _ => 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 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;
+
+ return new(
+ new ToolResultCompactionStrategy(CompactionTriggers.MessagesExceed(earlyMessageTrigger)),
+ new SummarizationCompactionStrategy(chatClient, CompactionTriggers.TokensExceed(earlyTokenTrigger)),
+ new TruncationCompactionStrategy(CompactionTriggers.TokensExceed(tokenLimit)));
+ }
+
+ 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;
+
+ return new(
+ new ToolResultCompactionStrategy(CompactionTriggers.MessagesExceed(earlyMessageTrigger)),
+ new SummarizationCompactionStrategy(chatClient, CompactionTriggers.TokensExceed(earlyTokenTrigger)),
+ new SlidingWindowCompactionStrategy(CompactionTriggers.TurnsExceed(turnLimit)),
+ new TruncationCompactionStrategy(CompactionTriggers.TokensExceed(tokenLimit)));
+ }
}
diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Compaction/CompactionStrategyCreateTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Compaction/CompactionStrategyCreateTests.cs
new file mode 100644
index 0000000000..8950c348af
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Compaction/CompactionStrategyCreateTests.cs
@@ -0,0 +1,237 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Agents.AI.Compaction;
+using Microsoft.Extensions.AI;
+using Moq;
+
+namespace Microsoft.Agents.AI.UnitTests.Compaction;
+
+///
+/// Contains tests for .
+///
+public class CompactionStrategyCreateTests
+{
+ private static IChatClient CreateMockChatClient()
+ {
+ Mock mock = new();
+ mock.Setup(c => c.GetResponseAsync(
+ It.IsAny>(),
+ It.IsAny(),
+ It.IsAny()))
+ .ReturnsAsync(new ChatResponse([new ChatMessage(ChatRole.Assistant, "summary")]));
+ return mock.Object;
+ }
+
+ // ── Gentle ────────────────────────────────────────────────────────────────
+
+ [Fact]
+ public void CreateGentleCompactReturnsTwoStrategyPipeline()
+ {
+ PipelineCompactionStrategy pipeline =
+ Assert.IsType(
+ CompactionStrategy.Create(CompactionApproach.Gentle, CompactionSize.Compact));
+
+ Assert.Equal(2, pipeline.Strategies.Count);
+ Assert.IsType(pipeline.Strategies[0]);
+ Assert.IsType(pipeline.Strategies[1]);
+ }
+
+ [Fact]
+ public void CreateGentleModerateReturnsTwoStrategyPipeline()
+ {
+ PipelineCompactionStrategy pipeline =
+ Assert.IsType(
+ CompactionStrategy.Create(CompactionApproach.Gentle, CompactionSize.Moderate));
+
+ Assert.Equal(2, pipeline.Strategies.Count);
+ Assert.IsType(pipeline.Strategies[0]);
+ Assert.IsType(pipeline.Strategies[1]);
+ }
+
+ [Fact]
+ public void CreateGentleGenerousReturnsTwoStrategyPipeline()
+ {
+ PipelineCompactionStrategy pipeline =
+ Assert.IsType(
+ CompactionStrategy.Create(CompactionApproach.Gentle, CompactionSize.Generous));
+
+ Assert.Equal(2, pipeline.Strategies.Count);
+ Assert.IsType(pipeline.Strategies[0]);
+ Assert.IsType(pipeline.Strategies[1]);
+ }
+
+ [Fact]
+ public void CreateGentleDoesNotRequireChatClient()
+ {
+ // No chatClient supplied — should succeed without throwing.
+ CompactionStrategy strategy = CompactionStrategy.Create(CompactionApproach.Gentle, CompactionSize.Moderate);
+ Assert.NotNull(strategy);
+ }
+
+ // ── Balanced ──────────────────────────────────────────────────────────────
+
+ [Fact]
+ public void CreateBalancedCompactReturnsThreeStrategyPipeline()
+ {
+ PipelineCompactionStrategy pipeline =
+ Assert.IsType(
+ CompactionStrategy.Create(CompactionApproach.Balanced, CompactionSize.Compact, CreateMockChatClient()));
+
+ Assert.Equal(3, pipeline.Strategies.Count);
+ Assert.IsType(pipeline.Strategies[0]);
+ Assert.IsType(pipeline.Strategies[1]);
+ Assert.IsType(pipeline.Strategies[2]);
+ }
+
+ [Fact]
+ public void CreateBalancedModerateReturnsThreeStrategyPipeline()
+ {
+ PipelineCompactionStrategy pipeline =
+ Assert.IsType(
+ CompactionStrategy.Create(CompactionApproach.Balanced, CompactionSize.Moderate, CreateMockChatClient()));
+
+ Assert.Equal(3, pipeline.Strategies.Count);
+ Assert.IsType(pipeline.Strategies[0]);
+ Assert.IsType(pipeline.Strategies[1]);
+ Assert.IsType(pipeline.Strategies[2]);
+ }
+
+ [Fact]
+ public void CreateBalancedGenerousReturnsThreeStrategyPipeline()
+ {
+ PipelineCompactionStrategy pipeline =
+ Assert.IsType(
+ CompactionStrategy.Create(CompactionApproach.Balanced, CompactionSize.Generous, CreateMockChatClient()));
+
+ Assert.Equal(3, pipeline.Strategies.Count);
+ Assert.IsType(pipeline.Strategies[0]);
+ Assert.IsType(pipeline.Strategies[1]);
+ Assert.IsType(pipeline.Strategies[2]);
+ }
+
+ [Fact]
+ public void CreateBalancedNullChatClientThrows()
+ {
+ Assert.Throws(
+ () => CompactionStrategy.Create(CompactionApproach.Balanced, CompactionSize.Moderate, null));
+ }
+
+ // ── Aggressive ────────────────────────────────────────────────────────────
+
+ [Fact]
+ public void CreateAggressiveCompactReturnsFourStrategyPipeline()
+ {
+ PipelineCompactionStrategy pipeline =
+ Assert.IsType(
+ CompactionStrategy.Create(CompactionApproach.Aggressive, CompactionSize.Compact, CreateMockChatClient()));
+
+ Assert.Equal(4, pipeline.Strategies.Count);
+ Assert.IsType(pipeline.Strategies[0]);
+ Assert.IsType(pipeline.Strategies[1]);
+ Assert.IsType(pipeline.Strategies[2]);
+ Assert.IsType(pipeline.Strategies[3]);
+ }
+
+ [Fact]
+ public void CreateAggressiveModerateReturnsFourStrategyPipeline()
+ {
+ PipelineCompactionStrategy pipeline =
+ Assert.IsType(
+ CompactionStrategy.Create(CompactionApproach.Aggressive, CompactionSize.Moderate, CreateMockChatClient()));
+
+ Assert.Equal(4, pipeline.Strategies.Count);
+ Assert.IsType(pipeline.Strategies[0]);
+ Assert.IsType(pipeline.Strategies[1]);
+ Assert.IsType(pipeline.Strategies[2]);
+ Assert.IsType(pipeline.Strategies[3]);
+ }
+
+ [Fact]
+ public void CreateAggressiveGenerousReturnsFourStrategyPipeline()
+ {
+ PipelineCompactionStrategy pipeline =
+ Assert.IsType(
+ CompactionStrategy.Create(CompactionApproach.Aggressive, CompactionSize.Generous, CreateMockChatClient()));
+
+ Assert.Equal(4, pipeline.Strategies.Count);
+ Assert.IsType(pipeline.Strategies[0]);
+ Assert.IsType(pipeline.Strategies[1]);
+ Assert.IsType(pipeline.Strategies[2]);
+ 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));
+ }
+
+ [Fact]
+ public void CreateInvalidSizeThrows()
+ {
+ Assert.Throws(
+ () => CompactionStrategy.Create(CompactionApproach.Gentle, (CompactionSize)99));
+ }
+
+ // ── Size-threshold behavioral verification ────────────────────────────────
+
+ ///
+ /// Verifies that and
+ /// configure different message thresholds: 18 messages (17 groups) should trigger the Compact
+ /// ToolResultCompaction stage (threshold 10) but NOT the Moderate one (threshold 20).
+ ///
+ [Fact]
+ public async Task CreateGentleSizeDifferentiatesMessageThresholdsAsync()
+ {
+ // Arrange: 17 groups = 1 ToolCall + 8 User + 8 AssistantText = 18 messages.
+ // Compact ToolResult triggers at MessagesExceed(10) → 18 > 10 → fires.
+ // Moderate ToolResult triggers at MessagesExceed(20) → 18 < 20 → does not fire.
+ //
+ // With minimumPreservedGroups=16 and 17 total groups the oldest 1 group (the tool-call
+ // group) is eligible for collapsing, making the behavioral difference observable.
+ CompactionStrategy compactPipeline = CompactionStrategy.Create(CompactionApproach.Gentle, CompactionSize.Compact);
+ CompactionStrategy moderatePipeline = CompactionStrategy.Create(CompactionApproach.Gentle, CompactionSize.Moderate);
+
+ List messages =
+ [
+ // 1 tool-call group (assistant FunctionCall + tool result = 2 messages, 1 group)
+ new(ChatRole.Assistant, [new FunctionCallContent("c1", "fetch")]),
+ new(ChatRole.Tool, "data"),
+ // 8 user/assistant pairs = 16 messages, 16 groups
+ new(ChatRole.User, "Q1"), new(ChatRole.Assistant, "A1"),
+ new(ChatRole.User, "Q2"), new(ChatRole.Assistant, "A2"),
+ new(ChatRole.User, "Q3"), new(ChatRole.Assistant, "A3"),
+ new(ChatRole.User, "Q4"), new(ChatRole.Assistant, "A4"),
+ new(ChatRole.User, "Q5"), new(ChatRole.Assistant, "A5"),
+ new(ChatRole.User, "Q6"), new(ChatRole.Assistant, "A6"),
+ new(ChatRole.User, "Q7"), new(ChatRole.Assistant, "A7"),
+ new(ChatRole.User, "Q8"), new(ChatRole.Assistant, "A8"),
+ ];
+
+ // Two separate indexes so strategies run independently.
+ CompactionMessageIndex compactIndex = CompactionMessageIndex.Create(messages);
+ CompactionMessageIndex moderateIndex = CompactionMessageIndex.Create(messages);
+
+ // Act
+ bool compactCompacted = await compactPipeline.CompactAsync(compactIndex);
+ bool moderateCompacted = await moderatePipeline.CompactAsync(moderateIndex);
+
+ // Assert
+ Assert.True(compactCompacted, "Compact size should trigger ToolResult compaction at 18 messages (> threshold of 10).");
+ Assert.False(moderateCompacted, "Moderate size should NOT trigger ToolResult compaction at 18 messages (< threshold of 20).");
+ }
+}