// Copyright (c) Microsoft. All rights reserved. using System; using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Shared.DiagnosticIds; using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.AI.Compaction; /// /// A compaction strategy that derives token thresholds from a model's context window size /// and maximum output tokens, applying a two-phase compaction pipeline: /// /// Tool result eviction () — collapses old tool call groups /// into concise summaries when the token count exceeds the . /// Truncation () — removes the oldest non-system message groups /// when the token count exceeds the . /// /// /// /// /// The input budget is defined as maxContextWindowTokens - maxOutputTokens, representing /// the maximum number of tokens available for the conversation input (including system messages, tools, and history). /// /// /// This strategy is a convenience wrapper around that automates /// threshold calculation from model specifications. /// /// [Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] public sealed class ContextWindowCompactionStrategy : CompactionStrategy { /// /// The default fraction of the input budget at which tool result eviction triggers. /// public const double DefaultToolEvictionThreshold = 0.5; /// /// The default fraction of the input budget at which truncation triggers. /// public const double DefaultTruncationThreshold = 0.8; private readonly PipelineCompactionStrategy _pipeline; /// /// Initializes a new instance of the class. /// /// /// The maximum number of tokens the model's context window supports (e.g., 1,050,000 for gpt-5.4). /// /// /// The maximum number of output tokens the model can generate per response (e.g., 128,000 for gpt-5.4). /// /// /// The fraction of the input budget (0.0, 1.0] at which tool result eviction triggers. /// Defaults to (0.5). /// /// /// The fraction of the input budget (0.0, 1.0] at which truncation triggers. /// Defaults to (0.8). /// Must be greater than or equal to . /// /// /// is not positive, or /// is negative or greater than or equal to , or /// or is not in (0.0, 1.0], or /// is less than . /// public ContextWindowCompactionStrategy( int maxContextWindowTokens, int maxOutputTokens, double toolEvictionThreshold = DefaultToolEvictionThreshold, double truncationThreshold = DefaultTruncationThreshold) : base(CompactionTriggers.Always) { Throw.IfLessThanOrEqual(maxContextWindowTokens, 0); Throw.IfLessThan(maxOutputTokens, 0); Throw.IfGreaterThanOrEqual(maxOutputTokens, maxContextWindowTokens); ValidateThreshold(toolEvictionThreshold, nameof(toolEvictionThreshold)); ValidateThreshold(truncationThreshold, nameof(truncationThreshold)); if (truncationThreshold < toolEvictionThreshold) { throw new ArgumentOutOfRangeException(nameof(truncationThreshold), truncationThreshold, $"Truncation threshold ({truncationThreshold}) must be greater than or equal to tool eviction threshold ({toolEvictionThreshold})."); } this.MaxContextWindowTokens = maxContextWindowTokens; this.MaxOutputTokens = maxOutputTokens; this.InputBudgetTokens = maxContextWindowTokens - maxOutputTokens; this.ToolEvictionThreshold = toolEvictionThreshold; this.TruncationThreshold = truncationThreshold; int toolEvictionTokens = (int)(this.InputBudgetTokens * toolEvictionThreshold); int truncationTokens = (int)(this.InputBudgetTokens * truncationThreshold); this._pipeline = new PipelineCompactionStrategy( new ToolResultCompactionStrategy( trigger: CompactionTriggers.TokensExceed(toolEvictionTokens), minimumPreservedGroups: 2), new TruncationCompactionStrategy( trigger: CompactionTriggers.TokensExceed(truncationTokens), minimumPreservedGroups: 2)); } /// /// Gets the maximum context window size in tokens. /// public int MaxContextWindowTokens { get; } /// /// Gets the maximum output tokens per response. /// public int MaxOutputTokens { get; } /// /// Gets the computed input budget in tokens ( minus ). /// public int InputBudgetTokens { get; } /// /// Gets the fraction of the input budget at which tool result eviction triggers. /// public double ToolEvictionThreshold { get; } /// /// Gets the fraction of the input budget at which truncation triggers. /// public double TruncationThreshold { get; } /// protected override async ValueTask CompactCoreAsync(CompactionMessageIndex index, ILogger logger, CancellationToken cancellationToken) { return await this._pipeline.CompactAsync(index, logger, cancellationToken).ConfigureAwait(false); } private static void ValidateThreshold(double value, string paramName) { if (value is <= 0.0 or > 1.0) { throw new ArgumentOutOfRangeException(paramName, value, "Threshold must be in the range (0.0, 1.0]."); } } }