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); }