From c4c236bbc9d2fb2276ef1fc9cf7bd15a6f155742 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 17 Mar 2026 13:56:21 -0700 Subject: [PATCH] Cleanup --- .../Compaction/CompactionApproach.cs | 4 + .../Compaction/CompactionSize.cs | 6 +- .../Compaction/CompactionStrategy.Factory.cs | 157 ++++++++++++++++++ .../Compaction/CompactionStrategy.cs | 116 +------------ .../CompactionStrategyCreateTests.cs | 15 +- 5 files changed, 171 insertions(+), 127 deletions(-) create mode 100644 dotnet/src/Microsoft.Agents.AI/Compaction/CompactionStrategy.Factory.cs diff --git a/dotnet/src/Microsoft.Agents.AI/Compaction/CompactionApproach.cs b/dotnet/src/Microsoft.Agents.AI/Compaction/CompactionApproach.cs index 4204a32c58..1b9a23cc55 100644 --- a/dotnet/src/Microsoft.Agents.AI/Compaction/CompactionApproach.cs +++ b/dotnet/src/Microsoft.Agents.AI/Compaction/CompactionApproach.cs @@ -5,6 +5,8 @@ using Microsoft.Shared.DiagnosticIds; namespace Microsoft.Agents.AI.Compaction; +#pragma warning disable IDE0001 // Simplify Names for namespace in comments + /// /// Describes the compaction approach used by a pre-configured . /// @@ -33,3 +35,5 @@ public enum CompactionApproach /// Aggressive, } + +#pragma warning restore IDE0001 // Simplify Names diff --git a/dotnet/src/Microsoft.Agents.AI/Compaction/CompactionSize.cs b/dotnet/src/Microsoft.Agents.AI/Compaction/CompactionSize.cs index a71ff63cd2..16e701c98e 100644 --- a/dotnet/src/Microsoft.Agents.AI/Compaction/CompactionSize.cs +++ b/dotnet/src/Microsoft.Agents.AI/Compaction/CompactionSize.cs @@ -19,19 +19,19 @@ namespace Microsoft.Agents.AI.Compaction; public enum CompactionSize { /// - /// Targets models with smaller context windows (approximately 4,000 tokens). + /// Maintains a smaller context windows. /// Compaction triggers earlier and keeps less history in context. /// Compact, /// - /// Targets models with a medium-sized context window (approximately 8,000 tokens). + /// Maintains a a medium-sized context window. /// This is a reasonable default for most common models. /// Moderate, /// - /// Targets models with large context windows (approximately 16,000 tokens or more). + /// Maintains a large context windows. /// Compaction triggers later and retains more history in context. /// Generous, diff --git a/dotnet/src/Microsoft.Agents.AI/Compaction/CompactionStrategy.Factory.cs b/dotnet/src/Microsoft.Agents.AI/Compaction/CompactionStrategy.Factory.cs new file mode 100644 index 0000000000..788cfd39ad --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI/Compaction/CompactionStrategy.Factory.cs @@ -0,0 +1,157 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using Microsoft.Extensions.AI; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Agents.AI.Compaction; + +/// +/// Base class for strategies that compact a to reduce context size. +/// +/// +/// +/// Compaction strategies operate on 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). +/// +/// +/// Every strategy requires a that determines whether compaction should +/// proceed based on current metrics (token count, message count, turn count, etc.). +/// The base class evaluates this trigger at the start of and skips compaction when +/// the trigger returns . +/// +/// +/// An optional target condition controls when compaction stops. Strategies incrementally exclude +/// groups and re-evaluate the target after each exclusion, stopping as soon as the target returns +/// . When no target is specified, it defaults to the inverse of the trigger — +/// meaning compaction stops when the trigger condition would no longer fire. +/// +/// +/// Strategies can be applied at three lifecycle points: +/// +/// In-run: During the tool loop, before each LLM call, to keep context within token limits. +/// Pre-write: Before persisting messages to storage via . +/// On existing storage: As a maintenance operation to compact stored history. +/// +/// +/// +/// Multiple strategies can be composed by applying them sequentially to the same +/// via . +/// +/// +public abstract partial class CompactionStrategy +{ + /// + /// 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 => 0x1FFF, // 8k + CompactionSize.Moderate => 0x7FFF, // 32k + CompactionSize.Generous => 0xFFFF, // 64k + _ => throw new ArgumentOutOfRangeException(nameof(size), size, null), + }; + + private static int GetMessageLimit(CompactionSize size) => size switch + { + CompactionSize.Compact => 50, + CompactionSize.Moderate => 500, + CompactionSize.Generous => 1000, + _ => throw new ArgumentOutOfRangeException(nameof(size), size, null), + }; + + private static int GetTurnLimit(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) => + 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/src/Microsoft.Agents.AI/Compaction/CompactionStrategy.cs b/dotnet/src/Microsoft.Agents.AI/Compaction/CompactionStrategy.cs index 818908cd8e..eeaab3dfb3 100644 --- a/dotnet/src/Microsoft.Agents.AI/Compaction/CompactionStrategy.cs +++ b/dotnet/src/Microsoft.Agents.AI/Compaction/CompactionStrategy.cs @@ -5,7 +5,6 @@ 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; @@ -48,7 +47,7 @@ namespace Microsoft.Agents.AI.Compaction; /// /// [Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] -public abstract class CompactionStrategy +public abstract partial class CompactionStrategy { /// /// Initializes a new instance of the class. @@ -162,117 +161,4 @@ 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 index 8950c348af..aa54dd00ca 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Compaction/CompactionStrategyCreateTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Compaction/CompactionStrategyCreateTests.cs @@ -211,17 +211,14 @@ public class CompactionStrategyCreateTests // 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"), ]; + 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);