From 554debe1bb8769b913e53e37ba757adb5a1b113d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 12 Mar 2026 23:35:58 +0000
Subject: [PATCH 1/5] Initial plan
From 42f30f885e6eb60146bf30e451afb6c491782025 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 12 Mar 2026 23:45:32 +0000
Subject: [PATCH 2/5] Allow developer to specify custom formatter for
ToolResultCompactionStrategy
Co-authored-by: crickman <66376200+crickman@users.noreply.github.com>
---
.../ToolResultCompactionStrategy.cs | 38 ++++++++-
.../ToolResultCompactionStrategyTests.cs | 81 +++++++++++++++++++
2 files changed, 115 insertions(+), 4 deletions(-)
diff --git a/dotnet/src/Microsoft.Agents.AI/Compaction/ToolResultCompactionStrategy.cs b/dotnet/src/Microsoft.Agents.AI/Compaction/ToolResultCompactionStrategy.cs
index 9b4dbb6b16..24057f2fd8 100644
--- a/dotnet/src/Microsoft.Agents.AI/Compaction/ToolResultCompactionStrategy.cs
+++ b/dotnet/src/Microsoft.Agents.AI/Compaction/ToolResultCompactionStrategy.cs
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
+using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
@@ -30,6 +31,12 @@ namespace Microsoft.Agents.AI.Compaction;
///
///
///
+/// A custom can be supplied to override the default YAML-like
+/// summary format. The formatter receives the being collapsed
+/// and must return the replacement summary string. is the
+/// built-in default and can be reused inside a custom formatter when needed.
+///
+///
/// is a hard floor: even if the
/// has not been reached, compaction will not touch the last non-system groups.
///
@@ -58,14 +65,24 @@ public sealed class ToolResultCompactionStrategy : CompactionStrategy
/// regardless of the target condition.
/// Defaults to , ensuring the current turn's tool interactions remain visible.
///
+ ///
+ /// An optional custom formatter that converts a into a summary string.
+ /// When , is used, which produces a YAML-like
+ /// block listing each tool name and its results.
+ ///
///
/// An optional target condition that controls when compaction stops. When ,
/// defaults to the inverse of the — compaction stops as soon as the trigger would no longer fire.
///
- public ToolResultCompactionStrategy(CompactionTrigger trigger, int minimumPreservedGroups = DefaultMinimumPreserved, CompactionTrigger? target = null)
+ public ToolResultCompactionStrategy(
+ CompactionTrigger trigger,
+ int minimumPreservedGroups = DefaultMinimumPreserved,
+ Func? toolCallFormatter = null,
+ CompactionTrigger? target = null)
: base(trigger, target)
{
this.MinimumPreservedGroups = EnsureNonNegative(minimumPreservedGroups);
+ this.ToolCallFormatter = toolCallFormatter ?? DefaultToolCallFormatter;
}
///
@@ -74,6 +91,15 @@ public ToolResultCompactionStrategy(CompactionTrigger trigger, int minimumPreser
///
public int MinimumPreservedGroups { get; }
+ ///
+ /// Gets the formatter used to convert a into a summary string.
+ ///
+ ///
+ /// Defaults to . Supply a custom function via the constructor
+ /// to override the format of collapsed tool call groups.
+ ///
+ public Func ToolCallFormatter { get; }
+
///
protected override ValueTask CompactCoreAsync(CompactionMessageIndex index, ILogger logger, CancellationToken cancellationToken)
{
@@ -120,7 +146,7 @@ protected override ValueTask CompactCoreAsync(CompactionMessageIndex index
int idx = eligibleIndices[e] + offset;
CompactionMessageGroup group = index.Groups[idx];
- string summary = BuildToolCallSummary(group);
+ string summary = this.ToolCallFormatter(group);
// Exclude the original group and insert a collapsed replacement
group.IsExcluded = true;
@@ -145,10 +171,14 @@ protected override ValueTask CompactCoreAsync(CompactionMessageIndex index
}
///
- /// Builds a concise summary string for a tool call group, including tool names,
+ /// The default formatter that produces a YAML-like summary of tool call groups, including tool names,
/// results, and deduplication counts for repeated tool names.
///
- private static string BuildToolCallSummary(CompactionMessageGroup group)
+ ///
+ /// This is the formatter used when no custom is supplied.
+ /// It can be referenced directly in a custom formatter to augment or wrap the default output.
+ ///
+ public static string DefaultToolCallFormatter(CompactionMessageGroup group)
{
// Collect function calls (callId, name) and results (callId → result text)
List<(string CallId, string Name)> functionCalls = [];
diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Compaction/ToolResultCompactionStrategyTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Compaction/ToolResultCompactionStrategyTests.cs
index b941439988..83b663d0cc 100644
--- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Compaction/ToolResultCompactionStrategyTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Compaction/ToolResultCompactionStrategyTests.cs
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
+using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Compaction;
@@ -348,4 +349,84 @@ public async Task CompactAsyncDeduplicatesWithFunctionResultContentAsync()
List included = [.. groups.GetIncludedMessages()];
Assert.Equal("[Tool Calls]\nget_weather:\n - Sunny\n - Rainy\nsearch_docs:\n - Found 3 docs", included[1].Text);
}
+
+ [Fact]
+ public async Task CompactAsyncUsesCustomFormatterAsync()
+ {
+ // Arrange — custom formatter that produces a collapsed message count
+ static string CustomFormatter(CompactionMessageGroup group) =>
+ $"[Collapsed: {group.Messages.Count} messages]";
+
+ ToolResultCompactionStrategy strategy = new(
+ trigger: _ => true,
+ minimumPreservedGroups: 1,
+ toolCallFormatter: CustomFormatter);
+
+ CompactionMessageIndex groups = CompactionMessageIndex.Create(
+ [
+ new ChatMessage(ChatRole.User, "Q1"),
+ new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("c1", "get_weather")]),
+ new ChatMessage(ChatRole.Tool, "Sunny"),
+ new ChatMessage(ChatRole.User, "Q2"),
+ ]);
+
+ // Act
+ bool result = await strategy.CompactAsync(groups);
+
+ // Assert — custom formatter output used instead of default YAML-like format
+ Assert.True(result);
+ List included = [.. groups.GetIncludedMessages()];
+ Assert.Equal("[Collapsed: 2 messages]", included[1].Text);
+ }
+
+ [Fact]
+ public void ToolCallFormatterPropertyIsNotNullWhenNoneProvided()
+ {
+ // Arrange
+ ToolResultCompactionStrategy strategy = new(CompactionTriggers.Always);
+
+ // Assert — ToolCallFormatter is not null when no custom formatter is provided
+ Assert.NotNull(strategy.ToolCallFormatter);
+ }
+
+ [Fact]
+ public void ToolCallFormatterPropertyReturnsCustomFormatterWhenProvided()
+ {
+ // Arrange
+ Func customFormatter = static _ => "custom";
+ ToolResultCompactionStrategy strategy = new(
+ CompactionTriggers.Always,
+ toolCallFormatter: customFormatter);
+
+ // Assert — ToolCallFormatter is the injected custom function
+ Assert.Same(customFormatter, strategy.ToolCallFormatter);
+ }
+
+ [Fact]
+ public async Task CompactAsyncCustomFormatterCanDelegateToDefaultAsync()
+ {
+ // Arrange — custom formatter that wraps the default output
+ static string WrappingFormatter(CompactionMessageGroup group) =>
+ $"CUSTOM_PREFIX\n{ToolResultCompactionStrategy.DefaultToolCallFormatter(group)}";
+
+ ToolResultCompactionStrategy strategy = new(
+ trigger: _ => true,
+ minimumPreservedGroups: 1,
+ toolCallFormatter: WrappingFormatter);
+
+ CompactionMessageIndex groups = CompactionMessageIndex.Create(
+ [
+ new ChatMessage(ChatRole.User, "Q1"),
+ new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("c1", "fn")]),
+ new ChatMessage(ChatRole.Tool, "result"),
+ new ChatMessage(ChatRole.User, "Q2"),
+ ]);
+
+ // Act
+ await strategy.CompactAsync(groups);
+
+ // Assert — wrapped default output
+ List included = [.. groups.GetIncludedMessages()];
+ Assert.Equal("CUSTOM_PREFIX\n[Tool Calls]\nfn:\n - result", included[1].Text);
+ }
}
From 1c226dabaa4f028255992528c06bd144c9e01015 Mon Sep 17 00:00:00 2001
From: Chris Rickman
Date: Fri, 13 Mar 2026 10:55:00 -0700
Subject: [PATCH 3/5] Refine shape
---
.../ToolResultCompactionStrategy.cs | 23 ++++++-------------
.../ToolResultCompactionStrategyTests.cs | 18 ++++++++++-----
2 files changed, 19 insertions(+), 22 deletions(-)
diff --git a/dotnet/src/Microsoft.Agents.AI/Compaction/ToolResultCompactionStrategy.cs b/dotnet/src/Microsoft.Agents.AI/Compaction/ToolResultCompactionStrategy.cs
index 24057f2fd8..6bb2ed26f6 100644
--- a/dotnet/src/Microsoft.Agents.AI/Compaction/ToolResultCompactionStrategy.cs
+++ b/dotnet/src/Microsoft.Agents.AI/Compaction/ToolResultCompactionStrategy.cs
@@ -65,11 +65,6 @@ public sealed class ToolResultCompactionStrategy : CompactionStrategy
/// regardless of the target condition.
/// Defaults to , ensuring the current turn's tool interactions remain visible.
///
- ///
- /// An optional custom formatter that converts a into a summary string.
- /// When , is used, which produces a YAML-like
- /// block listing each tool name and its results.
- ///
///
/// An optional target condition that controls when compaction stops. When ,
/// defaults to the inverse of the — compaction stops as soon as the trigger would no longer fire.
@@ -77,12 +72,10 @@ public sealed class ToolResultCompactionStrategy : CompactionStrategy
public ToolResultCompactionStrategy(
CompactionTrigger trigger,
int minimumPreservedGroups = DefaultMinimumPreserved,
- Func? toolCallFormatter = null,
CompactionTrigger? target = null)
: base(trigger, target)
{
this.MinimumPreservedGroups = EnsureNonNegative(minimumPreservedGroups);
- this.ToolCallFormatter = toolCallFormatter ?? DefaultToolCallFormatter;
}
///
@@ -92,13 +85,11 @@ public ToolResultCompactionStrategy(
public int MinimumPreservedGroups { get; }
///
- /// Gets the formatter used to convert a into a summary string.
+ /// An optional custom formatter that converts a into a summary string.
+ /// When , is used, which produces a YAML-like
+ /// block listing each tool name and its results.
///
- ///
- /// Defaults to . Supply a custom function via the constructor
- /// to override the format of collapsed tool call groups.
- ///
- public Func ToolCallFormatter { get; }
+ public Func? ToolCallFormatter { get; init; }
///
protected override ValueTask CompactCoreAsync(CompactionMessageIndex index, ILogger logger, CancellationToken cancellationToken)
@@ -146,7 +137,7 @@ protected override ValueTask CompactCoreAsync(CompactionMessageIndex index
int idx = eligibleIndices[e] + offset;
CompactionMessageGroup group = index.Groups[idx];
- string summary = this.ToolCallFormatter(group);
+ string summary = (this.ToolCallFormatter ?? DefaultToolCallFormatter).Invoke(group);
// Exclude the original group and insert a collapsed replacement
group.IsExcluded = true;
@@ -182,7 +173,7 @@ public static string DefaultToolCallFormatter(CompactionMessageGroup group)
{
// Collect function calls (callId, name) and results (callId → result text)
List<(string CallId, string Name)> functionCalls = [];
- Dictionary resultsByCallId = new();
+ Dictionary resultsByCallId = [];
List plainTextResults = [];
foreach (ChatMessage message in group.Messages)
@@ -217,7 +208,7 @@ public static string DefaultToolCallFormatter(CompactionMessageGroup group)
// grouping by tool name while preserving first-seen order.
int plainTextIdx = 0;
List orderedNames = [];
- Dictionary> groupedResults = new();
+ Dictionary> groupedResults = [];
foreach ((string callId, string name) in functionCalls)
{
diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Compaction/ToolResultCompactionStrategyTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Compaction/ToolResultCompactionStrategyTests.cs
index 83b663d0cc..7c7eba871d 100644
--- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Compaction/ToolResultCompactionStrategyTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Compaction/ToolResultCompactionStrategyTests.cs
@@ -359,8 +359,10 @@ static string CustomFormatter(CompactionMessageGroup group) =>
ToolResultCompactionStrategy strategy = new(
trigger: _ => true,
- minimumPreservedGroups: 1,
- toolCallFormatter: CustomFormatter);
+ minimumPreservedGroups: 1)
+ {
+ ToolCallFormatter = CustomFormatter,
+ };
CompactionMessageIndex groups = CompactionMessageIndex.Create(
[
@@ -395,8 +397,10 @@ public void ToolCallFormatterPropertyReturnsCustomFormatterWhenProvided()
// Arrange
Func customFormatter = static _ => "custom";
ToolResultCompactionStrategy strategy = new(
- CompactionTriggers.Always,
- toolCallFormatter: customFormatter);
+ CompactionTriggers.Always)
+ {
+ ToolCallFormatter = customFormatter
+ };
// Assert — ToolCallFormatter is the injected custom function
Assert.Same(customFormatter, strategy.ToolCallFormatter);
@@ -411,8 +415,10 @@ static string WrappingFormatter(CompactionMessageGroup group) =>
ToolResultCompactionStrategy strategy = new(
trigger: _ => true,
- minimumPreservedGroups: 1,
- toolCallFormatter: WrappingFormatter);
+ minimumPreservedGroups: 1)
+ {
+ ToolCallFormatter = WrappingFormatter
+ };
CompactionMessageIndex groups = CompactionMessageIndex.Create(
[
From 4812d2fc0cf60a0ea994e507327cde3a22a01cb9 Mon Sep 17 00:00:00 2001
From: Chris Rickman
Date: Fri, 13 Mar 2026 13:18:21 -0700
Subject: [PATCH 4/5] Fix test expectation
---
.../Compaction/ToolResultCompactionStrategyTests.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Compaction/ToolResultCompactionStrategyTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Compaction/ToolResultCompactionStrategyTests.cs
index 7c7eba871d..66cf35f97d 100644
--- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Compaction/ToolResultCompactionStrategyTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Compaction/ToolResultCompactionStrategyTests.cs
@@ -388,7 +388,7 @@ public void ToolCallFormatterPropertyIsNotNullWhenNoneProvided()
ToolResultCompactionStrategy strategy = new(CompactionTriggers.Always);
// Assert — ToolCallFormatter is not null when no custom formatter is provided
- Assert.NotNull(strategy.ToolCallFormatter);
+ Assert.Null(strategy.ToolCallFormatter);
}
[Fact]
From 0d6cfa237dc9bdae5851dcf8026c137c44a747d9 Mon Sep 17 00:00:00 2001
From: Chris <66376200+crickman@users.noreply.github.com>
Date: Fri, 13 Mar 2026 16:14:16 -0700
Subject: [PATCH 5/5] Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
---
.../Compaction/ToolResultCompactionStrategyTests.cs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Compaction/ToolResultCompactionStrategyTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Compaction/ToolResultCompactionStrategyTests.cs
index 66cf35f97d..c4006a925f 100644
--- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Compaction/ToolResultCompactionStrategyTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Compaction/ToolResultCompactionStrategyTests.cs
@@ -382,12 +382,12 @@ static string CustomFormatter(CompactionMessageGroup group) =>
}
[Fact]
- public void ToolCallFormatterPropertyIsNotNullWhenNoneProvided()
+ public void ToolCallFormatterPropertyIsNullWhenNoneProvided()
{
// Arrange
ToolResultCompactionStrategy strategy = new(CompactionTriggers.Always);
- // Assert — ToolCallFormatter is not null when no custom formatter is provided
+ // Assert — ToolCallFormatter is null when no custom formatter is provided
Assert.Null(strategy.ToolCallFormatter);
}