Compare commits

...
Author SHA1 Message Date
Chris Rickman bf0f7061eb Merge branch 'copilot/add-preconfigured-compaction-strategy' of https://github.com/microsoft/agent-framework into copilot/add-preconfigured-compaction-strategy 2026-03-27 15:50:32 -07:00
Chris Rickman b14d7dafb9 Add "count" term 2026-03-27 15:49:59 -07:00
ChrisandGitHub 0688b6b186 Merge branch 'main' into copilot/add-preconfigured-compaction-strategy 2026-03-27 15:42:32 -07:00
Chris Rickman 64e529916c Updated 2026-03-27 15:39:04 -07:00
ChrisandGitHub 7d6beea6e5 Merge branch 'main' into copilot/add-preconfigured-compaction-strategy 2026-03-25 14:32:18 -07:00
ChrisandGitHub 1b969b078e Merge branch 'main' into copilot/add-preconfigured-compaction-strategy 2026-03-24 11:57:30 -07:00
ChrisandGitHub a3f11f8cfd Merge branch 'main' into copilot/add-preconfigured-compaction-strategy 2026-03-18 12:46:11 -07:00
ChrisandGitHub 8e598ed8cb Merge branch 'main' into copilot/add-preconfigured-compaction-strategy 2026-03-18 12:05:20 -07:00
Chris Rickman 1dae527377 Comments 2026-03-17 14:13:50 -07:00
3f3bb77243 Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-17 14:10:09 -07:00
d48ddbef06 Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-17 14:09:48 -07:00
ChrisandGitHub 0985a7fb76 Merge branch 'main' into copilot/add-preconfigured-compaction-strategy 2026-03-17 13:56:47 -07:00
Chris Rickman c4c236bbc9 Cleanup 2026-03-17 13:56:21 -07:00
copilot-swe-agent[bot]andcrickman 74e75268bc Add CompactionApproach/CompactionSize enums and CompactionStrategy.Create() factory
Co-authored-by: crickman <66376200+crickman@users.noreply.github.com>
2026-03-12 23:42:13 +00:00
copilot-swe-agent[bot] 579993d165 Initial plan 2026-03-12 23:31:13 +00:00
6 changed files with 481 additions and 1 deletions
@@ -0,0 +1,36 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics.CodeAnalysis;
using Microsoft.Shared.DiagnosticIds;
namespace Microsoft.Agents.AI.Compaction;
#pragma warning disable IDE0001 // Simplify Names for namespace in comments
/// <summary>
/// Describes the compaction approach used by a pre-configured <see cref="CompactionStrategy"/>.
/// </summary>
/// <seealso cref="CompactionStrategy.Create"/>
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
public enum CompactionApproach
{
/// <summary>
/// Applies the lightest available compaction techniques.
/// Collapses old tool call groups into concise summaries and uses truncation as an emergency backstop.
/// </summary>
Gentle,
/// <summary>
/// Balances context preservation with compaction efficiency.
/// Applies tool result collapsing, LLM-based summarization, and truncation as an emergency backstop.
/// </summary>
Balanced,
/// <summary>
/// Applies the most aggressive available compaction techniques.
/// Applies tool result collapsing, LLM-based summarization, turn-based sliding window, and truncation.
/// </summary>
Aggressive,
}
#pragma warning restore IDE0001 // Simplify Names
@@ -0,0 +1,38 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics.CodeAnalysis;
using Microsoft.Shared.DiagnosticIds;
namespace Microsoft.Agents.AI.Compaction;
/// <summary>
/// Describes the context-size profile used by a pre-configured <see cref="CompactionStrategy"/>.
/// </summary>
/// <remarks>
/// 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:
/// <see cref="Compact"/> for smaller context windows, <see cref="Moderate"/> for common mid-range models,
/// and <see cref="Generous"/> for models with large context windows.
/// </remarks>
/// <seealso cref="CompactionStrategy.Create"/>
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
public enum CompactionSize
{
/// <summary>
/// Maintains a smaller context window.
/// Compaction triggers earlier and keeps less history in context.
/// </summary>
Compact,
/// <summary>
/// Maintains a medium-sized context window.
/// This is a reasonable default for most common models.
/// </summary>
Moderate,
/// <summary>
/// Maintains a large context window.
/// Compaction triggers later and retains more history in context.
/// </summary>
Generous,
}
@@ -0,0 +1,171 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.Compaction;
/// <summary>
/// Base class for strategies that compact a <see cref="CompactionMessageIndex"/> to reduce context size.
/// </summary>
/// <remarks>
/// <para>
/// Compaction strategies operate on <see cref="CompactionMessageIndex"/> instances, which organize messages
/// into atomic groups that respect the tool-call/result pairing constraint. Strategies mutate the collection
/// in place by marking groups as excluded, removing groups, or replacing message content (e.g., with summaries).
/// </para>
/// <para>
/// Every strategy requires a <see cref="CompactionTrigger"/> that determines whether compaction should
/// proceed based on current <see cref="CompactionMessageIndex"/> metrics (token count, message count, turn count, etc.).
/// The base class evaluates this trigger at the start of <see cref="CompactAsync"/> and skips compaction when
/// the trigger returns <see langword="false"/>.
/// </para>
/// <para>
/// An optional <b>target</b> condition controls when compaction stops. Strategies incrementally exclude
/// groups and re-evaluate the target after each exclusion, stopping as soon as the target returns
/// <see langword="true"/>. When no target is specified, it defaults to the inverse of the trigger —
/// meaning compaction stops when the trigger condition would no longer fire.
/// </para>
/// <para>
/// Strategies can be applied at three lifecycle points:
/// <list type="bullet">
/// <item><description><b>In-run</b>: During the tool loop, before each LLM call, to keep context within token limits.</description></item>
/// <item><description><b>Pre-write</b>: Before persisting messages to storage via <see cref="ChatHistoryProvider"/>.</description></item>
/// <item><description><b>On existing storage</b>: As a maintenance operation to compact stored history.</description></item>
/// </list>
/// </para>
/// <para>
/// Multiple strategies can be composed by applying them sequentially to the same <see cref="CompactionMessageIndex"/>
/// via <see cref="PipelineCompactionStrategy"/>.
/// </para>
/// </remarks>
public abstract partial class CompactionStrategy
{
/// <summary>
/// Creates a pre-configured <see cref="CompactionStrategy"/> from a combination of
/// <paramref name="approach"/> and <paramref name="size"/>.
/// </summary>
/// <remarks>
/// <para>
/// The <paramref name="approach"/> controls which strategies are included in the pipeline:
/// <list type="bullet">
/// <item><description><see cref="CompactionApproach.Gentle"/>: tool result collapsing + truncation backstop. No <paramref name="chatClient"/> required.</description></item>
/// <item><description><see cref="CompactionApproach.Balanced"/>: tool result collapsing + LLM summarization + truncation backstop.</description></item>
/// <item><description><see cref="CompactionApproach.Aggressive"/>: tool result collapsing + LLM summarization + sliding window + truncation backstop.</description></item>
/// </list>
/// </para>
/// <para>
/// The <paramref name="size"/> controls the token and message thresholds at which each stage triggers.
/// Choose a size that matches the input token limit of your model.
/// </para>
/// </remarks>
/// <param name="approach">
/// The compaction approach that controls which strategy or pipeline to use.
/// </param>
/// <param name="size">
/// The context-size profile that controls token and message thresholds.
/// </param>
/// <param name="chatClient">
/// The <see cref="IChatClient"/> used for LLM-based summarization.
/// </param>
/// <returns>A <see cref="CompactionStrategy"/> configured for the specified approach and size.</returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="chatClient"/> is <see langword="null"/> and <paramref name="approach"/> requires one.
/// </exception>
/// <exception cref="ArgumentOutOfRangeException">
/// <paramref name="approach"/> or <paramref name="size"/> is not a defined enum value.
/// </exception>
public static CompactionStrategy Create(CompactionApproach approach, CompactionSize size, IChatClient chatClient)
{
int tokenLimit = GetTokenCountLimit(size);
int messageLimit = GetMessageCountLimit(size);
return approach switch
{
CompactionApproach.Gentle => CreateGentlePipeline(tokenLimit, messageLimit, chatClient),
CompactionApproach.Balanced => CreateBalancedPipeline(tokenLimit, messageLimit, chatClient),
CompactionApproach.Aggressive => CreateAggressivePipeline(tokenLimit, messageLimit, GetTurnCountLimit(size), chatClient),
_ => throw new ArgumentOutOfRangeException(nameof(approach), approach, null),
};
}
private static int GetTokenCountLimit(CompactionSize size) => size switch
{
CompactionSize.Compact => 0x1FFF, // 8k
CompactionSize.Moderate => 0x7FFF, // 32k
CompactionSize.Generous => 0xFFFF, // 64k
_ => throw new ArgumentOutOfRangeException(nameof(size), size, null),
};
private static int GetMessageCountLimit(CompactionSize size) => size switch
{
CompactionSize.Compact => 50,
CompactionSize.Moderate => 500,
CompactionSize.Generous => 1000,
_ => throw new ArgumentOutOfRangeException(nameof(size), size, null),
};
private static int GetTurnCountLimit(CompactionSize size) => size switch
{
CompactionSize.Compact => 25,
CompactionSize.Moderate => 250,
CompactionSize.Generous => 500,
_ => throw new ArgumentOutOfRangeException(nameof(size), size, null),
};
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)
{
int messageTarget = messageLimit * 3 / 4;
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)),
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 messageTarget = messageLimit * 3 / 4;
int tokenTarget = tokenLimit * 3 / 4;
return new(
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)));
}
}
@@ -47,7 +47,7 @@ namespace Microsoft.Agents.AI.Compaction;
/// </para>
/// </remarks>
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
public abstract class CompactionStrategy
public abstract partial class CompactionStrategy
{
/// <summary>
/// Initializes a new instance of the <see cref="CompactionStrategy"/> class.
@@ -50,6 +50,14 @@ public static class CompactionTriggers
public static CompactionTrigger TokensExceed(int maxTokens) =>
index => index.IncludedTokenCount > maxTokens;
/// <summary>
/// Creates a trigger that fires when the included message count is below the specified maximum.
/// </summary>
/// <param name="maxMessages">The message threshold.</param>
/// <returns>A <see cref="CompactionTrigger"/> that evaluates included message count.</returns>
public static CompactionTrigger MessagesBelow(int maxMessages) =>
index => index.IncludedMessageCount < maxMessages;
/// <summary>
/// Creates a trigger that fires when the included message count exceeds the specified maximum.
/// </summary>
@@ -0,0 +1,227 @@
// 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;
/// <summary>
/// Contains tests for <see cref="CompactionStrategy.Create"/>.
/// </summary>
public class CompactionStrategyCreateTests
{
private static IChatClient CreateMockChatClient()
{
Mock<IChatClient> mock = new();
mock.Setup(c => c.GetResponseAsync(
It.IsAny<IEnumerable<ChatMessage>>(),
It.IsAny<ChatOptions>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new ChatResponse([new ChatMessage(ChatRole.Assistant, "summary")]));
return mock.Object;
}
// ── Gentle ────────────────────────────────────────────────────────────────
[Fact]
public void CreateGentleCompactReturnsTwoStrategyPipeline()
{
PipelineCompactionStrategy pipeline =
Assert.IsType<PipelineCompactionStrategy>(
CompactionStrategy.Create(CompactionApproach.Gentle, CompactionSize.Compact, CreateMockChatClient()));
Assert.Equal(2, pipeline.Strategies.Count);
Assert.IsType<ToolResultCompactionStrategy>(pipeline.Strategies[0]);
Assert.IsType<SummarizationCompactionStrategy>(pipeline.Strategies[1]);
}
[Fact]
public void CreateGentleModerateReturnsTwoStrategyPipeline()
{
PipelineCompactionStrategy pipeline =
Assert.IsType<PipelineCompactionStrategy>(
CompactionStrategy.Create(CompactionApproach.Gentle, CompactionSize.Moderate, CreateMockChatClient()));
Assert.Equal(2, pipeline.Strategies.Count);
Assert.IsType<ToolResultCompactionStrategy>(pipeline.Strategies[0]);
Assert.IsType<SummarizationCompactionStrategy>(pipeline.Strategies[1]);
}
[Fact]
public void CreateGentleGenerousReturnsTwoStrategyPipeline()
{
PipelineCompactionStrategy pipeline =
Assert.IsType<PipelineCompactionStrategy>(
CompactionStrategy.Create(CompactionApproach.Gentle, CompactionSize.Generous, CreateMockChatClient()));
Assert.Equal(2, pipeline.Strategies.Count);
Assert.IsType<ToolResultCompactionStrategy>(pipeline.Strategies[0]);
Assert.IsType<SummarizationCompactionStrategy>(pipeline.Strategies[1]);
}
[Fact]
public void CreateGentleDoesNotRequireChatClient()
{
// No chatClient supplied — should succeed without throwing.
CompactionStrategy strategy = CompactionStrategy.Create(CompactionApproach.Gentle, CompactionSize.Moderate, CreateMockChatClient());
Assert.NotNull(strategy);
}
// ── Balanced ──────────────────────────────────────────────────────────────
[Fact]
public void CreateBalancedCompactReturnsThreeStrategyPipeline()
{
PipelineCompactionStrategy pipeline =
Assert.IsType<PipelineCompactionStrategy>(
CompactionStrategy.Create(CompactionApproach.Balanced, CompactionSize.Compact, CreateMockChatClient()));
Assert.Equal(3, pipeline.Strategies.Count);
Assert.IsType<ToolResultCompactionStrategy>(pipeline.Strategies[0]);
Assert.IsType<SummarizationCompactionStrategy>(pipeline.Strategies[1]);
Assert.IsType<TruncationCompactionStrategy>(pipeline.Strategies[2]);
}
[Fact]
public void CreateBalancedModerateReturnsThreeStrategyPipeline()
{
PipelineCompactionStrategy pipeline =
Assert.IsType<PipelineCompactionStrategy>(
CompactionStrategy.Create(CompactionApproach.Balanced, CompactionSize.Moderate, CreateMockChatClient()));
Assert.Equal(3, pipeline.Strategies.Count);
Assert.IsType<ToolResultCompactionStrategy>(pipeline.Strategies[0]);
Assert.IsType<SummarizationCompactionStrategy>(pipeline.Strategies[1]);
Assert.IsType<TruncationCompactionStrategy>(pipeline.Strategies[2]);
}
[Fact]
public void CreateBalancedGenerousReturnsThreeStrategyPipeline()
{
PipelineCompactionStrategy pipeline =
Assert.IsType<PipelineCompactionStrategy>(
CompactionStrategy.Create(CompactionApproach.Balanced, CompactionSize.Generous, CreateMockChatClient()));
Assert.Equal(3, pipeline.Strategies.Count);
Assert.IsType<ToolResultCompactionStrategy>(pipeline.Strategies[0]);
Assert.IsType<SummarizationCompactionStrategy>(pipeline.Strategies[1]);
Assert.IsType<TruncationCompactionStrategy>(pipeline.Strategies[2]);
}
[Fact]
public void CreateBalancedNullChatClientThrows()
{
Assert.Throws<ArgumentNullException>(
() => CompactionStrategy.Create(CompactionApproach.Balanced, CompactionSize.Moderate, null!));
}
// ── Aggressive ────────────────────────────────────────────────────────────
[Fact]
public void CreateAggressiveCompactReturnsFourStrategyPipeline()
{
PipelineCompactionStrategy pipeline =
Assert.IsType<PipelineCompactionStrategy>(
CompactionStrategy.Create(CompactionApproach.Aggressive, CompactionSize.Compact, CreateMockChatClient()));
Assert.Equal(4, pipeline.Strategies.Count);
Assert.IsType<ToolResultCompactionStrategy>(pipeline.Strategies[0]);
Assert.IsType<SummarizationCompactionStrategy>(pipeline.Strategies[1]);
Assert.IsType<SlidingWindowCompactionStrategy>(pipeline.Strategies[2]);
Assert.IsType<TruncationCompactionStrategy>(pipeline.Strategies[3]);
}
[Fact]
public void CreateAggressiveModerateReturnsFourStrategyPipeline()
{
PipelineCompactionStrategy pipeline =
Assert.IsType<PipelineCompactionStrategy>(
CompactionStrategy.Create(CompactionApproach.Aggressive, CompactionSize.Moderate, CreateMockChatClient()));
Assert.Equal(4, pipeline.Strategies.Count);
Assert.IsType<ToolResultCompactionStrategy>(pipeline.Strategies[0]);
Assert.IsType<SummarizationCompactionStrategy>(pipeline.Strategies[1]);
Assert.IsType<SlidingWindowCompactionStrategy>(pipeline.Strategies[2]);
Assert.IsType<TruncationCompactionStrategy>(pipeline.Strategies[3]);
}
[Fact]
public void CreateAggressiveGenerousReturnsFourStrategyPipeline()
{
PipelineCompactionStrategy pipeline =
Assert.IsType<PipelineCompactionStrategy>(
CompactionStrategy.Create(CompactionApproach.Aggressive, CompactionSize.Generous, CreateMockChatClient()));
Assert.Equal(4, pipeline.Strategies.Count);
Assert.IsType<ToolResultCompactionStrategy>(pipeline.Strategies[0]);
Assert.IsType<SummarizationCompactionStrategy>(pipeline.Strategies[1]);
Assert.IsType<SlidingWindowCompactionStrategy>(pipeline.Strategies[2]);
Assert.IsType<TruncationCompactionStrategy>(pipeline.Strategies[3]);
}
// ── Invalid enum values ───────────────────────────────────────────────────
[Fact]
public void CreateInvalidApproachThrows()
{
Assert.Throws<ArgumentOutOfRangeException>(
() => CompactionStrategy.Create((CompactionApproach)99, CompactionSize.Moderate, CreateMockChatClient()));
}
[Fact]
public void CreateInvalidSizeThrows()
{
Assert.Throws<ArgumentOutOfRangeException>(
() => CompactionStrategy.Create(CompactionApproach.Gentle, (CompactionSize)99, CreateMockChatClient()));
}
// ── Size-threshold behavioral verification ────────────────────────────────
/// <summary>
/// Verifies that <see cref="CompactionSize.Compact"/> and <see cref="CompactionSize.Moderate"/>
/// configure different message thresholds.
/// </summary>
[Fact]
public async Task CreateGentleSizeDifferentiatesMessageThresholdsAsync()
{
// Arrange: 1 tool-call group + 99 (User, Assistant) groups = 100 groups / 200 messages.
// Compact ToolResult triggers at MessagesExceed(50) → 200 > 50 → fires.
// Moderate ToolResult triggers at MessagesExceed(500) → 200 < 500 → does not fire.
//
// 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, CreateMockChatClient());
CompactionStrategy moderatePipeline = CompactionStrategy.Create(CompactionApproach.Gentle, CompactionSize.Moderate, CreateMockChatClient());
List<ChatMessage> messages =
[
// 1 tool-call group (assistant FunctionCall + tool result = 2 messages, 1 group)
new(ChatRole.Assistant, [new FunctionCallContent("c1", "fetch")]),
new(ChatRole.Tool, "data"),
];
for (int index = 0; index < 99; ++index)
{
messages.Add(new(ChatRole.User, $"Q{index}"));
messages.Add(new(ChatRole.Assistant, $"A{index}"));
}
// 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.");
Assert.False(moderateCompacted, "Moderate size should NOT trigger ToolResult compaction.");
}
}