Files
Chris d3f0c33180 .NET Compaction - Introducing compaction strategies and pipeline (#4533)
* 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>
2026-03-11 00:41:39 +00:00

237 lines
7.8 KiB
C#

// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Compaction;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging;
namespace Microsoft.Agents.AI.UnitTests.Compaction;
/// <summary>
/// Contains tests for the <see cref="CompactionStrategy"/> abstract base class.
/// </summary>
public class CompactionStrategyTests
{
[Fact]
public void ConstructorNullTriggerThrows()
{
// Act & Assert
Assert.Throws<ArgumentNullException>(() => new TestStrategy(null!));
}
[Fact]
public async Task CompactAsyncTriggerNotMetReturnsFalseAsync()
{
// Arrange — trigger never fires, but enough non-system groups to pass short-circuit
TestStrategy strategy = new(_ => false);
CompactionMessageIndex index = CompactionMessageIndex.Create(
[
new ChatMessage(ChatRole.User, "Hello"),
new ChatMessage(ChatRole.Assistant, "Hi!"),
]);
// Act
bool result = await strategy.CompactAsync(index);
// Assert
Assert.False(result);
Assert.Equal(0, strategy.ApplyCallCount);
}
[Fact]
public async Task CompactAsyncTriggerMetCallsApplyAsync()
{
// Arrange — trigger always fires, enough non-system groups
TestStrategy strategy = new(_ => true, applyFunc: _ => true);
CompactionMessageIndex index = CompactionMessageIndex.Create(
[
new ChatMessage(ChatRole.User, "Hello"),
new ChatMessage(ChatRole.Assistant, "Hi!"),
]);
// Act
bool result = await strategy.CompactAsync(index);
// Assert
Assert.True(result);
Assert.Equal(1, strategy.ApplyCallCount);
}
[Fact]
public async Task CompactAsyncReturnsFalseWhenApplyReturnsFalseAsync()
{
// Arrange — trigger fires but Apply does nothing
TestStrategy strategy = new(_ => true, applyFunc: _ => false);
CompactionMessageIndex index = CompactionMessageIndex.Create(
[
new ChatMessage(ChatRole.User, "Hello"),
new ChatMessage(ChatRole.Assistant, "Hi!"),
]);
// Act
bool result = await strategy.CompactAsync(index);
// Assert
Assert.False(result);
Assert.Equal(1, strategy.ApplyCallCount);
}
[Fact]
public async Task CompactAsyncSingleNonSystemGroupShortCircuitsAsync()
{
// Arrange — trigger would fire, but only 1 non-system group → short-circuit
TestStrategy strategy = new(_ => true, applyFunc: _ => true);
CompactionMessageIndex index = CompactionMessageIndex.Create([new ChatMessage(ChatRole.User, "Hello")]);
// Act
bool result = await strategy.CompactAsync(index);
// Assert — short-circuited before trigger or Apply
Assert.False(result);
Assert.Equal(0, strategy.ApplyCallCount);
}
[Fact]
public async Task CompactAsyncSingleNonSystemGroupWithSystemShortCircuitsAsync()
{
// Arrange — system group + 1 non-system group → still short-circuits
TestStrategy strategy = new(_ => true, applyFunc: _ => true);
CompactionMessageIndex index = CompactionMessageIndex.Create(
[
new ChatMessage(ChatRole.System, "You are helpful."),
new ChatMessage(ChatRole.User, "Hello"),
]);
// Act
bool result = await strategy.CompactAsync(index);
// Assert — system groups don't count, still only 1 non-system group
Assert.False(result);
Assert.Equal(0, strategy.ApplyCallCount);
}
[Fact]
public async Task CompactAsyncTwoNonSystemGroupsProceedsToTriggerAsync()
{
// Arrange — exactly 2 non-system groups: boundary passes, trigger fires
TestStrategy strategy = new(_ => true, applyFunc: _ => true);
CompactionMessageIndex index = CompactionMessageIndex.Create(
[
new ChatMessage(ChatRole.User, "Hello"),
new ChatMessage(ChatRole.Assistant, "Hi!"),
]);
// Act
bool result = await strategy.CompactAsync(index);
// Assert — not short-circuited, Apply was called
Assert.True(result);
Assert.Equal(1, strategy.ApplyCallCount);
}
[Fact]
public async Task CompactAsyncDefaultTargetIsInverseOfTriggerAsync()
{
// Arrange — trigger fires when groups > 2
// Default target should be: stop when groups <= 2 (i.e., !trigger)
CompactionTrigger trigger = CompactionTriggers.GroupsExceed(2);
TestStrategy strategy = new(trigger, applyFunc: index =>
{
// Exclude oldest non-system group one at a time
foreach (CompactionMessageGroup group in index.Groups)
{
if (!group.IsExcluded && group.Kind != CompactionGroupKind.System)
{
group.IsExcluded = true;
// Target (default = !trigger) returns true when groups <= 2
// So the strategy would check Target after this exclusion
break;
}
}
return true;
});
CompactionMessageIndex index = CompactionMessageIndex.Create(
[
new ChatMessage(ChatRole.User, "Q1"),
new ChatMessage(ChatRole.Assistant, "A1"),
new ChatMessage(ChatRole.User, "Q2"),
new ChatMessage(ChatRole.Assistant, "A2"),
]);
// Act
bool result = await strategy.CompactAsync(index);
// Assert — trigger fires (4 > 2), Apply is called
Assert.True(result);
Assert.Equal(1, strategy.ApplyCallCount);
}
[Fact]
public async Task CompactAsyncCustomTargetIsPassedToStrategyAsync()
{
// Arrange — custom target that always signals stop
bool targetCalled = false;
bool CustomTarget(CompactionMessageIndex _)
{
targetCalled = true;
return true;
}
TestStrategy strategy = new(_ => true, CustomTarget, _ =>
{
// Access the target from within the strategy
return true;
});
CompactionMessageIndex index = CompactionMessageIndex.Create(
[
new ChatMessage(ChatRole.User, "Hello"),
new ChatMessage(ChatRole.Assistant, "Hi!"),
]);
// Act
await strategy.CompactAsync(index);
// Assert — the custom target is accessible (verified by TestStrategy checking it)
Assert.Equal(1, strategy.ApplyCallCount);
// The target is accessible to derived classes via the protected property
Assert.True(strategy.InvokeTarget(index));
Assert.True(targetCalled);
}
/// <summary>
/// A concrete test implementation of <see cref="CompactionStrategy"/> for testing the base class.
/// </summary>
private sealed class TestStrategy : CompactionStrategy
{
private readonly Func<CompactionMessageIndex, bool>? _applyFunc;
public TestStrategy(
CompactionTrigger trigger,
CompactionTrigger? target = null,
Func<CompactionMessageIndex, bool>? applyFunc = null)
: base(trigger, target)
{
this._applyFunc = applyFunc;
}
public int ApplyCallCount { get; private set; }
/// <summary>
/// Exposes the protected Target property for test verification.
/// </summary>
public bool InvokeTarget(CompactionMessageIndex index) => this.Target(index);
protected override ValueTask<bool> CompactCoreAsync(CompactionMessageIndex index, ILogger logger, CancellationToken cancellationToken)
{
this.ApplyCallCount++;
bool result = this._applyFunc?.Invoke(index) ?? false;
return new(result);
}
}
}