.NET Compaction - Introducing compaction strategies and pipeline#4533
.NET Compaction - Introducing compaction strategies and pipeline#4533
Conversation
Co-authored-by: crickman <66376200+crickman@users.noreply.github.com>
dotnet/src/Microsoft.Agents.AI/Compaction/TruncationCompactionStrategy.cs
Outdated
Show resolved
Hide resolved
dotnet/src/Microsoft.Agents.AI/Compaction/ToolResultCompactionStrategy.cs
Show resolved
Hide resolved
dotnet/src/Microsoft.Agents.AI/Compaction/SummarizationCompactionStrategy.cs
Outdated
Show resolved
Hide resolved
|
|
||
| ### Registering through `ChatClientAgentOptions` | ||
|
|
||
| `AIContextProviders` can also be specified directly on `ChatClientAgentOptions` instead of calling `UseAIContextProviders` on the builder: |
There was a problem hiding this comment.
Consider also adding the impact of this choice here, similar to in the sample itself, i.e. that it doesn't do in function loop compaction.
There was a problem hiding this comment.
Agreed, we need to find a way to make the decision on which compaction system (MEAI vs this) to use very clear in samples and docs.
|
|
||
| /// <summary> | ||
| /// A compaction strategy that removes the oldest user turns and their associated response groups | ||
| /// to bound conversation length. |
There was a problem hiding this comment.
I've read the comments for SlidingWindow and Truncation, and it's not clear to me why I would pick the one over the other. In terms of naming I would have thought that truncation is a type of sliding window as well, in that it's essentially a window of the most recent x messages. Is there some additional core functionality that we should highlight that differentiates the two, both in naming and xml docs?
There was a problem hiding this comment.
Yes, agree they are both similiar. I struggled with that as well. As it stands, truncation is more granular in that it operates at the message (message group) level, while sliding window lops of an entire user turn. (SlidingWindow more agressive than Truncation)
I'll re-review the original requirements/analysis and also see if comments can't be improved.
dotnet/src/Microsoft.Agents.AI/Compaction/ToolResultCompactionStrategy.cs
Show resolved
Hide resolved
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
dotnet/src/Microsoft.Agents.AI/Compaction/SlidingWindowCompactionStrategy.cs
Outdated
Show resolved
Hide resolved
dotnet/src/Microsoft.Agents.AI/Compaction/SummarizationCompactionStrategy.cs
Show resolved
Hide resolved
dotnet/src/Microsoft.Agents.AI.Abstractions/InMemoryChatHistoryProvider.cs
Outdated
Show resolved
Hide resolved
| // Extract tool names from FunctionCallContent | ||
| List<string> toolNames = []; | ||
| foreach (ChatMessage message in group.Messages) | ||
| { | ||
| if (message.Contents is not null) | ||
| { | ||
| foreach (AIContent content in message.Contents) | ||
| { | ||
| if (content is FunctionCallContent fcc) | ||
| { | ||
| toolNames.Add(fcc.Name); | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
toolNames may include duplicates (e.g., repeated tool calls in the same assistant message or multiple function-call contents). Consider de-duplicating (and possibly ordering) the tool names before generating the summary string so the collapsed output is stable and easier to read/debug.
| group.IsExcluded = true; | ||
| group.ExcludeReason = $"Collapsed by {nameof(ToolResultCompactionStrategy)}"; | ||
|
|
||
| string summary = $"[Tool calls: {string.Join(", ", toolNames)}]"; |
There was a problem hiding this comment.
toolNames may include duplicates (e.g., repeated tool calls in the same assistant message or multiple function-call contents). Consider de-duplicating (and possibly ordering) the tool names before generating the summary string so the collapsed output is stable and easier to read/debug.
| /// <summary> | ||
| /// Gets the total number of original messages (that are not summaries). | ||
| /// </summary> | ||
| public int RawMessageCount => this.Groups.Where(group => group.Kind != CompactionGroupKind.Summary).Sum(group => group.MessageCount); |
There was a problem hiding this comment.
Is there a way to cache any of these computations such that we don't iterate the entire index for every check?
There was a problem hiding this comment.
Good call out...the thought had crossed my mind, but I was hesitant to get too fancy. I think we can definately do an efficiency improvement here...i've already got an idea, but don't want to slip Atul's ship goal:
dotnet/src/Microsoft.Agents.AI/Compaction/SummarizationCompactionStrategy.cs
Outdated
Show resolved
Hide resolved
dotnet/src/Microsoft.Agents.AI/Compaction/SummarizationCompactionStrategy.cs
Show resolved
Hide resolved
dotnet/src/Microsoft.Agents.AI.Abstractions/InMemoryChatHistoryProvider.cs
Outdated
Show resolved
Hide resolved
…ionStrategy.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…yProvider.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
dotnet/src/Microsoft.Agents.AI.Abstractions/InMemoryChatHistoryProvider.cs
Show resolved
Hide resolved
|
|
||
| ## Expected Behavior | ||
|
|
||
| The sample runs a seven-turn shopping-assistant conversation with tool calls. After each turn it prints the current in-memory message count so you can observe the pipeline compacting the history as the conversation grows. |
There was a problem hiding this comment.
The README says the sample prints the in-memory message count so you can observe the pipeline compacting the history, but the code prints the InMemoryChatHistoryProvider stored history count (which generally continues to grow) rather than the compacted message set sent to the model. Either adjust the text to clarify it’s printing stored history growth, or change the sample to report the compacted message count/view.
| The sample runs a seven-turn shopping-assistant conversation with tool calls. After each turn it prints the current in-memory message count so you can observe the pipeline compacting the history as the conversation grows. | |
| The sample runs a seven-turn shopping-assistant conversation with tool calls. After each turn it prints the total number of messages stored in the in-memory history provider. This stored history count will generally grow over time, while the compaction pipeline independently controls how many of those messages are actually sent to the model in each turn. |
| ChatMessage summaryMessage = new(ChatRole.Assistant, $"[Summary]\n{summaryText}"); | ||
| (summaryMessage.AdditionalProperties ??= [])[CompactionMessageGroup.SummaryPropertyKey] = true; | ||
|
|
||
| index.InsertGroup(insertIndex, CompactionGroupKind.Summary, [summaryMessage]); |
There was a problem hiding this comment.
SummarizationCompactionStrategy inserts the summary group without a turnIndex, so it ends up with TurnIndex = null. That means it won’t participate in turn-based metrics/strategies (e.g., sliding-window compaction) and may be preserved differently than intended. Consider passing an appropriate turn index (for example, the TurnIndex of the first summarized group) when inserting the summary group.
| index.InsertGroup(insertIndex, CompactionGroupKind.Summary, [summaryMessage]); | |
| // Use the turn index of the first summarized group so the summary participates in turn-based strategies | |
| var summaryTurnIndex = excludedGroups.Count > 0 ? excludedGroups[0].TurnIndex : null; | |
| index.InsertGroup(insertIndex, CompactionGroupKind.Summary, [summaryMessage], summaryTurnIndex); |
| if (session.TryGetInMemoryChatHistory(out var history)) | ||
| { | ||
| Console.ForegroundColor = ConsoleColor.Cyan; | ||
| Console.WriteLine($"\n[Messages: x{history.Count}]\n"); |
There was a problem hiding this comment.
The sample output label prints "[Messages: x{history.Count}]"; the x looks accidental and makes the count harder to read. Consider changing it to just "[Messages: {history.Count}]" (or similar).
| Console.WriteLine($"\n[Messages: x{history.Count}]\n"); | |
| Console.WriteLine($"\n[Messages: {history.Count}]\n"); |
Motivation and Context
Context management (compaction) is one of the key features in the "dev-harness" effort. This change introduces structured handling of long-running AI chat conversations by compacting historical context while preserving key decisions and intent. By reducing token growth and context drift, it improves response quality, performance, and cost predictability over extended sessions. The implementation is designed to be extensible and transparent, making context lifecycle management a first‑class concern for agent development.
[Spec]
Description
The goal of this approach is inject compaction as part of the
IChatClientarchitecture. This approach maintains an index that allows for presenting a compacted version of the conversation without modifying the source "chat history".Features
IChatReducerDetails
Compaction occurs via a
CompactionStrategy. A set of strategies are incuded as part of this initial release, including a pipeline strategy that is able to sequentially apply one or more strategies:ToolResultCompactionStrategySummarizationCompactionStrategySlidingWindowCompactionStrategyTruncationCompactionStrategyPipelineCompactionStrategyChatReducerCompactionStrategyIChatReducerIChatReducerCode
or
Contribution Checklist