// Copyright (c) Microsoft. All rights reserved. using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Shared.DiagnosticIds; namespace Microsoft.Agents.AI.Compaction; /// /// A compaction strategy that removes the oldest non-system message groups, /// keeping at least most-recent groups intact. /// /// /// /// This strategy preserves system messages and removes the oldest non-system message groups first. /// It respects atomic group boundaries — an assistant message with tool calls and its /// corresponding tool result messages are always removed together. /// /// /// is a hard floor: even if the /// has not been reached, compaction will not touch the last non-system groups. /// /// /// The controls when compaction proceeds. /// Use for common trigger conditions such as token or group thresholds. /// /// [Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] public sealed class TruncationCompactionStrategy : CompactionStrategy { /// /// The default minimum number of most-recent non-system groups to preserve. /// public const int DefaultMinimumPreserved = 32; /// /// Initializes a new instance of the class. /// /// /// The that controls when compaction proceeds. /// /// /// 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. /// /// /// An optional target condition that controls when compaction stops. When , /// defaults to the inverse of the — compaction stops as soon as the trigger would no longer fire. /// public TruncationCompactionStrategy(CompactionTrigger trigger, int minimumPreservedGroups = DefaultMinimumPreserved, CompactionTrigger? target = null) : base(trigger, target) { this.MinimumPreservedGroups = EnsureNonNegative(minimumPreservedGroups); } /// /// 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. /// public int MinimumPreservedGroups { get; } /// protected override ValueTask CompactCoreAsync(CompactionMessageIndex index, ILogger logger, CancellationToken cancellationToken) { // Count removable (non-system, non-excluded) groups int removableCount = 0; for (int i = 0; i < index.Groups.Count; i++) { CompactionMessageGroup group = index.Groups[i]; if (!group.IsExcluded && group.Kind != CompactionGroupKind.System) { removableCount++; } } int maxRemovable = removableCount - this.MinimumPreservedGroups; if (maxRemovable <= 0) { return new ValueTask(false); } // Exclude oldest non-system groups one at a time, re-checking target after each bool compacted = false; int removed = 0; for (int i = 0; i < index.Groups.Count && removed < maxRemovable; i++) { CompactionMessageGroup group = index.Groups[i]; if (group.IsExcluded || group.Kind == CompactionGroupKind.System) { continue; } group.IsExcluded = true; group.ExcludeReason = $"Truncated by {nameof(TruncationCompactionStrategy)}"; removed++; compacted = true; // Stop when target condition is met if (this.Target(index)) { break; } } return new ValueTask(compacted); } }