mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf0f7061eb | ||
|
|
b14d7dafb9 | ||
|
|
0688b6b186 | ||
|
|
64e529916c | ||
|
|
7d6beea6e5 | ||
|
|
1b969b078e | ||
|
|
a3f11f8cfd | ||
|
|
8e598ed8cb | ||
|
|
1dae527377 | ||
|
|
3f3bb77243 | ||
|
|
d48ddbef06 | ||
|
|
0985a7fb76 | ||
|
|
c4c236bbc9 | ||
|
|
74e75268bc | ||
|
|
579993d165 |
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.Shared.DiagnosticIds;
|
||||
|
||||
namespace Microsoft.Agents.AI.Compaction;
|
||||
|
||||
#pragma warning disable IDE0001 // Simplify Names for namespace in comments
|
||||
|
||||
/// <summary>
|
||||
/// Describes the compaction approach used by a pre-configured <see cref="CompactionStrategy"/>.
|
||||
/// </summary>
|
||||
/// <seealso cref="CompactionStrategy.Create"/>
|
||||
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
|
||||
public enum CompactionApproach
|
||||
{
|
||||
/// <summary>
|
||||
/// Applies the lightest available compaction techniques.
|
||||
/// Collapses old tool call groups into concise summaries and uses truncation as an emergency backstop.
|
||||
/// </summary>
|
||||
Gentle,
|
||||
|
||||
/// <summary>
|
||||
/// Balances context preservation with compaction efficiency.
|
||||
/// Applies tool result collapsing, LLM-based summarization, and truncation as an emergency backstop.
|
||||
/// </summary>
|
||||
Balanced,
|
||||
|
||||
/// <summary>
|
||||
/// Applies the most aggressive available compaction techniques.
|
||||
/// Applies tool result collapsing, LLM-based summarization, turn-based sliding window, and truncation.
|
||||
/// </summary>
|
||||
Aggressive,
|
||||
}
|
||||
|
||||
#pragma warning restore IDE0001 // Simplify Names
|
||||
@@ -0,0 +1,38 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.Shared.DiagnosticIds;
|
||||
|
||||
namespace Microsoft.Agents.AI.Compaction;
|
||||
|
||||
/// <summary>
|
||||
/// Describes the context-size profile used by a pre-configured <see cref="CompactionStrategy"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The size profile controls the token and message thresholds at which compaction triggers.
|
||||
/// Choose a size that matches the input token limit of your model:
|
||||
/// <see cref="Compact"/> for smaller context windows, <see cref="Moderate"/> for common mid-range models,
|
||||
/// and <see cref="Generous"/> for models with large context windows.
|
||||
/// </remarks>
|
||||
/// <seealso cref="CompactionStrategy.Create"/>
|
||||
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
|
||||
public enum CompactionSize
|
||||
{
|
||||
/// <summary>
|
||||
/// Maintains a smaller context window.
|
||||
/// Compaction triggers earlier and keeps less history in context.
|
||||
/// </summary>
|
||||
Compact,
|
||||
|
||||
/// <summary>
|
||||
/// Maintains a medium-sized context window.
|
||||
/// This is a reasonable default for most common models.
|
||||
/// </summary>
|
||||
Moderate,
|
||||
|
||||
/// <summary>
|
||||
/// Maintains a large context window.
|
||||
/// Compaction triggers later and retains more history in context.
|
||||
/// </summary>
|
||||
Generous,
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
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>
|
||||
public abstract partial class CompactionStrategy
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a pre-configured <see cref="CompactionStrategy"/> from a combination of
|
||||
/// <paramref name="approach"/> and <paramref name="size"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The <paramref name="approach"/> controls which strategies are included in the pipeline:
|
||||
/// <list type="bullet">
|
||||
/// <item><description><see cref="CompactionApproach.Gentle"/>: tool result collapsing + truncation backstop. No <paramref name="chatClient"/> required.</description></item>
|
||||
/// <item><description><see cref="CompactionApproach.Balanced"/>: tool result collapsing + LLM summarization + truncation backstop.</description></item>
|
||||
/// <item><description><see cref="CompactionApproach.Aggressive"/>: tool result collapsing + LLM summarization + sliding window + truncation backstop.</description></item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The <paramref name="size"/> controls the token and message thresholds at which each stage triggers.
|
||||
/// Choose a size that matches the input token limit of your model.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="approach">
|
||||
/// The compaction approach that controls which strategy or pipeline to use.
|
||||
/// </param>
|
||||
/// <param name="size">
|
||||
/// The context-size profile that controls token and message thresholds.
|
||||
/// </param>
|
||||
/// <param name="chatClient">
|
||||
/// The <see cref="IChatClient"/> used for LLM-based summarization.
|
||||
/// </param>
|
||||
/// <returns>A <see cref="CompactionStrategy"/> configured for the specified approach and size.</returns>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// <paramref name="chatClient"/> is <see langword="null"/> and <paramref name="approach"/> requires one.
|
||||
/// </exception>
|
||||
/// <exception cref="ArgumentOutOfRangeException">
|
||||
/// <paramref name="approach"/> or <paramref name="size"/> is not a defined enum value.
|
||||
/// </exception>
|
||||
public static CompactionStrategy Create(CompactionApproach approach, CompactionSize size, IChatClient chatClient)
|
||||
{
|
||||
int tokenLimit = GetTokenCountLimit(size);
|
||||
int messageLimit = GetMessageCountLimit(size);
|
||||
|
||||
return approach switch
|
||||
{
|
||||
CompactionApproach.Gentle => CreateGentlePipeline(tokenLimit, messageLimit, chatClient),
|
||||
CompactionApproach.Balanced => CreateBalancedPipeline(tokenLimit, messageLimit, chatClient),
|
||||
CompactionApproach.Aggressive => CreateAggressivePipeline(tokenLimit, messageLimit, GetTurnCountLimit(size), chatClient),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(approach), approach, null),
|
||||
};
|
||||
}
|
||||
|
||||
private static int GetTokenCountLimit(CompactionSize size) => size switch
|
||||
{
|
||||
CompactionSize.Compact => 0x1FFF, // 8k
|
||||
CompactionSize.Moderate => 0x7FFF, // 32k
|
||||
CompactionSize.Generous => 0xFFFF, // 64k
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(size), size, null),
|
||||
};
|
||||
|
||||
private static int GetMessageCountLimit(CompactionSize size) => size switch
|
||||
{
|
||||
CompactionSize.Compact => 50,
|
||||
CompactionSize.Moderate => 500,
|
||||
CompactionSize.Generous => 1000,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(size), size, null),
|
||||
};
|
||||
|
||||
private static int GetTurnCountLimit(CompactionSize size) => size switch
|
||||
{
|
||||
CompactionSize.Compact => 25,
|
||||
CompactionSize.Moderate => 250,
|
||||
CompactionSize.Generous => 500,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(size), size, null),
|
||||
};
|
||||
|
||||
private static PipelineCompactionStrategy CreateGentlePipeline(int tokenLimit, int messageLimit, IChatClient chatClient)
|
||||
{
|
||||
int messageTarget = messageLimit * 4 / 5;
|
||||
int tokenTarget = tokenLimit * 4 / 5;
|
||||
|
||||
return new(
|
||||
new ToolResultCompactionStrategy(
|
||||
trigger: CompactionTriggers.MessagesExceed(messageLimit),
|
||||
target: CompactionTriggers.MessagesBelow(messageTarget)),
|
||||
new SummarizationCompactionStrategy(
|
||||
chatClient,
|
||||
trigger: CompactionTriggers.TokensExceed(tokenLimit),
|
||||
target: CompactionTriggers.TokensBelow(tokenTarget)));
|
||||
}
|
||||
|
||||
private static PipelineCompactionStrategy CreateBalancedPipeline(int tokenLimit, int messageLimit, IChatClient chatClient)
|
||||
{
|
||||
int messageTarget = messageLimit * 3 / 4;
|
||||
int tokenTarget = tokenLimit * 4 / 5;
|
||||
|
||||
return new(
|
||||
new ToolResultCompactionStrategy(
|
||||
trigger: CompactionTriggers.MessagesExceed(messageLimit),
|
||||
target: CompactionTriggers.MessagesBelow(messageTarget)),
|
||||
new SummarizationCompactionStrategy(
|
||||
chatClient,
|
||||
trigger: CompactionTriggers.TokensExceed(tokenLimit),
|
||||
target: CompactionTriggers.TokensBelow(tokenTarget)),
|
||||
new TruncationCompactionStrategy(
|
||||
trigger: CompactionTriggers.TokensExceed(tokenLimit),
|
||||
target: CompactionTriggers.TokensBelow(tokenTarget)));
|
||||
}
|
||||
|
||||
private static PipelineCompactionStrategy CreateAggressivePipeline(int tokenLimit, int messageLimit, int turnLimit, IChatClient chatClient)
|
||||
{
|
||||
// Early stages trigger at half the limit so compaction kicks in sooner and
|
||||
// the sliding window and truncation backstop are reached less often.
|
||||
int messageTarget = messageLimit * 3 / 4;
|
||||
int tokenTarget = tokenLimit * 3 / 4;
|
||||
|
||||
return new(
|
||||
new ToolResultCompactionStrategy(
|
||||
trigger: CompactionTriggers.MessagesExceed(messageLimit),
|
||||
target: CompactionTriggers.MessagesBelow(messageTarget)),
|
||||
new SummarizationCompactionStrategy(
|
||||
chatClient,
|
||||
trigger: CompactionTriggers.TokensExceed(tokenLimit),
|
||||
target: CompactionTriggers.TokensBelow(tokenTarget)),
|
||||
new SlidingWindowCompactionStrategy(
|
||||
trigger: CompactionTriggers.TokensExceed(tokenLimit),
|
||||
target: CompactionTriggers.TokensBelow(tokenTarget)),
|
||||
new TruncationCompactionStrategy(
|
||||
trigger: CompactionTriggers.TokensExceed(tokenLimit),
|
||||
target: CompactionTriggers.TokensBelow(tokenTarget)));
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@ namespace Microsoft.Agents.AI.Compaction;
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
|
||||
public abstract class CompactionStrategy
|
||||
public abstract partial class CompactionStrategy
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CompactionStrategy"/> class.
|
||||
|
||||
@@ -50,6 +50,14 @@ public static class CompactionTriggers
|
||||
public static CompactionTrigger TokensExceed(int maxTokens) =>
|
||||
index => index.IncludedTokenCount > maxTokens;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a trigger that fires when the included message count is below the specified maximum.
|
||||
/// </summary>
|
||||
/// <param name="maxMessages">The message threshold.</param>
|
||||
/// <returns>A <see cref="CompactionTrigger"/> that evaluates included message count.</returns>
|
||||
public static CompactionTrigger MessagesBelow(int maxMessages) =>
|
||||
index => index.IncludedMessageCount < maxMessages;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a trigger that fires when the included message count exceeds the specified maximum.
|
||||
/// </summary>
|
||||
|
||||
+227
@@ -0,0 +1,227 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Compaction;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Moq;
|
||||
|
||||
namespace Microsoft.Agents.AI.UnitTests.Compaction;
|
||||
|
||||
/// <summary>
|
||||
/// Contains tests for <see cref="CompactionStrategy.Create"/>.
|
||||
/// </summary>
|
||||
public class CompactionStrategyCreateTests
|
||||
{
|
||||
private static IChatClient CreateMockChatClient()
|
||||
{
|
||||
Mock<IChatClient> mock = new();
|
||||
mock.Setup(c => c.GetResponseAsync(
|
||||
It.IsAny<IEnumerable<ChatMessage>>(),
|
||||
It.IsAny<ChatOptions>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new ChatResponse([new ChatMessage(ChatRole.Assistant, "summary")]));
|
||||
return mock.Object;
|
||||
}
|
||||
|
||||
// ── Gentle ────────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void CreateGentleCompactReturnsTwoStrategyPipeline()
|
||||
{
|
||||
PipelineCompactionStrategy pipeline =
|
||||
Assert.IsType<PipelineCompactionStrategy>(
|
||||
CompactionStrategy.Create(CompactionApproach.Gentle, CompactionSize.Compact, CreateMockChatClient()));
|
||||
|
||||
Assert.Equal(2, pipeline.Strategies.Count);
|
||||
Assert.IsType<ToolResultCompactionStrategy>(pipeline.Strategies[0]);
|
||||
Assert.IsType<SummarizationCompactionStrategy>(pipeline.Strategies[1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateGentleModerateReturnsTwoStrategyPipeline()
|
||||
{
|
||||
PipelineCompactionStrategy pipeline =
|
||||
Assert.IsType<PipelineCompactionStrategy>(
|
||||
CompactionStrategy.Create(CompactionApproach.Gentle, CompactionSize.Moderate, CreateMockChatClient()));
|
||||
|
||||
Assert.Equal(2, pipeline.Strategies.Count);
|
||||
Assert.IsType<ToolResultCompactionStrategy>(pipeline.Strategies[0]);
|
||||
Assert.IsType<SummarizationCompactionStrategy>(pipeline.Strategies[1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateGentleGenerousReturnsTwoStrategyPipeline()
|
||||
{
|
||||
PipelineCompactionStrategy pipeline =
|
||||
Assert.IsType<PipelineCompactionStrategy>(
|
||||
CompactionStrategy.Create(CompactionApproach.Gentle, CompactionSize.Generous, CreateMockChatClient()));
|
||||
|
||||
Assert.Equal(2, pipeline.Strategies.Count);
|
||||
Assert.IsType<ToolResultCompactionStrategy>(pipeline.Strategies[0]);
|
||||
Assert.IsType<SummarizationCompactionStrategy>(pipeline.Strategies[1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateGentleDoesNotRequireChatClient()
|
||||
{
|
||||
// No chatClient supplied — should succeed without throwing.
|
||||
CompactionStrategy strategy = CompactionStrategy.Create(CompactionApproach.Gentle, CompactionSize.Moderate, CreateMockChatClient());
|
||||
Assert.NotNull(strategy);
|
||||
}
|
||||
|
||||
// ── Balanced ──────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void CreateBalancedCompactReturnsThreeStrategyPipeline()
|
||||
{
|
||||
PipelineCompactionStrategy pipeline =
|
||||
Assert.IsType<PipelineCompactionStrategy>(
|
||||
CompactionStrategy.Create(CompactionApproach.Balanced, CompactionSize.Compact, CreateMockChatClient()));
|
||||
|
||||
Assert.Equal(3, pipeline.Strategies.Count);
|
||||
Assert.IsType<ToolResultCompactionStrategy>(pipeline.Strategies[0]);
|
||||
Assert.IsType<SummarizationCompactionStrategy>(pipeline.Strategies[1]);
|
||||
Assert.IsType<TruncationCompactionStrategy>(pipeline.Strategies[2]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateBalancedModerateReturnsThreeStrategyPipeline()
|
||||
{
|
||||
PipelineCompactionStrategy pipeline =
|
||||
Assert.IsType<PipelineCompactionStrategy>(
|
||||
CompactionStrategy.Create(CompactionApproach.Balanced, CompactionSize.Moderate, CreateMockChatClient()));
|
||||
|
||||
Assert.Equal(3, pipeline.Strategies.Count);
|
||||
Assert.IsType<ToolResultCompactionStrategy>(pipeline.Strategies[0]);
|
||||
Assert.IsType<SummarizationCompactionStrategy>(pipeline.Strategies[1]);
|
||||
Assert.IsType<TruncationCompactionStrategy>(pipeline.Strategies[2]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateBalancedGenerousReturnsThreeStrategyPipeline()
|
||||
{
|
||||
PipelineCompactionStrategy pipeline =
|
||||
Assert.IsType<PipelineCompactionStrategy>(
|
||||
CompactionStrategy.Create(CompactionApproach.Balanced, CompactionSize.Generous, CreateMockChatClient()));
|
||||
|
||||
Assert.Equal(3, pipeline.Strategies.Count);
|
||||
Assert.IsType<ToolResultCompactionStrategy>(pipeline.Strategies[0]);
|
||||
Assert.IsType<SummarizationCompactionStrategy>(pipeline.Strategies[1]);
|
||||
Assert.IsType<TruncationCompactionStrategy>(pipeline.Strategies[2]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateBalancedNullChatClientThrows()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(
|
||||
() => CompactionStrategy.Create(CompactionApproach.Balanced, CompactionSize.Moderate, null!));
|
||||
}
|
||||
|
||||
// ── Aggressive ────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void CreateAggressiveCompactReturnsFourStrategyPipeline()
|
||||
{
|
||||
PipelineCompactionStrategy pipeline =
|
||||
Assert.IsType<PipelineCompactionStrategy>(
|
||||
CompactionStrategy.Create(CompactionApproach.Aggressive, CompactionSize.Compact, CreateMockChatClient()));
|
||||
|
||||
Assert.Equal(4, pipeline.Strategies.Count);
|
||||
Assert.IsType<ToolResultCompactionStrategy>(pipeline.Strategies[0]);
|
||||
Assert.IsType<SummarizationCompactionStrategy>(pipeline.Strategies[1]);
|
||||
Assert.IsType<SlidingWindowCompactionStrategy>(pipeline.Strategies[2]);
|
||||
Assert.IsType<TruncationCompactionStrategy>(pipeline.Strategies[3]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateAggressiveModerateReturnsFourStrategyPipeline()
|
||||
{
|
||||
PipelineCompactionStrategy pipeline =
|
||||
Assert.IsType<PipelineCompactionStrategy>(
|
||||
CompactionStrategy.Create(CompactionApproach.Aggressive, CompactionSize.Moderate, CreateMockChatClient()));
|
||||
|
||||
Assert.Equal(4, pipeline.Strategies.Count);
|
||||
Assert.IsType<ToolResultCompactionStrategy>(pipeline.Strategies[0]);
|
||||
Assert.IsType<SummarizationCompactionStrategy>(pipeline.Strategies[1]);
|
||||
Assert.IsType<SlidingWindowCompactionStrategy>(pipeline.Strategies[2]);
|
||||
Assert.IsType<TruncationCompactionStrategy>(pipeline.Strategies[3]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateAggressiveGenerousReturnsFourStrategyPipeline()
|
||||
{
|
||||
PipelineCompactionStrategy pipeline =
|
||||
Assert.IsType<PipelineCompactionStrategy>(
|
||||
CompactionStrategy.Create(CompactionApproach.Aggressive, CompactionSize.Generous, CreateMockChatClient()));
|
||||
|
||||
Assert.Equal(4, pipeline.Strategies.Count);
|
||||
Assert.IsType<ToolResultCompactionStrategy>(pipeline.Strategies[0]);
|
||||
Assert.IsType<SummarizationCompactionStrategy>(pipeline.Strategies[1]);
|
||||
Assert.IsType<SlidingWindowCompactionStrategy>(pipeline.Strategies[2]);
|
||||
Assert.IsType<TruncationCompactionStrategy>(pipeline.Strategies[3]);
|
||||
}
|
||||
|
||||
// ── Invalid enum values ───────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void CreateInvalidApproachThrows()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(
|
||||
() => CompactionStrategy.Create((CompactionApproach)99, CompactionSize.Moderate, CreateMockChatClient()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateInvalidSizeThrows()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(
|
||||
() => CompactionStrategy.Create(CompactionApproach.Gentle, (CompactionSize)99, CreateMockChatClient()));
|
||||
}
|
||||
|
||||
// ── Size-threshold behavioral verification ────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that <see cref="CompactionSize.Compact"/> and <see cref="CompactionSize.Moderate"/>
|
||||
/// configure different message thresholds.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task CreateGentleSizeDifferentiatesMessageThresholdsAsync()
|
||||
{
|
||||
// Arrange: 1 tool-call group + 99 (User, Assistant) groups = 100 groups / 200 messages.
|
||||
// Compact ToolResult triggers at MessagesExceed(50) → 200 > 50 → fires.
|
||||
// Moderate ToolResult triggers at MessagesExceed(500) → 200 < 500 → does not fire.
|
||||
//
|
||||
// The configuration preserves a number of most-recent groups, leaving the oldest
|
||||
// tool-call group eligible for collapsing, which makes the behavioral difference
|
||||
// between Compact and Moderate sizes observable in this test.
|
||||
CompactionStrategy compactPipeline = CompactionStrategy.Create(CompactionApproach.Gentle, CompactionSize.Compact, CreateMockChatClient());
|
||||
CompactionStrategy moderatePipeline = CompactionStrategy.Create(CompactionApproach.Gentle, CompactionSize.Moderate, CreateMockChatClient());
|
||||
|
||||
List<ChatMessage> messages =
|
||||
[
|
||||
// 1 tool-call group (assistant FunctionCall + tool result = 2 messages, 1 group)
|
||||
new(ChatRole.Assistant, [new FunctionCallContent("c1", "fetch")]),
|
||||
new(ChatRole.Tool, "data"),
|
||||
];
|
||||
|
||||
for (int index = 0; index < 99; ++index)
|
||||
{
|
||||
messages.Add(new(ChatRole.User, $"Q{index}"));
|
||||
messages.Add(new(ChatRole.Assistant, $"A{index}"));
|
||||
}
|
||||
|
||||
// Two separate indexes so strategies run independently.
|
||||
CompactionMessageIndex compactIndex = CompactionMessageIndex.Create(messages);
|
||||
CompactionMessageIndex moderateIndex = CompactionMessageIndex.Create(messages);
|
||||
|
||||
// Act
|
||||
bool compactCompacted = await compactPipeline.CompactAsync(compactIndex);
|
||||
bool moderateCompacted = await moderatePipeline.CompactAsync(moderateIndex);
|
||||
|
||||
// Assert
|
||||
Assert.True(compactCompacted, "Compact size should trigger ToolResult compaction.");
|
||||
Assert.False(moderateCompacted, "Moderate size should NOT trigger ToolResult compaction.");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user