// Copyright (c) Microsoft. All rights reserved. using System.Threading; using System.Threading.Tasks; 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. /// /// 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 minimumPreserved = DefaultMinimumPreserved, CompactionTrigger? target = null) : base(trigger, target) { this.MinimumPreserved = minimumPreserved; } /// /// 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 MinimumPreserved { get; } /// protected override Task ApplyCompactionAsync(MessageIndex index, CancellationToken cancellationToken) { // Count removable (non-system, non-excluded) groups int removableCount = 0; for (int i = 0; i < index.Groups.Count; i++) { MessageGroup group = index.Groups[i]; if (!group.IsExcluded && group.Kind != MessageGroupKind.System) { removableCount++; } } int maxRemovable = removableCount - this.MinimumPreserved; if (maxRemovable <= 0) { return Task.FromResult(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++) { MessageGroup group = index.Groups[i]; if (group.IsExcluded || group.Kind == MessageGroupKind.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 Task.FromResult(compacted); } }