// Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.AI; namespace Microsoft.Agents.AI.Compaction; /// /// A compaction strategy that removes the oldest message groups until the estimated /// token count is within a specified budget. /// /// /// /// 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. /// /// /// The trigger condition fires only when the current token count exceeds maxTokens. /// /// public class TruncationCompactionStrategy : ChatHistoryCompactionStrategy { private readonly int _maxTokens; /// /// Initializes a new instance of the class. /// /// The maximum token budget. Groups are removed until the token count is at or below this value. /// /// The minimum number of most-recent non-system message groups to keep. /// Defaults to 1 so that at least the latest exchange is always preserved. /// public TruncationCompactionStrategy(int maxTokens, int preserveRecentGroups = 1) : base(new TruncationReducer(preserveRecentGroups)) { this._maxTokens = maxTokens; } /// protected override bool ShouldCompact(ChatHistoryMetric metrics) => metrics.TokenCount > this._maxTokens; /// /// An that removes the oldest non-system message groups, /// keeping at least the most recent group. /// private sealed class TruncationReducer(int preserveRecentGroups) : IChatReducer { public Task> ReduceAsync( IEnumerable messages, CancellationToken cancellationToken = default) { IReadOnlyList messageList = [.. messages]; ChatMessageGroup[] removableGroups = [.. CurrentMetrics.Groups.Where(g => g.Kind != ChatMessageGroupKind.System)]; if (removableGroups.Length == 0) { return Task.FromResult>(messageList); } // Remove oldest non-system groups, keeping at least preserveRecentGroups. int maxRemovable = removableGroups.Length - preserveRecentGroups; if (maxRemovable <= 0) { return Task.FromResult>(messageList); } HashSet removedGroupStarts = []; for (int ri = 0; ri < maxRemovable; ri++) { removedGroupStarts.Add(removableGroups[ri].StartIndex); } List messagesToKeep = new(messageList.Count); foreach (ChatMessageGroup group in CurrentMetrics.Groups) { if (removedGroupStarts.Contains(group.StartIndex)) { continue; } for (int j = group.StartIndex; j < group.StartIndex + group.Count; j++) { messagesToKeep.Add(messageList[j]); } } return Task.FromResult>(messagesToKeep); } } }