mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
* Checkpoint * Checkpoint * Stable * Strategies * Updated * Encoding * Formatting * Cleanup * Formatting * Tests * Tuning * Update tests * Test update * Remove working solution * Add sample to solution * Sample readyme * Experimental * Format * Formatting * Encoding * Support IChatReducer * Sample output formatting * Initial plan * Replace CompactingChatClient with MessageCompactionContextProvider Co-authored-by: crickman <66376200+crickman@users.noreply.github.com> * Boundary condition * Fix encoding * Fix cast * Test coverage * Namespace * Improvements * Efficiency * Cleanup * Detect service managed conversation * Fix namespace * Fix merge * Fix test expectation * Update dotnet/src/Microsoft.Agents.AI.Abstractions/InMemoryChatHistoryProvider.cs Co-authored-by: westey <164392973+westey-m@users.noreply.github.com> * Address PR comments (x1) * Update comment * Update comments * Clean-up * Format output * Sync sample comment * Fix condition * Adjust data-flow * Address comments (x2) * Direct compaction * Fix summarization content * Argument check / fix count calculation * Minor follow-up * Diagnostics * Minor updates * Fix state test * Fix sliding window perf * Stable state keys * Increase size computation * Formatting * Add README.md for Agent_Step18_CompactionPipeline sample (#4574) * Sample comments * Updated * Update dotnet/src/Microsoft.Agents.AI/Compaction/MessageIndex.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update dotnet/tests/Microsoft.Agents.AI.UnitTests/Compaction/CompactionProviderTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update dotnet/src/Microsoft.Agents.AI/Compaction/MessageIndex.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Address copilot comments * Fix namespace * Comments / convensions * Prefix `MessageGroup` and `MessageIndex` * Fix sliding window * Update dotnet/src/Microsoft.Agents.AI/Compaction/SummarizationCompactionStrategy.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update dotnet/src/Microsoft.Agents.AI.Abstractions/InMemoryChatHistoryProvider.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Python alignment * Fix merge * Fix equality, readme, and sample * Readme update and ToolResult fix * Update dotnet/src/Microsoft.Agents.AI/Compaction/SummarizationCompactionStrategy.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update dotnet/samples/02-agents/Agents/Agent_Step18_CompactionPipeline/README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Simplify readme * Update dotnet/samples/02-agents/Agents/Agent_Step18_CompactionPipeline/README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Remove example * Remove unused --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: westey <164392973+westey-m@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
165 lines
8.1 KiB
C#
165 lines
8.1 KiB
C#
// 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;
|
|
|
|
/// <summary>
|
|
/// Base class for strategies that compact a <see cref="CompactionMessageIndex"/> to reduce context size.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// Compaction strategies operate on <see cref="CompactionMessageIndex"/> 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).
|
|
/// </para>
|
|
/// <para>
|
|
/// Every strategy requires a <see cref="CompactionTrigger"/> that determines whether compaction should
|
|
/// proceed based on current <see cref="CompactionMessageIndex"/> metrics (token count, message count, turn count, etc.).
|
|
/// The base class evaluates this trigger at the start of <see cref="CompactAsync"/> and skips compaction when
|
|
/// the trigger returns <see langword="false"/>.
|
|
/// </para>
|
|
/// <para>
|
|
/// An optional <b>target</b> condition controls when compaction stops. Strategies incrementally exclude
|
|
/// groups and re-evaluate the target after each exclusion, stopping as soon as the target returns
|
|
/// <see langword="true"/>. When no target is specified, it defaults to the inverse of the trigger —
|
|
/// meaning compaction stops when the trigger condition would no longer fire.
|
|
/// </para>
|
|
/// <para>
|
|
/// Strategies can be applied at three lifecycle points:
|
|
/// <list type="bullet">
|
|
/// <item><description><b>In-run</b>: During the tool loop, before each LLM call, to keep context within token limits.</description></item>
|
|
/// <item><description><b>Pre-write</b>: Before persisting messages to storage via <see cref="ChatHistoryProvider"/>.</description></item>
|
|
/// <item><description><b>On existing storage</b>: As a maintenance operation to compact stored history.</description></item>
|
|
/// </list>
|
|
/// </para>
|
|
/// <para>
|
|
/// Multiple strategies can be composed by applying them sequentially to the same <see cref="CompactionMessageIndex"/>
|
|
/// via <see cref="PipelineCompactionStrategy"/>.
|
|
/// </para>
|
|
/// </remarks>
|
|
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
|
|
public abstract class CompactionStrategy
|
|
{
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="CompactionStrategy"/> class.
|
|
/// </summary>
|
|
/// <param name="trigger">
|
|
/// The <see cref="CompactionTrigger"/> that determines whether compaction should proceed.
|
|
/// </param>
|
|
/// <param name="target">
|
|
/// An optional target condition that controls when compaction stops. Strategies re-evaluate
|
|
/// this predicate after each incremental exclusion and stop when it returns <see langword="true"/>.
|
|
/// When <see langword="null"/>, defaults to the inverse of the <paramref name="trigger"/> — compaction
|
|
/// stops as soon as the trigger condition would no longer fire.
|
|
/// </param>
|
|
protected CompactionStrategy(CompactionTrigger trigger, CompactionTrigger? target = null)
|
|
{
|
|
this.Trigger = Throw.IfNull(trigger);
|
|
this.Target = target ?? (index => !trigger(index));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the trigger predicate that controls when compaction proceeds.
|
|
/// </summary>
|
|
protected CompactionTrigger Trigger { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the target predicate that controls when compaction stops.
|
|
/// Strategies re-evaluate this after each incremental exclusion and stop when it returns <see langword="true"/>.
|
|
/// </summary>
|
|
protected CompactionTrigger Target { get; }
|
|
|
|
/// <summary>
|
|
/// Applies the strategy-specific compaction logic to the specified message index.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This method is called by <see cref="CompactAsync"/> only when the <see cref="Trigger"/>
|
|
/// returns <see langword="true"/>. Implementations do not need to evaluate the trigger or
|
|
/// report metrics — the base class handles both. Implementations should use <see cref="Target"/>
|
|
/// to determine when to stop compacting incrementally.
|
|
/// </remarks>
|
|
/// <param name="index">The message index to compact. The strategy mutates this collection in place.</param>
|
|
/// <param name="logger">The <see cref="ILogger"/> for emitting compaction diagnostics.</param>
|
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
|
|
/// <returns>A task whose result is <see langword="true"/> if any compaction was performed, <see langword="false"/> otherwise.</returns>
|
|
protected abstract ValueTask<bool> CompactCoreAsync(CompactionMessageIndex index, ILogger logger, CancellationToken cancellationToken);
|
|
|
|
/// <summary>
|
|
/// Evaluates the <see cref="Trigger"/> and, when it fires, delegates to
|
|
/// <see cref="CompactCoreAsync"/> and reports compaction metrics.
|
|
/// </summary>
|
|
/// <param name="index">The message index to compact. The strategy mutates this collection in place.</param>
|
|
/// <param name="logger">An optional <see cref="ILogger"/> for emitting compaction diagnostics. When <see langword="null"/>, logging is disabled.</param>
|
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
|
|
/// <returns>A task representing the asynchronous operation. The task result is <see langword="true"/> if compaction occurred, <see langword="false"/> otherwise.</returns>
|
|
public async ValueTask<bool> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ensures the provided value is not a negative number.
|
|
/// </summary>
|
|
/// <param name="value">The target value.</param>
|
|
/// <returns>0 if negative; otherwise the value</returns>
|
|
protected static int EnsureNonNegative(int value) => Math.Max(0, value);
|
|
}
|