// Copyright (c) Microsoft. All rights reserved. using System; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Shared.DiagnosticIds; 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 . /// /// [Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] public abstract class CompactionStrategy { /// /// Initializes a new instance of the class. /// /// /// The that determines whether compaction should proceed. /// /// /// An optional target condition that controls when compaction stops. Strategies re-evaluate /// this predicate after each incremental exclusion and stop when it returns . /// When , defaults to the inverse of the — compaction /// stops as soon as the trigger condition would no longer fire. /// protected CompactionStrategy(CompactionTrigger trigger, CompactionTrigger? target = null) { this.Trigger = Throw.IfNull(trigger); this.Target = target ?? (index => !trigger(index)); } /// /// Gets the trigger predicate that controls when compaction proceeds. /// protected CompactionTrigger Trigger { get; } /// /// Gets the target predicate that controls when compaction stops. /// Strategies re-evaluate this after each incremental exclusion and stop when it returns . /// protected CompactionTrigger Target { get; } /// /// Applies the strategy-specific compaction logic to the specified message index. /// /// /// This method is called by only when the /// returns . Implementations do not need to evaluate the trigger or /// report metrics — the base class handles both. Implementations should use /// to determine when to stop compacting incrementally. /// /// The message index to compact. The strategy mutates this collection in place. /// The for emitting compaction diagnostics. /// The to monitor for cancellation requests. /// A task whose result is if any compaction was performed, otherwise. protected abstract ValueTask CompactCoreAsync(CompactionMessageIndex index, ILogger logger, CancellationToken cancellationToken); /// /// Evaluates the and, when it fires, delegates to /// and reports compaction metrics. /// /// The message index to compact. The strategy mutates this collection in place. /// An optional for emitting compaction diagnostics. When , logging is disabled. /// The to monitor for cancellation requests. /// A task representing the asynchronous operation. The task result is if compaction occurred, otherwise. public async ValueTask CompactAsync(CompactionMessageIndex index, ILogger? logger = null, CancellationToken cancellationToken = default) { string strategyName = this.GetType().Name; logger ??= NullLogger.Instance; using Activity? activity = CompactionTelemetry.ActivitySource.StartActivity(CompactionTelemetry.ActivityNames.Compact); activity?.SetTag(CompactionTelemetry.Tags.Strategy, strategyName); if (index.IncludedNonSystemGroupCount <= 1 || !this.Trigger(index)) { activity?.SetTag(CompactionTelemetry.Tags.Triggered, false); logger.LogCompactionSkipped(strategyName); return false; } activity?.SetTag(CompactionTelemetry.Tags.Triggered, true); int beforeTokens = index.IncludedTokenCount; int beforeGroups = index.IncludedGroupCount; int beforeMessages = index.IncludedMessageCount; Stopwatch stopwatch = Stopwatch.StartNew(); bool compacted = await this.CompactCoreAsync(index, logger, cancellationToken).ConfigureAwait(false); stopwatch.Stop(); activity?.SetTag(CompactionTelemetry.Tags.Compacted, compacted); if (compacted) { activity? .SetTag(CompactionTelemetry.Tags.BeforeTokens, beforeTokens) .SetTag(CompactionTelemetry.Tags.AfterTokens, index.IncludedTokenCount) .SetTag(CompactionTelemetry.Tags.BeforeMessages, beforeMessages) .SetTag(CompactionTelemetry.Tags.AfterMessages, index.IncludedMessageCount) .SetTag(CompactionTelemetry.Tags.BeforeGroups, beforeGroups) .SetTag(CompactionTelemetry.Tags.AfterGroups, index.IncludedGroupCount) .SetTag(CompactionTelemetry.Tags.DurationMs, stopwatch.ElapsedMilliseconds); logger.LogCompactionCompleted( strategyName, stopwatch.ElapsedMilliseconds, beforeMessages, index.IncludedMessageCount, beforeGroups, index.IncludedGroupCount, beforeTokens, index.IncludedTokenCount); } return compacted; } /// /// Ensures the provided value is not a negative number. /// /// The target value. /// 0 if negative; otherwise the value protected static int EnsureNonNegative(int value) => Math.Max(0, value); }