// 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].");
}
}
}