mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Strategies
This commit is contained in:
@@ -23,6 +23,12 @@ namespace Microsoft.Agents.AI.Compaction;
|
||||
/// 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>
|
||||
@@ -43,9 +49,16 @@ public abstract class CompactionStrategy
|
||||
/// <param name="trigger">
|
||||
/// The <see cref="CompactionTrigger"/> that determines whether compaction should proceed.
|
||||
/// </param>
|
||||
protected CompactionStrategy(CompactionTrigger trigger)
|
||||
/// <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>
|
||||
@@ -53,6 +66,12 @@ public abstract class CompactionStrategy
|
||||
/// </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>
|
||||
/// Evaluates the <see cref="Trigger"/> and, when it fires, delegates to
|
||||
/// <see cref="ApplyCompactionAsync"/> and reports compaction metrics.
|
||||
@@ -98,7 +117,8 @@ public abstract class CompactionStrategy
|
||||
/// <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.
|
||||
/// 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="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
|
||||
|
||||
@@ -20,6 +20,14 @@ public static class CompactionTriggers
|
||||
public static readonly CompactionTrigger Always =
|
||||
_ => true;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a trigger that fires when the included token count is below the specified maximum.
|
||||
/// </summary>
|
||||
/// <param name="maxTokens">The token threshold. Compaction proceeds when included tokens exceed this value.</param>
|
||||
/// <returns>A <see cref="CompactionTrigger"/> that evaluates included token count.</returns>
|
||||
public static CompactionTrigger TokensBelow(int maxTokens) =>
|
||||
index => index.IncludedTokenCount < maxTokens;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a trigger that fires when the included token count exceeds the specified maximum.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -42,8 +41,12 @@ public sealed class SlidingWindowCompactionStrategy : CompactionStrategy
|
||||
/// <param name="maximumTurns">
|
||||
/// The maximum number of user turns to keep. Older turns and their associated responses are removed.
|
||||
/// </param>
|
||||
public SlidingWindowCompactionStrategy(int maximumTurns = DefaultMaximumTurns)
|
||||
: base(CompactionTriggers.TurnsExceed(maximumTurns))
|
||||
/// <param name="target">
|
||||
/// An optional target condition that controls when compaction stops. When <see langword="null"/>,
|
||||
/// defaults to the inverse of the auto-derived trigger — compaction stops as soon as the turn count is within bounds.
|
||||
/// </param>
|
||||
public SlidingWindowCompactionStrategy(int maximumTurns = DefaultMaximumTurns, CompactionTrigger? target = null)
|
||||
: base(CompactionTriggers.TurnsExceed(maximumTurns), target)
|
||||
{
|
||||
this.MaxTurns = maximumTurns;
|
||||
}
|
||||
@@ -71,24 +74,30 @@ public sealed class SlidingWindowCompactionStrategy : CompactionStrategy
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
// Determine which turn indices to exclude (oldest)
|
||||
// Exclude one turn at a time from oldest, re-checking target after each
|
||||
int turnsToRemove = includedTurns.Count - this.MaxTurns;
|
||||
HashSet<int> excludedTurnIndices = [.. includedTurns.Take(turnsToRemove)];
|
||||
|
||||
bool compacted = false;
|
||||
for (int i = 0; i < index.Groups.Count; i++)
|
||||
|
||||
for (int t = 0; t < turnsToRemove; t++)
|
||||
{
|
||||
MessageGroup group = index.Groups[i];
|
||||
if (group.IsExcluded || group.Kind == MessageGroupKind.System)
|
||||
int turnToExclude = includedTurns[t];
|
||||
|
||||
for (int i = 0; i < index.Groups.Count; i++)
|
||||
{
|
||||
continue;
|
||||
MessageGroup group = index.Groups[i];
|
||||
if (!group.IsExcluded && group.Kind != MessageGroupKind.System && group.TurnIndex == turnToExclude)
|
||||
{
|
||||
group.IsExcluded = true;
|
||||
group.ExcludeReason = $"Excluded by {nameof(SlidingWindowCompactionStrategy)}";
|
||||
}
|
||||
}
|
||||
|
||||
if (group.TurnIndex is int ti && excludedTurnIndices.Contains(ti))
|
||||
compacted = true;
|
||||
|
||||
// Stop when target condition is met
|
||||
if (this.Target(index))
|
||||
{
|
||||
group.IsExcluded = true;
|
||||
group.ExcludeReason = $"Excluded by {nameof(SlidingWindowCompactionStrategy)}";
|
||||
compacted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -58,12 +58,17 @@ public sealed class SummarizationCompactionStrategy : CompactionStrategy
|
||||
/// An optional custom system prompt for the summarization LLM call. When <see langword="null"/>,
|
||||
/// <see cref="DefaultSummarizationPrompt"/> is used.
|
||||
/// </param>
|
||||
/// <param name="target">
|
||||
/// An optional target condition that controls when compaction stops. When <see langword="null"/>,
|
||||
/// defaults to the inverse of the <paramref name="trigger"/> — compaction stops as soon as the trigger would no longer fire.
|
||||
/// </param>
|
||||
public SummarizationCompactionStrategy(
|
||||
IChatClient chatClient,
|
||||
CompactionTrigger trigger,
|
||||
int preserveRecentGroups = 4,
|
||||
string? summarizationPrompt = null)
|
||||
: base(trigger)
|
||||
string? summarizationPrompt = null,
|
||||
CompactionTrigger? target = null)
|
||||
: base(trigger, target)
|
||||
{
|
||||
this.ChatClient = Throw.IfNull(chatClient);
|
||||
this.PreserveRecentGroups = preserveRecentGroups;
|
||||
@@ -100,19 +105,19 @@ public sealed class SummarizationCompactionStrategy : CompactionStrategy
|
||||
}
|
||||
|
||||
int protectedFromEnd = Math.Min(this.PreserveRecentGroups, nonSystemIncludedCount);
|
||||
int groupsToSummarize = nonSystemIncludedCount - protectedFromEnd;
|
||||
int maxSummarizable = nonSystemIncludedCount - protectedFromEnd;
|
||||
|
||||
if (groupsToSummarize <= 0)
|
||||
if (maxSummarizable <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Collect the oldest non-system included groups for summarization
|
||||
// Mark oldest non-system groups for summarization one at a time until the target is met
|
||||
StringBuilder conversationText = new();
|
||||
int summarized = 0;
|
||||
int insertIndex = -1;
|
||||
|
||||
for (int i = 0; i < index.Groups.Count && summarized < groupsToSummarize; i++)
|
||||
for (int i = 0; i < index.Groups.Count && summarized < maxSummarizable; i++)
|
||||
{
|
||||
MessageGroup group = index.Groups[i];
|
||||
if (group.IsExcluded || group.Kind == MessageGroupKind.System)
|
||||
@@ -138,6 +143,12 @@ public sealed class SummarizationCompactionStrategy : CompactionStrategy
|
||||
group.IsExcluded = true;
|
||||
group.ExcludeReason = $"Summarized by {nameof(SummarizationCompactionStrategy)}";
|
||||
summarized++;
|
||||
|
||||
// Stop marking when target condition is met
|
||||
if (this.Target(index))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (summarized == 0)
|
||||
@@ -145,7 +156,7 @@ public sealed class SummarizationCompactionStrategy : CompactionStrategy
|
||||
return false;
|
||||
}
|
||||
|
||||
// Generate summary using the chat client
|
||||
// Generate summary using the chat client (single LLM call for all marked groups)
|
||||
ChatResponse response = await this.ChatClient.GetResponseAsync(
|
||||
[
|
||||
new ChatMessage(ChatRole.System, this.SummarizationPrompt),
|
||||
|
||||
@@ -44,8 +44,12 @@ public sealed class ToolResultCompactionStrategy : CompactionStrategy
|
||||
/// The number of most-recent non-system message groups to protect from collapsing.
|
||||
/// Defaults to <see cref="DefaultPreserveRecentGroups"/>, ensuring the current turn's tool interactions remain visible.
|
||||
/// </param>
|
||||
public ToolResultCompactionStrategy(CompactionTrigger trigger, int preserveRecentGroups = DefaultPreserveRecentGroups)
|
||||
: base(trigger)
|
||||
/// <param name="target">
|
||||
/// An optional target condition that controls when compaction stops. When <see langword="null"/>,
|
||||
/// defaults to the inverse of the <paramref name="trigger"/> — compaction stops as soon as the trigger would no longer fire.
|
||||
/// </param>
|
||||
public ToolResultCompactionStrategy(CompactionTrigger trigger, int preserveRecentGroups = DefaultPreserveRecentGroups, CompactionTrigger? target = null)
|
||||
: base(trigger, target)
|
||||
{
|
||||
this.PreserveRecentGroups = preserveRecentGroups;
|
||||
}
|
||||
@@ -76,15 +80,30 @@ public sealed class ToolResultCompactionStrategy : CompactionStrategy
|
||||
protectedGroupIndices.Add(nonSystemIncludedIndices[i]);
|
||||
}
|
||||
|
||||
// Process from end to start so insertions don't shift earlier indices
|
||||
bool compacted = false;
|
||||
for (int i = index.Groups.Count - 1; i >= 0; i--)
|
||||
// Collect eligible tool groups in order (oldest first)
|
||||
List<int> eligibleIndices = [];
|
||||
for (int i = 0; i < index.Groups.Count; i++)
|
||||
{
|
||||
MessageGroup group = index.Groups[i];
|
||||
if (group.IsExcluded || group.Kind != MessageGroupKind.ToolCall || protectedGroupIndices.Contains(i))
|
||||
if (!group.IsExcluded && group.Kind == MessageGroupKind.ToolCall && !protectedGroupIndices.Contains(i))
|
||||
{
|
||||
continue;
|
||||
eligibleIndices.Add(i);
|
||||
}
|
||||
}
|
||||
|
||||
if (eligibleIndices.Count == 0)
|
||||
{
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
// Collapse one tool group at a time from oldest, re-checking target after each
|
||||
bool compacted = false;
|
||||
int offset = 0;
|
||||
|
||||
for (int e = 0; e < eligibleIndices.Count; e++)
|
||||
{
|
||||
int idx = eligibleIndices[e] + offset;
|
||||
MessageGroup group = index.Groups[idx];
|
||||
|
||||
// Extract tool names from FunctionCallContent
|
||||
List<string> toolNames = [];
|
||||
@@ -107,9 +126,16 @@ public sealed class ToolResultCompactionStrategy : CompactionStrategy
|
||||
group.ExcludeReason = $"Collapsed by {nameof(ToolResultCompactionStrategy)}";
|
||||
|
||||
string summary = $"[Tool calls: {string.Join(", ", toolNames)}]";
|
||||
index.InsertGroup(i + 1, MessageGroupKind.AssistantText, [new ChatMessage(ChatRole.Assistant, summary)], group.TurnIndex);
|
||||
index.InsertGroup(idx + 1, MessageGroupKind.AssistantText, [new ChatMessage(ChatRole.Assistant, summary)], group.TurnIndex);
|
||||
offset++; // Each insertion shifts subsequent indices by 1
|
||||
|
||||
compacted = true;
|
||||
|
||||
// Stop when target condition is met
|
||||
if (this.Target(index))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return Task.FromResult(compacted);
|
||||
|
||||
@@ -37,8 +37,12 @@ public sealed class TruncationCompactionStrategy : CompactionStrategy
|
||||
/// 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.
|
||||
/// </param>
|
||||
public TruncationCompactionStrategy(CompactionTrigger trigger, int preserveRecentGroups = DefaultPreserveRecentGroups)
|
||||
: base(trigger)
|
||||
/// <param name="target">
|
||||
/// An optional target condition that controls when compaction stops. When <see langword="null"/>,
|
||||
/// defaults to the inverse of the <paramref name="trigger"/> — compaction stops as soon as the trigger would no longer fire.
|
||||
/// </param>
|
||||
public TruncationCompactionStrategy(CompactionTrigger trigger, int preserveRecentGroups = DefaultPreserveRecentGroups, CompactionTrigger? target = null)
|
||||
: base(trigger, target)
|
||||
{
|
||||
this.PreserveRecentGroups = preserveRecentGroups;
|
||||
}
|
||||
@@ -68,7 +72,7 @@ public sealed class TruncationCompactionStrategy : CompactionStrategy
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
// Exclude oldest non-system groups first (iterate from the beginning)
|
||||
// Exclude oldest non-system groups one at a time, re-checking target after each
|
||||
bool compacted = false;
|
||||
int removed = 0;
|
||||
for (int i = 0; i < index.Groups.Count && removed < maxRemovable; i++)
|
||||
@@ -83,6 +87,12 @@ public sealed class TruncationCompactionStrategy : CompactionStrategy
|
||||
group.ExcludeReason = $"Truncated by {nameof(TruncationCompactionStrategy)}";
|
||||
removed++;
|
||||
compacted = true;
|
||||
|
||||
// Stop when target condition is met
|
||||
if (this.Target(index))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return Task.FromResult(compacted);
|
||||
|
||||
+4
-4
@@ -75,13 +75,13 @@ public class TruncationCompactionStrategyTests
|
||||
// Act
|
||||
bool result = await strategy.CompactAsync(groups);
|
||||
|
||||
// Assert
|
||||
// Assert — incremental: excludes until GroupsExceed(2) is no longer met → 2 groups remain
|
||||
Assert.True(result);
|
||||
Assert.Equal(1, groups.IncludedGroupCount);
|
||||
// Oldest 3 excluded, newest 1 kept
|
||||
Assert.Equal(2, groups.IncludedGroupCount);
|
||||
// Oldest 2 excluded, newest 2 kept
|
||||
Assert.True(groups.Groups[0].IsExcluded);
|
||||
Assert.True(groups.Groups[1].IsExcluded);
|
||||
Assert.True(groups.Groups[2].IsExcluded);
|
||||
Assert.False(groups.Groups[2].IsExcluded);
|
||||
Assert.False(groups.Groups[3].IsExcluded);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user