From fcd60daed51dd8f56a9755294d72ff6e5cd673cf Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 4 Mar 2026 23:31:18 -0800 Subject: [PATCH] Checkpoint --- dotnet/agent-working-dotnet.slnx | 1 + .../Agent_Step18_CompactionPipeline.csproj | 21 + .../Program.cs | 109 +++++ .../ChatHistoryProvider.cs | 30 -- .../Compaction/ICompactionStrategy.cs | 8 +- .../Compaction/MessageGroup.cs | 10 +- .../{MessageGroups.cs => MessageIndex.cs} | 109 ++++- .../Compaction/PipelineCompactionStrategy.cs | 7 +- .../InMemoryChatHistoryProvider.cs | 57 +-- .../InMemoryChatHistoryProviderOptions.cs | 15 - .../ChatClient/ChatClientAgentOptions.cs | 4 - .../ChatClient/ChatClientExtensions.cs | 1 + .../ChatClient/CompactingChatClient.cs | 75 --- .../Compaction/CompactingChatClient.cs | 133 +++++ .../SummarizationCompactionStrategy.cs | 2 +- .../TruncationCompactionStrategy.cs | 2 +- ...emoryChatHistoryProviderCompactionTests.cs | 455 +++++++++--------- ...ageGroupsTests.cs => MessageIndexTests.cs} | 60 +-- .../PipelineCompactionStrategyTests.cs | 92 ++-- .../TruncationCompactionStrategyTests.cs | 18 +- 20 files changed, 703 insertions(+), 506 deletions(-) create mode 100644 dotnet/samples/02-agents/Agents/Agent_Step18_CompactionPipeline/Agent_Step18_CompactionPipeline.csproj create mode 100644 dotnet/samples/02-agents/Agents/Agent_Step18_CompactionPipeline/Program.cs rename dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/{MessageGroups.cs => MessageIndex.cs} (72%) delete mode 100644 dotnet/src/Microsoft.Agents.AI/ChatClient/CompactingChatClient.cs create mode 100644 dotnet/src/Microsoft.Agents.AI/Compaction/CompactingChatClient.cs rename dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/{MessageGroupsTests.cs => MessageIndexTests.cs} (89%) diff --git a/dotnet/agent-working-dotnet.slnx b/dotnet/agent-working-dotnet.slnx index 651b185a23..60542e7969 100644 --- a/dotnet/agent-working-dotnet.slnx +++ b/dotnet/agent-working-dotnet.slnx @@ -56,6 +56,7 @@ + diff --git a/dotnet/samples/02-agents/Agents/Agent_Step18_CompactionPipeline/Agent_Step18_CompactionPipeline.csproj b/dotnet/samples/02-agents/Agents/Agent_Step18_CompactionPipeline/Agent_Step18_CompactionPipeline.csproj new file mode 100644 index 0000000000..0f9de7c359 --- /dev/null +++ b/dotnet/samples/02-agents/Agents/Agent_Step18_CompactionPipeline/Agent_Step18_CompactionPipeline.csproj @@ -0,0 +1,21 @@ + + + + Exe + net10.0 + + enable + enable + + + + + + + + + + + + + diff --git a/dotnet/samples/02-agents/Agents/Agent_Step18_CompactionPipeline/Program.cs b/dotnet/samples/02-agents/Agents/Agent_Step18_CompactionPipeline/Program.cs new file mode 100644 index 0000000000..82a38c538c --- /dev/null +++ b/dotnet/samples/02-agents/Agents/Agent_Step18_CompactionPipeline/Program.cs @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft. All rights reserved. + +// This sample demonstrates how to use a ChatHistoryCompactionPipeline as the ChatReducer for an agent's +// in-memory chat history. The pipeline chains multiple compaction strategies from gentle to aggressive: +// 1. ToolResultCompactionStrategy - Collapses old tool-call groups into concise summaries +// 2. SummarizationCompactionStrategy - LLM-compresses older conversation spans +// 3. SlidingWindowCompactionStrategy - Keeps only the most recent N user turns +// 4. TruncationCompactionStrategy - Emergency token-budget backstop + +using System.ComponentModel; +using Azure.AI.OpenAI; +using Azure.Identity; +using Microsoft.Agents.AI; +using Microsoft.Agents.AI.Compaction; +using Microsoft.Extensions.AI; + +var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); +var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini"; + +// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production. +// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid +// latency issues, unintended credential probing, and potential security risks from fallback mechanisms. +AzureOpenAIClient openAIClient = new(new Uri(endpoint), new DefaultAzureCredential()); + +// Create a chat client for the agent and a separate one for the summarization strategy. +// Using the same model for simplicity; in production, use a smaller/cheaper model for summarization. +IChatClient agentChatClient = openAIClient.GetChatClient(deploymentName).AsIChatClient(); +IChatClient summarizerChatClient = openAIClient.GetChatClient(deploymentName).AsIChatClient(); + +// Define a tool the agent can use, so we can see tool-result compaction in action. +[Description("Look up the current price of a product by name.")] +static string LookupPrice([Description("The product name to look up.")] string productName) => + productName.ToUpperInvariant() switch + { + "LAPTOP" => "The laptop costs $999.99.", + "KEYBOARD" => "The keyboard costs $79.99.", + "MOUSE" => "The mouse costs $29.99.", + _ => $"Sorry, I don't have pricing for '{productName}'." + }; + +// Configure the compaction pipeline with one of each strategy, ordered least to most aggressive. +//const int MaxTokens = 512; +//const int MaxTurns = 4; +const int MaxGroups = 2; + +PipelineCompactionStrategy compactionPipeline = + new(// 1. Gentle: collapse old tool-call groups into short summaries like "[Tool calls: LookupPrice]" + //new ToolResultCompactionStrategy(MaxTokens, preserveRecentGroups: 2), + + // 2. Moderate: use an LLM to summarize older conversation spans into a concise message + new SummarizationCompactionStrategy(summarizerChatClient, MaxGroups) + + // 3. Aggressive: keep only the last N user turns and their responses + //new SlidingWindowCompactionStrategy(MaxTurns), + + // 4. Emergency: drop oldest groups until under the token budget + //new TruncationCompactionStrategy(MaxGroups) + ); + +// Create the agent with an in-memory chat history provider whose reducer is the compaction pipeline. +AIAgent agent = + agentChatClient.AsAIAgent( + new ChatClientAgentOptions + { + Name = "ShoppingAssistant", + ChatOptions = new() + { + Instructions = + """ + You are a helpful, but long winded, shopping assistant. + Help the user look up prices and compare products. + When responding, Be sure to be extra descriptive and use as + many words as possible without sounding ridiculous. + """, + Tools = [AIFunctionFactory.Create(LookupPrice)], + }, + CompactionStrategy = compactionPipeline, + }); + +AgentSession session = await agent.CreateSessionAsync(); + +// Helper to print chat history size +void PrintChatHistory() +{ + if (session.TryGetInMemoryChatHistory(out var history)) + { + Console.WriteLine($" [Chat history: {history.Count} messages]\n"); + } +} + +// Run a multi-turn conversation with tool calls to exercise the pipeline. +string[] prompts = +[ + "What's the price of a laptop?", + "How about a keyboard?", + "And a mouse?", + "Which product is the cheapest?", + "Can you compare the laptop and the keyboard for me?", + "What was the first product I asked about?", + "Thank you!", +]; + +foreach (string prompt in prompts) +{ + Console.WriteLine($"User: {prompt}"); + Console.WriteLine($"Agent: {await agent.RunAsync(prompt, session)}"); + + PrintChatHistory(); +} diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/ChatHistoryProvider.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/ChatHistoryProvider.cs index cdd21e9e1c..ad3f3aacfb 100644 --- a/dotnet/src/Microsoft.Agents.AI.Abstractions/ChatHistoryProvider.cs +++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/ChatHistoryProvider.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; -using Microsoft.Agents.AI.Compaction; using Microsoft.Extensions.AI; using Microsoft.Shared.Diagnostics; @@ -270,35 +269,6 @@ public abstract class ChatHistoryProvider protected virtual ValueTask StoreChatHistoryAsync(InvokedContext context, CancellationToken cancellationToken = default) => default; - /// - /// Compacts the messages in place using the specified compaction strategy before they are stored. - /// - /// The messages to compact. This list is mutated in place. - /// The compaction strategy to apply. - /// The to monitor for cancellation requests. - /// A task representing the asynchronous operation. The task result is if compaction occurred. - /// - /// - /// This method organizes the messages into atomic units, - /// applies the compaction strategy, and replaces the contents of the list with the compacted result. - /// Tool call groups (assistant message + tool results) are treated as atomic units. - /// - /// - protected static async Task CompactMessagesAsync(List messages, ICompactionStrategy compactionStrategy, CancellationToken cancellationToken = default) - { - MessageGroups groups = MessageGroups.Create(messages); - - bool compacted = await compactionStrategy.CompactAsync(groups, cancellationToken).ConfigureAwait(false); - - if (compacted) - { - messages.Clear(); - messages.AddRange(groups.GetIncludedMessages()); - } - - return compacted; - } - /// Asks the for an object of the specified type . /// The type of object being requested. /// An optional key that can be used to help identify the target service. diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/ICompactionStrategy.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/ICompactionStrategy.cs index 615317062d..c894dd0d19 100644 --- a/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/ICompactionStrategy.cs +++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/ICompactionStrategy.cs @@ -6,11 +6,11 @@ using System.Threading.Tasks; namespace Microsoft.Agents.AI.Compaction; /// -/// Defines a strategy for compacting a to reduce context size. +/// Defines a strategy for compacting a to reduce context size. /// /// /// -/// Compaction strategies operate on instances, which organize messages +/// Compaction strategies operate on 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). /// @@ -23,7 +23,7 @@ namespace Microsoft.Agents.AI.Compaction; /// /// /// -/// Multiple strategies can be composed by applying them sequentially to the same . +/// Multiple strategies can be composed by applying them sequentially to the same . /// /// public interface ICompactionStrategy @@ -34,5 +34,5 @@ public interface ICompactionStrategy /// The message group collection to compact. The strategy mutates this collection in place. /// The to monitor for cancellation requests. /// A task representing the asynchronous operation. The task result is if compaction occurred, otherwise. - Task CompactAsync(MessageGroups groups, CancellationToken cancellationToken = default); + Task CompactAsync(MessageIndex groups, CancellationToken cancellationToken = default); } diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/MessageGroup.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/MessageGroup.cs index d443b15d87..f8d41a41fb 100644 --- a/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/MessageGroup.cs +++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/MessageGroup.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; +using System.Text.Json.Serialization; using Microsoft.Extensions.AI; namespace Microsoft.Agents.AI.Compaction; @@ -21,8 +22,8 @@ namespace Microsoft.Agents.AI.Compaction; /// /// /// Each group tracks its , , and -/// so that can efficiently aggregate totals across all or only included groups. -/// These values are computed by and passed into the constructor. +/// so that can efficiently aggregate totals across all or only included groups. +/// These values are computed by and passed into the constructor. /// /// public sealed class MessageGroup @@ -32,7 +33,7 @@ public sealed class MessageGroup /// /// /// When this key is present with a value of , the message is classified as - /// by . + /// by . /// public static readonly string SummaryPropertyKey = "_is_summary"; @@ -47,6 +48,7 @@ public sealed class MessageGroup /// The zero-based user turn this group belongs to, or for groups that precede /// the first user message (e.g., system messages). /// + [JsonConstructor] public MessageGroup(MessageGroupKind kind, IReadOnlyList messages, int byteCount, int tokenCount, int? turnIndex = null) { this.Kind = kind; @@ -97,7 +99,7 @@ public sealed class MessageGroup /// /// /// Excluded groups are preserved in the collection for diagnostics or storage purposes - /// but are not included when calling . + /// but are not included when calling . /// public bool IsExcluded { get; set; } diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/MessageGroups.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/MessageIndex.cs similarity index 72% rename from dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/MessageGroups.cs rename to dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/MessageIndex.cs index 5661c1516a..c018774d91 100644 --- a/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/MessageGroups.cs +++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/MessageIndex.cs @@ -13,7 +13,7 @@ namespace Microsoft.Agents.AI.Compaction; /// /// /// -/// provides structural grouping of messages into logical units that +/// provides structural grouping of messages into logical units that /// respect the atomic group preservation constraint: tool call assistant messages and their corresponding /// tool result messages are always grouped together. /// @@ -27,9 +27,16 @@ namespace Microsoft.Agents.AI.Compaction; /// and . The collection provides aggregate properties for both /// the total (all groups) and included (non-excluded groups only) counts. /// +/// +/// Instances created via track internal state that enables efficient incremental +/// updates via . This allows caching a instance and +/// appending only new messages without reprocessing the entire history. +/// /// -public sealed class MessageGroups +public sealed class MessageIndex { + private int _currentTurn; + /// /// Gets the list of message groups in this collection. /// @@ -41,25 +48,43 @@ public sealed class MessageGroups public Tokenizer? Tokenizer { get; } /// - /// Initializes a new instance of the class with the specified groups. + /// Gets the number of raw messages that have been processed into groups. + /// + /// + /// This value is set by and updated by . + /// It is used by to determine which messages are new and need processing. + /// + public int ProcessedMessageCount { get; private set; } + + /// + /// Initializes a new instance of the class with the specified groups. /// /// The message groups. /// An optional tokenizer retained for computing token counts when adding new groups. - public MessageGroups(IList groups, Tokenizer? tokenizer = null) + public MessageIndex(IList groups, Tokenizer? tokenizer = null) { this.Groups = groups; this.Tokenizer = tokenizer; + + for (int index = groups.Count - 1; index >= 0; --index) + { + if (this.Groups[0].TurnIndex.HasValue) + { + this._currentTurn = this.Groups[0].TurnIndex!.Value; + break; + } + } } /// - /// Creates a from a flat list of instances. + /// Creates a from a flat list of instances. /// /// The messages to group. /// /// An optional for computing token counts on each group. /// When , token counts are estimated as ByteCount / 4. /// - /// A new with messages organized into logical groups. + /// A new with messages organized into logical groups. /// /// The grouping algorithm: /// @@ -70,11 +95,61 @@ public sealed class MessageGroups /// Assistant messages without tool calls become groups. /// /// - public static MessageGroups Create(IList messages, Tokenizer? tokenizer = null) + public static MessageIndex Create(IList messages, Tokenizer? tokenizer = null) { - List groups = []; - int index = 0; - int currentTurn = 0; + MessageIndex instance = new(new List(), tokenizer); + instance.AppendFromMessages(messages, 0); + return instance; + } + + /// + /// Incrementally updates the groups with new messages from the conversation. + /// + /// + /// The full list of messages for the conversation. This must be the same list (or a replacement with the same + /// prefix) that was used to create or last update this instance. + /// + /// + /// + /// If the message count exceeds , only the new (delta) messages + /// are processed and appended as new groups. Existing groups and their compaction state (exclusions) + /// are preserved, allowing compaction strategies to build on previous results. + /// + /// + /// If the message count is less than (e.g., after storage compaction + /// replaced messages with summaries), all groups are cleared and rebuilt from scratch. + /// + /// + /// If the message count equals , no work is performed. + /// + /// + public void Update(IList allMessages) + { + if (allMessages.Count == this.ProcessedMessageCount) + { + return; // No new messages + } + + if (allMessages.Count < this.ProcessedMessageCount) + { + // Message list shrank (e.g., after storage compaction). Rebuild from scratch. + this.ProcessedMessageCount = 0; + } + + if (this.ProcessedMessageCount == 0) + { + // First update on a manually constructed instance — clear any pre-existing groups + this.Groups.Clear(); + this._currentTurn = 0; + } + + // Process only the delta messages + this.AppendFromMessages(allMessages, this.ProcessedMessageCount); + } + + private void AppendFromMessages(IList messages, int startIndex) + { + int index = startIndex; while (index < messages.Count) { @@ -83,13 +158,13 @@ public sealed class MessageGroups if (message.Role == ChatRole.System) { // System messages are not part of any turn - groups.Add(CreateGroup(MessageGroupKind.System, [message], tokenizer, turnIndex: null)); + this.Groups.Add(CreateGroup(MessageGroupKind.System, [message], this.Tokenizer, turnIndex: null)); index++; } else if (message.Role == ChatRole.User) { - currentTurn++; - groups.Add(CreateGroup(MessageGroupKind.User, [message], tokenizer, currentTurn)); + this._currentTurn++; + this.Groups.Add(CreateGroup(MessageGroupKind.User, [message], this.Tokenizer, this._currentTurn)); index++; } else if (message.Role == ChatRole.Assistant && HasToolCalls(message)) @@ -104,21 +179,21 @@ public sealed class MessageGroups index++; } - groups.Add(CreateGroup(MessageGroupKind.ToolCall, groupMessages, tokenizer, currentTurn)); + this.Groups.Add(CreateGroup(MessageGroupKind.ToolCall, groupMessages, this.Tokenizer, this._currentTurn)); } else if (message.Role == ChatRole.Assistant && IsSummaryMessage(message)) { - groups.Add(CreateGroup(MessageGroupKind.Summary, [message], tokenizer, currentTurn)); + this.Groups.Add(CreateGroup(MessageGroupKind.Summary, [message], this.Tokenizer, this._currentTurn)); index++; } else { - groups.Add(CreateGroup(MessageGroupKind.AssistantText, [message], tokenizer, currentTurn)); + this.Groups.Add(CreateGroup(MessageGroupKind.AssistantText, [message], this.Tokenizer, this._currentTurn)); index++; } } - return new MessageGroups(groups, tokenizer); + this.ProcessedMessageCount = messages.Count; } /// diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/PipelineCompactionStrategy.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/PipelineCompactionStrategy.cs index 0c4af67700..2346d03c32 100644 --- a/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/PipelineCompactionStrategy.cs +++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/Compaction/PipelineCompactionStrategy.cs @@ -9,7 +9,7 @@ namespace Microsoft.Agents.AI.Compaction; /// /// A compaction strategy that executes a sequential pipeline of instances -/// against the same . +/// against the same . /// /// /// @@ -28,7 +28,8 @@ public sealed class PipelineCompactionStrategy : ICompactionStrategy /// Initializes a new instance of the class. /// /// The ordered sequence of strategies to execute. Must not be empty. - public PipelineCompactionStrategy(params IEnumerable strategies) + ///// An optional cache for instances. When , a default is created. + public PipelineCompactionStrategy(params IEnumerable strategies/*, IMessageIndexCache? cache = null*/) { this.Strategies = [.. Throw.IfNull(strategies)]; } @@ -58,7 +59,7 @@ public sealed class PipelineCompactionStrategy : ICompactionStrategy public int? TargetIncludedGroupCount { get; set; } /// - public async Task CompactAsync(MessageGroups groups, CancellationToken cancellationToken = default) + public async Task CompactAsync(MessageIndex groups, CancellationToken cancellationToken = default) { bool anyCompacted = false; diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/InMemoryChatHistoryProvider.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/InMemoryChatHistoryProvider.cs index 4356c1ac3e..5f03ad424a 100644 --- a/dotnet/src/Microsoft.Agents.AI.Abstractions/InMemoryChatHistoryProvider.cs +++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/InMemoryChatHistoryProvider.cs @@ -6,7 +6,6 @@ using System.Linq; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; -using Microsoft.Agents.AI.Compaction; using Microsoft.Extensions.AI; using Microsoft.Shared.Diagnostics; @@ -47,7 +46,6 @@ public sealed class InMemoryChatHistoryProvider : ChatHistoryProvider options?.JsonSerializerOptions); this.ChatReducer = options?.ChatReducer; this.ReducerTriggerEvent = options?.ReducerTriggerEvent ?? InMemoryChatHistoryProviderOptions.ChatReducerTriggerEvent.BeforeMessagesRetrieval; - this.CompactionStrategy = options?.CompactionStrategy; } /// @@ -63,11 +61,6 @@ public sealed class InMemoryChatHistoryProvider : ChatHistoryProvider /// public InMemoryChatHistoryProviderOptions.ChatReducerTriggerEvent ReducerTriggerEvent { get; } - /// - /// Gets the compaction strategy used to compact stored messages. If , no compaction is applied. - /// - public ICompactionStrategy? CompactionStrategy { get; } - /// /// Gets the chat messages stored for the specified session. /// @@ -84,20 +77,21 @@ public sealed class InMemoryChatHistoryProvider : ChatHistoryProvider /// is . public void SetMessages(AgentSession? session, List messages) { - _ = Throw.IfNull(messages); + Throw.IfNull(messages); - var state = this._sessionState.GetOrInitializeState(session); + State state = this._sessionState.GetOrInitializeState(session); state.Messages = messages; } /// protected override async ValueTask> ProvideChatHistoryAsync(InvokingContext context, CancellationToken cancellationToken = default) { - var state = this._sessionState.GetOrInitializeState(context.Session); + State state = this._sessionState.GetOrInitializeState(context.Session); if (this.ReducerTriggerEvent is InMemoryChatHistoryProviderOptions.ChatReducerTriggerEvent.BeforeMessagesRetrieval && this.ChatReducer is not null) { - state.Messages = (await this.ChatReducer.ReduceAsync(state.Messages, cancellationToken).ConfigureAwait(false)).ToList(); + // Apply pre-invocation compaction strategy if configured + await this.CompactMessagesAsync(state, cancellationToken).ConfigureAwait(false); } return state.Messages; @@ -106,46 +100,29 @@ public sealed class InMemoryChatHistoryProvider : ChatHistoryProvider /// protected override async ValueTask StoreChatHistoryAsync(InvokedContext context, CancellationToken cancellationToken = default) { - var state = this._sessionState.GetOrInitializeState(context.Session); + State state = this._sessionState.GetOrInitializeState(context.Session); // Add request and response messages to the provider var allNewMessages = context.RequestMessages.Concat(context.ResponseMessages ?? []); state.Messages.AddRange(allNewMessages); - if (this.ReducerTriggerEvent is InMemoryChatHistoryProviderOptions.ChatReducerTriggerEvent.AfterMessageAdded && this.ChatReducer is not null) + if (this.ReducerTriggerEvent is InMemoryChatHistoryProviderOptions.ChatReducerTriggerEvent.AfterMessageAdded) { - state.Messages = (await this.ChatReducer.ReduceAsync(state.Messages, cancellationToken).ConfigureAwait(false)).ToList(); - } - - // Apply compaction strategy if configured (pre-write compaction) - if (this.CompactionStrategy is not null) - { - await CompactMessagesAsync(state.Messages, this.CompactionStrategy, cancellationToken).ConfigureAwait(false); + // Apply pre-write compaction strategy if configured + await this.CompactMessagesAsync(state, cancellationToken).ConfigureAwait(false); } } - /// - /// Compacts the stored messages for the specified session using the given or configured compaction strategy. - /// - /// The agent session whose stored messages should be compacted. - /// - /// An optional compaction strategy to use. If , the provider's configured - /// is used. If neither is available, an is thrown. - /// - /// The to monitor for cancellation requests. - /// A task representing the asynchronous operation. The task result is if compaction occurred. - /// No compaction strategy is configured or provided. - /// - /// This method enables on-demand compaction of stored history, for example as a maintenance operation. - /// It reads the full stored history, applies the compaction strategy, and writes the compacted result back. - /// - public async Task CompactStorageAsync(AgentSession? session, ICompactionStrategy? compactionStrategy = null, CancellationToken cancellationToken = default) + private async Task CompactMessagesAsync(State state, CancellationToken cancellationToken = default) { - ICompactionStrategy strategy = compactionStrategy ?? this.CompactionStrategy - ?? throw new InvalidOperationException("No compaction strategy is configured or provided."); + if (this.ChatReducer is not null) + { + // ChatReducer takes precedence, if configured + state.Messages = [.. await this.ChatReducer.ReduceAsync(state.Messages, cancellationToken).ConfigureAwait(false)]; + return; + } - var state = this._sessionState.GetOrInitializeState(session); - return await CompactMessagesAsync(state.Messages, strategy, cancellationToken).ConfigureAwait(false); + // %%% TODO: CONSIDER COMPACTION } /// diff --git a/dotnet/src/Microsoft.Agents.AI.Abstractions/InMemoryChatHistoryProviderOptions.cs b/dotnet/src/Microsoft.Agents.AI.Abstractions/InMemoryChatHistoryProviderOptions.cs index b30968aa61..5d15bb416b 100644 --- a/dotnet/src/Microsoft.Agents.AI.Abstractions/InMemoryChatHistoryProviderOptions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Abstractions/InMemoryChatHistoryProviderOptions.cs @@ -74,21 +74,6 @@ public sealed class InMemoryChatHistoryProviderOptions /// public Func, IEnumerable>? ProvideOutputMessageFilter { get; set; } - /// - /// Gets or sets an optional to apply to stored messages after new messages are added. - /// - /// - /// - /// When set, this strategy is applied to the full stored message list after new messages have been appended. - /// This enables pre-write compaction to limit storage size. - /// - /// - /// The compaction strategy organizes messages into atomic groups (preserving tool-call/result pairings) - /// before applying the strategy logic. See for details. - /// - /// - public ICompactionStrategy? CompactionStrategy { get; set; } - /// /// Defines the events that can trigger a reducer in the . /// diff --git a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentOptions.cs b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentOptions.cs index 4e1fe61f12..7deff4056c 100644 --- a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentOptions.cs +++ b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentOptions.cs @@ -59,10 +59,6 @@ public sealed class ChatClientAgentOptions /// The strategy organizes messages into atomic groups (preserving tool-call/result pairings) /// before applying compaction logic. See for details. /// - /// - /// This is separate from the compaction strategy on , - /// which applies pre-write compaction before storing messages. Both can be used together. - /// /// public ICompactionStrategy? CompactionStrategy { get; set; } diff --git a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientExtensions.cs index 8b83830afc..a8d57ec3d0 100644 --- a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientExtensions.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using Microsoft.Agents.AI; +using Microsoft.Agents.AI.Compaction; using Microsoft.Extensions.AI; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; diff --git a/dotnet/src/Microsoft.Agents.AI/ChatClient/CompactingChatClient.cs b/dotnet/src/Microsoft.Agents.AI/ChatClient/CompactingChatClient.cs deleted file mode 100644 index 8e94b840ea..0000000000 --- a/dotnet/src/Microsoft.Agents.AI/ChatClient/CompactingChatClient.cs +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Agents.AI.Compaction; -using Microsoft.Extensions.AI; -using Microsoft.Shared.Diagnostics; - -namespace Microsoft.Agents.AI; - -/// -/// A delegating that applies an to the message list -/// before each call to the inner chat client. -/// -/// -/// -/// This client is used for in-run compaction during the tool loop. It is inserted into the -/// pipeline before the so that -/// compaction is applied before every LLM call, including those triggered by tool call iterations. -/// -/// -/// The compaction strategy organizes messages into atomic groups (preserving tool-call/result pairings) -/// before applying compaction logic. Only included messages are forwarded to the inner client. -/// -/// -internal sealed class CompactingChatClient : DelegatingChatClient -{ - private readonly ICompactionStrategy _compactionStrategy; - - /// - /// Initializes a new instance of the class. - /// - /// The inner chat client to delegate to. - /// The compaction strategy to apply before each call. - public CompactingChatClient(IChatClient innerClient, ICompactionStrategy compactionStrategy) - : base(innerClient) - { - this._compactionStrategy = Throw.IfNull(compactionStrategy); - } - - /// - public override async Task GetResponseAsync( - IEnumerable messages, - ChatOptions? options = null, - CancellationToken cancellationToken = default) - { - List compactedMessages = await this.ApplyCompactionAsync(messages, cancellationToken).ConfigureAwait(false); - return await base.GetResponseAsync(compactedMessages, options, cancellationToken).ConfigureAwait(false); - } - - /// - public override async IAsyncEnumerable GetStreamingResponseAsync( - IEnumerable messages, - ChatOptions? options = null, - [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - List compactedMessages = await this.ApplyCompactionAsync(messages, cancellationToken).ConfigureAwait(false); - await foreach (var update in base.GetStreamingResponseAsync(compactedMessages, options, cancellationToken).ConfigureAwait(false)) - { - yield return update; - } - } - - private async Task> ApplyCompactionAsync(IEnumerable messages, CancellationToken cancellationToken) - { - List messageList = messages as List ?? [.. messages]; - MessageGroups groups = MessageGroups.Create(messageList); - - bool compacted = await this._compactionStrategy.CompactAsync(groups, cancellationToken).ConfigureAwait(false); - - return compacted ? [.. groups.GetIncludedMessages()] : messageList; - } -} diff --git a/dotnet/src/Microsoft.Agents.AI/Compaction/CompactingChatClient.cs b/dotnet/src/Microsoft.Agents.AI/Compaction/CompactingChatClient.cs new file mode 100644 index 0000000000..c7fbf66115 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI/Compaction/CompactingChatClient.cs @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.AI; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Agents.AI.Compaction; + +/// +/// A delegating that applies an to the message list +/// before each call to the inner chat client. +/// +/// +/// +/// This client is used for in-run compaction during the tool loop. It is inserted into the +/// pipeline before the `FunctionInvokingChatClient` so that +/// compaction is applied before every LLM call, including those triggered by tool call iterations. +/// +/// +/// The compaction strategy organizes messages into atomic groups (preserving tool-call/result pairings) +/// before applying compaction logic. Only included messages are forwarded to the inner client. +/// +/// +internal sealed class CompactingChatClient : DelegatingChatClient +{ + private readonly ICompactionStrategy _compactionStrategy; + private readonly ProviderSessionState _sessionState; + + /// + /// Initializes a new instance of the class. + /// + /// The inner chat client to delegate to. + /// The compaction strategy to apply before each call. + public CompactingChatClient(IChatClient innerClient, ICompactionStrategy compactionStrategy) + : base(innerClient) + { + this._compactionStrategy = Throw.IfNull(compactionStrategy); + this._sessionState = new ProviderSessionState( + _ => new State(), + Convert.ToBase64String(BitConverter.GetBytes(compactionStrategy.GetHashCode())), + AgentJsonUtilities.DefaultOptions); + } + + /// + public override async Task GetResponseAsync( + IEnumerable messages, + ChatOptions? options = null, + CancellationToken cancellationToken = default) + { + IEnumerable compactedMessages = await this.ApplyCompactionAsync(messages, cancellationToken).ConfigureAwait(false); + return await base.GetResponseAsync(compactedMessages, options, cancellationToken).ConfigureAwait(false); + } + + /// + public override async IAsyncEnumerable GetStreamingResponseAsync( + IEnumerable messages, + ChatOptions? options = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + IEnumerable compactedMessages = await this.ApplyCompactionAsync(messages, cancellationToken).ConfigureAwait(false); + await foreach (var update in base.GetStreamingResponseAsync(compactedMessages, options, cancellationToken).ConfigureAwait(false)) + { + yield return update; + } + } + + /// + public override object? GetService(Type serviceType, object? serviceKey = null) + { + Throw.IfNull(serviceType); + + return + serviceKey is null && serviceType.IsInstanceOfType(typeof(ICompactionStrategy)) ? + this._compactionStrategy : + base.GetService(serviceType, serviceKey); + } + + private async Task> ApplyCompactionAsync( + IEnumerable messages, CancellationToken cancellationToken) + { + List messageList = messages as List ?? [.. messages]; // %%% TODO - LIST COPY + + AgentRunContext? currentAgentContext = AIAgent.CurrentRunContext; + if (currentAgentContext is null || + currentAgentContext.Session is null) + { + // No session available — no reason to compact + return messages; + } + + State state = this._sessionState.GetOrInitializeState(currentAgentContext.Session); + + MessageIndex messageIndex; + if (state.MessageIndex.Count > 0) + { + // Update existing index + messageIndex = new(state.MessageIndex); + messageIndex.Update(messageList); + } + else + { + // First pass — initialize message index state + messageIndex = MessageIndex.Create(messageList); + } + + // Apply compaction + bool wasCompacted = await this._compactionStrategy.CompactAsync(messageIndex, cancellationToken).ConfigureAwait(false); + + if (wasCompacted) + { + state.MessageIndex = [.. messageIndex.Groups]; // %%% TODO - LIST COPY + } + + return wasCompacted ? messageIndex.GetIncludedMessages() : messageList; + } + + /// + /// Represents the state of a stored in the . + /// + public sealed class State + { + /// + /// Gets or sets the message index. + /// + [JsonPropertyName("messages")] + public List MessageIndex { get; set; } = []; + } +} diff --git a/dotnet/src/Microsoft.Agents.AI/Compaction/SummarizationCompactionStrategy.cs b/dotnet/src/Microsoft.Agents.AI/Compaction/SummarizationCompactionStrategy.cs index 3255d3f998..51c5b4e224 100644 --- a/dotnet/src/Microsoft.Agents.AI/Compaction/SummarizationCompactionStrategy.cs +++ b/dotnet/src/Microsoft.Agents.AI/Compaction/SummarizationCompactionStrategy.cs @@ -58,7 +58,7 @@ public sealed class SummarizationCompactionStrategy : ICompactionStrategy public string SummarizationPrompt { get; } /// - public async Task CompactAsync(MessageGroups groups, CancellationToken cancellationToken = default) + public async Task CompactAsync(MessageIndex groups, CancellationToken cancellationToken = default) { int includedCount = groups.IncludedGroupCount; if (includedCount <= this.MaxGroupsBeforeSummary) diff --git a/dotnet/src/Microsoft.Agents.AI/Compaction/TruncationCompactionStrategy.cs b/dotnet/src/Microsoft.Agents.AI/Compaction/TruncationCompactionStrategy.cs index dbeef544cd..47d729be57 100644 --- a/dotnet/src/Microsoft.Agents.AI/Compaction/TruncationCompactionStrategy.cs +++ b/dotnet/src/Microsoft.Agents.AI/Compaction/TruncationCompactionStrategy.cs @@ -44,7 +44,7 @@ public sealed class TruncationCompactionStrategy : ICompactionStrategy public bool PreserveSystemMessages { get; } /// - public Task CompactAsync(MessageGroups groups, CancellationToken cancellationToken = default) + public Task CompactAsync(MessageIndex groups, CancellationToken cancellationToken = default) { int includedCount = groups.IncludedGroupCount; if (includedCount <= this.MaxGroups) diff --git a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/InMemoryChatHistoryProviderCompactionTests.cs b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/InMemoryChatHistoryProviderCompactionTests.cs index ae02b85779..d9155e90f0 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/InMemoryChatHistoryProviderCompactionTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/InMemoryChatHistoryProviderCompactionTests.cs @@ -1,268 +1,269 @@ // Copyright (c) Microsoft. All rights reserved. -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Agents.AI.Compaction; -using Microsoft.Extensions.AI; -using Moq; +// %%% SAVE - RE-ANALYZE +//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.Abstractions.UnitTests.Compaction; +//namespace Microsoft.Agents.AI.Abstractions.UnitTests.Compaction; -/// -/// Contains tests for the compaction integration with . -/// -public class InMemoryChatHistoryProviderCompactionTests -{ - private static readonly AIAgent s_mockAgent = new Mock().Object; +///// +///// Contains tests for the compaction integration with . +///// +//public class InMemoryChatHistoryProviderCompactionTests +//{ +// private static readonly AIAgent s_mockAgent = new Mock().Object; - private static AgentSession CreateMockSession() => new Mock().Object; +// private static AgentSession CreateMockSession() => new Mock().Object; - [Fact] - public void Constructor_SetsCompactionStrategy_FromOptions() - { - // Arrange - Mock strategy = new(); +// [Fact] +// public void Constructor_SetsCompactionStrategy_FromOptions() +// { +// // Arrange +// Mock strategy = new(); - // Act - InMemoryChatHistoryProvider provider = new(new InMemoryChatHistoryProviderOptions - { - CompactionStrategy = strategy.Object, - }); +// // Act +// InMemoryChatHistoryProvider provider = new(new InMemoryChatHistoryProviderOptions +// { +// CompactionStrategy = strategy.Object, +// }); - // Assert - Assert.Same(strategy.Object, provider.CompactionStrategy); - } +// // Assert +// Assert.Same(strategy.Object, provider.CompactionStrategy); +// } - [Fact] - public void Constructor_CompactionStrategyIsNull_ByDefault() - { - // Arrange & Act - InMemoryChatHistoryProvider provider = new(); +// [Fact] +// public void Constructor_CompactionStrategyIsNull_ByDefault() +// { +// // Arrange & Act +// InMemoryChatHistoryProvider provider = new(); - // Assert - Assert.Null(provider.CompactionStrategy); - } +// // Assert +// Assert.Null(provider.CompactionStrategy); +// } - [Fact] - public async Task StoreChatHistoryAsync_AppliesCompaction_WhenStrategyConfiguredAsync() - { - // Arrange — mock strategy that excludes the first included non-system group - Mock mockStrategy = new(); - mockStrategy.Setup(s => s.CompactAsync(It.IsAny(), It.IsAny())) - .Callback((groups, _) => - { - foreach (MessageGroup group in groups.Groups) - { - if (!group.IsExcluded && group.Kind != MessageGroupKind.System) - { - group.IsExcluded = true; - group.ExcludeReason = "Mock compaction"; - break; - } - } - }) - .ReturnsAsync(true); +// [Fact] +// public async Task StoreChatHistoryAsync_AppliesCompaction_WhenStrategyConfiguredAsync() +// { +// // Arrange — mock strategy that excludes the first included non-system group +// Mock mockStrategy = new(); +// mockStrategy.Setup(s => s.CompactAsync(It.IsAny(), It.IsAny())) +// .Callback((groups, _) => +// { +// foreach (MessageGroup group in groups.Groups) +// { +// if (!group.IsExcluded && group.Kind != MessageGroupKind.System) +// { +// group.IsExcluded = true; +// group.ExcludeReason = "Mock compaction"; +// break; +// } +// } +// }) +// .ReturnsAsync(true); - InMemoryChatHistoryProvider provider = new(new InMemoryChatHistoryProviderOptions - { - CompactionStrategy = mockStrategy.Object, - }); +// InMemoryChatHistoryProvider provider = new(new InMemoryChatHistoryProviderOptions +// { +// CompactionStrategy = mockStrategy.Object, +// }); - AgentSession session = CreateMockSession(); +// AgentSession session = CreateMockSession(); - // Pre-populate with some messages - List existingMessages = - [ - new ChatMessage(ChatRole.User, "First"), - new ChatMessage(ChatRole.Assistant, "Response 1"), - ]; - provider.SetMessages(session, existingMessages); +// // Pre-populate with some messages +// List existingMessages = +// [ +// new ChatMessage(ChatRole.User, "First"), +// new ChatMessage(ChatRole.Assistant, "Response 1"), +// ]; +// provider.SetMessages(session, existingMessages); - // Invoke the store flow with additional messages - List requestMessages = - [ - new ChatMessage(ChatRole.User, "Second"), - ]; - List responseMessages = - [ - new ChatMessage(ChatRole.Assistant, "Response 2"), - ]; +// // Invoke the store flow with additional messages +// List requestMessages = +// [ +// new ChatMessage(ChatRole.User, "Second"), +// ]; +// List responseMessages = +// [ +// new ChatMessage(ChatRole.Assistant, "Response 2"), +// ]; - ChatHistoryProvider.InvokedContext context = new(s_mockAgent, session, requestMessages, responseMessages); +// ChatHistoryProvider.InvokedContext context = new(s_mockAgent, session, requestMessages, responseMessages); - // Act - await provider.InvokedAsync(context); +// // Act +// await provider.InvokedAsync(context); - // Assert - compaction should have removed one group - List storedMessages = provider.GetMessages(session); - Assert.Equal(3, storedMessages.Count); - mockStrategy.Verify(s => s.CompactAsync(It.IsAny(), It.IsAny()), Times.Once); - } +// // Assert - compaction should have removed one group +// List storedMessages = provider.GetMessages(session); +// Assert.Equal(3, storedMessages.Count); +// mockStrategy.Verify(s => s.CompactAsync(It.IsAny(), It.IsAny()), Times.Once); +// } - [Fact] - public async Task StoreChatHistoryAsync_DoesNotCompact_WhenNoStrategyAsync() - { - // Arrange - InMemoryChatHistoryProvider provider = new(); - AgentSession session = CreateMockSession(); +// [Fact] +// public async Task StoreChatHistoryAsync_DoesNotCompact_WhenNoStrategyAsync() +// { +// // Arrange +// InMemoryChatHistoryProvider provider = new(); +// AgentSession session = CreateMockSession(); - List requestMessages = - [ - new ChatMessage(ChatRole.User, "Hello"), - ]; - List responseMessages = - [ - new ChatMessage(ChatRole.Assistant, "Hi!"), - ]; +// List requestMessages = +// [ +// new ChatMessage(ChatRole.User, "Hello"), +// ]; +// List responseMessages = +// [ +// new ChatMessage(ChatRole.Assistant, "Hi!"), +// ]; - ChatHistoryProvider.InvokedContext context = new(s_mockAgent, session, requestMessages, responseMessages); +// ChatHistoryProvider.InvokedContext context = new(s_mockAgent, session, requestMessages, responseMessages); - // Act - await provider.InvokedAsync(context); +// // Act +// await provider.InvokedAsync(context); - // Assert - all messages should be stored - List storedMessages = provider.GetMessages(session); - Assert.Equal(2, storedMessages.Count); - } +// // Assert - all messages should be stored +// List storedMessages = provider.GetMessages(session); +// Assert.Equal(2, storedMessages.Count); +// } - [Fact] - public async Task CompactStorageAsync_CompactsStoredMessagesAsync() - { - // Arrange — mock strategy that excludes the two oldest non-system groups - Mock mockStrategy = new(); - mockStrategy.Setup(s => s.CompactAsync(It.IsAny(), It.IsAny())) - .Callback((groups, _) => - { - int excluded = 0; - foreach (MessageGroup group in groups.Groups) - { - if (!group.IsExcluded && group.Kind != MessageGroupKind.System && excluded < 2) - { - group.IsExcluded = true; - excluded++; - } - } - }) - .ReturnsAsync(true); +// [Fact] +// public async Task CompactStorageAsync_CompactsStoredMessagesAsync() +// { +// // Arrange — mock strategy that excludes the two oldest non-system groups +// Mock mockStrategy = new(); +// mockStrategy.Setup(s => s.CompactAsync(It.IsAny(), It.IsAny())) +// .Callback((groups, _) => +// { +// int excluded = 0; +// foreach (MessageGroup group in groups.Groups) +// { +// if (!group.IsExcluded && group.Kind != MessageGroupKind.System && excluded < 2) +// { +// group.IsExcluded = true; +// excluded++; +// } +// } +// }) +// .ReturnsAsync(true); - InMemoryChatHistoryProvider provider = new(new InMemoryChatHistoryProviderOptions - { - CompactionStrategy = mockStrategy.Object, - }); +// InMemoryChatHistoryProvider provider = new(new InMemoryChatHistoryProviderOptions +// { +// CompactionStrategy = mockStrategy.Object, +// }); - AgentSession session = CreateMockSession(); - provider.SetMessages(session, - [ - new ChatMessage(ChatRole.User, "First"), - new ChatMessage(ChatRole.Assistant, "Response 1"), - new ChatMessage(ChatRole.User, "Second"), - new ChatMessage(ChatRole.Assistant, "Response 2"), - ]); +// AgentSession session = CreateMockSession(); +// provider.SetMessages(session, +// [ +// new ChatMessage(ChatRole.User, "First"), +// new ChatMessage(ChatRole.Assistant, "Response 1"), +// new ChatMessage(ChatRole.User, "Second"), +// new ChatMessage(ChatRole.Assistant, "Response 2"), +// ]); - // Act - bool result = await provider.CompactStorageAsync(session); +// // Act +// bool result = await provider.CompactStorageAsync(session); - // Assert - Assert.True(result); - List messages = provider.GetMessages(session); - Assert.Equal(2, messages.Count); - mockStrategy.Verify(s => s.CompactAsync(It.IsAny(), It.IsAny()), Times.Once); - } +// // Assert +// Assert.True(result); +// List messages = provider.GetMessages(session); +// Assert.Equal(2, messages.Count); +// mockStrategy.Verify(s => s.CompactAsync(It.IsAny(), It.IsAny()), Times.Once); +// } - [Fact] - public async Task CompactStorageAsync_UsesProvidedStrategy_OverDefaultAsync() - { - // Arrange - Mock defaultStrategy = new(); - Mock overrideStrategy = new(); +// [Fact] +// public async Task CompactStorageAsync_UsesProvidedStrategy_OverDefaultAsync() +// { +// // Arrange +// Mock defaultStrategy = new(); +// Mock overrideStrategy = new(); - overrideStrategy.Setup(s => s.CompactAsync(It.IsAny(), It.IsAny())) - .Callback((groups, _) => - { - // Exclude all but the last group - for (int i = 0; i < groups.Groups.Count - 1; i++) - { - groups.Groups[i].IsExcluded = true; - } - }) - .ReturnsAsync(true); +// overrideStrategy.Setup(s => s.CompactAsync(It.IsAny(), It.IsAny())) +// .Callback((groups, _) => +// { +// // Exclude all but the last group +// for (int i = 0; i < groups.Groups.Count - 1; i++) +// { +// groups.Groups[i].IsExcluded = true; +// } +// }) +// .ReturnsAsync(true); - InMemoryChatHistoryProvider provider = new(new InMemoryChatHistoryProviderOptions - { - CompactionStrategy = defaultStrategy.Object, - }); +// InMemoryChatHistoryProvider provider = new(new InMemoryChatHistoryProviderOptions +// { +// CompactionStrategy = defaultStrategy.Object, +// }); - AgentSession session = CreateMockSession(); - provider.SetMessages(session, - [ - new ChatMessage(ChatRole.User, "First"), - new ChatMessage(ChatRole.User, "Second"), - new ChatMessage(ChatRole.User, "Third"), - ]); +// AgentSession session = CreateMockSession(); +// provider.SetMessages(session, +// [ +// new ChatMessage(ChatRole.User, "First"), +// new ChatMessage(ChatRole.User, "Second"), +// new ChatMessage(ChatRole.User, "Third"), +// ]); - // Act - bool result = await provider.CompactStorageAsync(session, overrideStrategy.Object); +// // Act +// bool result = await provider.CompactStorageAsync(session, overrideStrategy.Object); - // Assert - Assert.True(result); - List messages = provider.GetMessages(session); - Assert.Single(messages); - Assert.Equal("Third", messages[0].Text); +// // Assert +// Assert.True(result); +// List messages = provider.GetMessages(session); +// Assert.Single(messages); +// Assert.Equal("Third", messages[0].Text); - // Verify the override was used, not the default - overrideStrategy.Verify(s => s.CompactAsync(It.IsAny(), It.IsAny()), Times.Once); - defaultStrategy.Verify(s => s.CompactAsync(It.IsAny(), It.IsAny()), Times.Never); - } +// // Verify the override was used, not the default +// overrideStrategy.Verify(s => s.CompactAsync(It.IsAny(), It.IsAny()), Times.Once); +// defaultStrategy.Verify(s => s.CompactAsync(It.IsAny(), It.IsAny()), Times.Never); +// } - [Fact] - public async Task CompactStorageAsync_Throws_WhenNoStrategyAvailableAsync() - { - // Arrange - InMemoryChatHistoryProvider provider = new(); - AgentSession session = CreateMockSession(); +// [Fact] +// public async Task CompactStorageAsync_Throws_WhenNoStrategyAvailableAsync() +// { +// // Arrange +// InMemoryChatHistoryProvider provider = new(); +// AgentSession session = CreateMockSession(); - // Act & Assert - await Assert.ThrowsAsync( - () => provider.CompactStorageAsync(session)); - } +// // Act & Assert +// await Assert.ThrowsAsync( +// () => provider.CompactStorageAsync(session)); +// } - [Fact] - public async Task CompactStorageAsync_WithCustomStrategy_AppliesCustomLogicAsync() - { - // Arrange - Mock mockStrategy = new(); - mockStrategy.Setup(s => s.CompactAsync(It.IsAny(), It.IsAny())) - .Callback((groups, _) => - { - // Exclude all user groups - foreach (MessageGroup group in groups.Groups) - { - if (group.Kind == MessageGroupKind.User) - { - group.IsExcluded = true; - } - } - }) - .ReturnsAsync(true); +// [Fact] +// public async Task CompactStorageAsync_WithCustomStrategy_AppliesCustomLogicAsync() +// { +// // Arrange +// Mock mockStrategy = new(); +// mockStrategy.Setup(s => s.CompactAsync(It.IsAny(), It.IsAny())) +// .Callback((groups, _) => +// { +// // Exclude all user groups +// foreach (MessageGroup group in groups.Groups) +// { +// if (group.Kind == MessageGroupKind.User) +// { +// group.IsExcluded = true; +// } +// } +// }) +// .ReturnsAsync(true); - InMemoryChatHistoryProvider provider = new(); - AgentSession session = CreateMockSession(); - provider.SetMessages(session, - [ - new ChatMessage(ChatRole.System, "System"), - new ChatMessage(ChatRole.User, "User message"), - new ChatMessage(ChatRole.Assistant, "Response"), - ]); +// InMemoryChatHistoryProvider provider = new(); +// AgentSession session = CreateMockSession(); +// provider.SetMessages(session, +// [ +// new ChatMessage(ChatRole.System, "System"), +// new ChatMessage(ChatRole.User, "User message"), +// new ChatMessage(ChatRole.Assistant, "Response"), +// ]); - // Act - bool result = await provider.CompactStorageAsync(session, mockStrategy.Object); +// // Act +// bool result = await provider.CompactStorageAsync(session, mockStrategy.Object); - // Assert - Assert.True(result); - List messages = provider.GetMessages(session); - Assert.Equal(2, messages.Count); - Assert.Equal(ChatRole.System, messages[0].Role); - Assert.Equal(ChatRole.Assistant, messages[1].Role); - } -} +// // Assert +// Assert.True(result); +// List messages = provider.GetMessages(session); +// Assert.Equal(2, messages.Count); +// Assert.Equal(ChatRole.System, messages[0].Role); +// Assert.Equal(ChatRole.Assistant, messages[1].Role); +// } +//} diff --git a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/MessageGroupsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/MessageIndexTests.cs similarity index 89% rename from dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/MessageGroupsTests.cs rename to dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/MessageIndexTests.cs index c3dfedd208..5ded224c52 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/MessageGroupsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/MessageIndexTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; using Microsoft.Agents.AI.Compaction; @@ -7,9 +7,9 @@ using Microsoft.Extensions.AI; namespace Microsoft.Agents.AI.Abstractions.UnitTests.Compaction; /// -/// Contains tests for the class. +/// Contains tests for the class. /// -public class MessageGroupsTests +public class MessageIndexTests { [Fact] public void Create_EmptyList_ReturnsEmptyGroups() @@ -18,7 +18,7 @@ public class MessageGroupsTests List messages = []; // Act - MessageGroups groups = MessageGroups.Create(messages); + MessageIndex groups = MessageIndex.Create(messages); // Assert Assert.Empty(groups.Groups); @@ -34,7 +34,7 @@ public class MessageGroupsTests ]; // Act - MessageGroups groups = MessageGroups.Create(messages); + MessageIndex groups = MessageIndex.Create(messages); // Assert Assert.Single(groups.Groups); @@ -52,7 +52,7 @@ public class MessageGroupsTests ]; // Act - MessageGroups groups = MessageGroups.Create(messages); + MessageIndex groups = MessageIndex.Create(messages); // Assert Assert.Single(groups.Groups); @@ -69,7 +69,7 @@ public class MessageGroupsTests ]; // Act - MessageGroups groups = MessageGroups.Create(messages); + MessageIndex groups = MessageIndex.Create(messages); // Assert Assert.Single(groups.Groups); @@ -86,7 +86,7 @@ public class MessageGroupsTests List messages = [assistantMessage, toolResult]; // Act - MessageGroups groups = MessageGroups.Create(messages); + MessageIndex groups = MessageIndex.Create(messages); // Assert Assert.Single(groups.Groups); @@ -109,7 +109,7 @@ public class MessageGroupsTests List messages = [systemMsg, userMsg, assistantToolCall, toolResult, assistantText]; // Act - MessageGroups groups = MessageGroups.Create(messages); + MessageIndex groups = MessageIndex.Create(messages); // Assert Assert.Equal(4, groups.Groups.Count); @@ -134,7 +134,7 @@ public class MessageGroupsTests List messages = [assistantToolCall, toolResult1, toolResult2]; // Act - MessageGroups groups = MessageGroups.Create(messages); + MessageIndex groups = MessageIndex.Create(messages); // Assert Assert.Single(groups.Groups); @@ -150,7 +150,7 @@ public class MessageGroupsTests ChatMessage msg2 = new(ChatRole.Assistant, "Response"); ChatMessage msg3 = new(ChatRole.User, "Second"); - MessageGroups groups = MessageGroups.Create([msg1, msg2, msg3]); + MessageIndex groups = MessageIndex.Create([msg1, msg2, msg3]); groups.Groups[1].IsExcluded = true; // Act @@ -169,7 +169,7 @@ public class MessageGroupsTests ChatMessage msg1 = new(ChatRole.User, "First"); ChatMessage msg2 = new(ChatRole.Assistant, "Response"); - MessageGroups groups = MessageGroups.Create([msg1, msg2]); + MessageIndex groups = MessageIndex.Create([msg1, msg2]); groups.Groups[0].IsExcluded = true; // Act @@ -183,7 +183,7 @@ public class MessageGroupsTests public void IncludedGroupCount_ReflectsExclusions() { // Arrange - MessageGroups groups = MessageGroups.Create( + MessageIndex groups = MessageIndex.Create( [ new ChatMessage(ChatRole.User, "A"), new ChatMessage(ChatRole.Assistant, "B"), @@ -207,7 +207,7 @@ public class MessageGroupsTests List messages = [summaryMessage]; // Act - MessageGroups groups = MessageGroups.Create(messages); + MessageIndex groups = MessageIndex.Create(messages); // Assert Assert.Single(groups.Groups); @@ -227,7 +227,7 @@ public class MessageGroupsTests List messages = [systemMsg, summaryMsg, userMsg]; // Act - MessageGroups groups = MessageGroups.Create(messages); + MessageIndex groups = MessageIndex.Create(messages); // Assert Assert.Equal(3, groups.Groups.Count); @@ -264,7 +264,7 @@ public class MessageGroupsTests public void Create_ComputesByteCount_Utf8() { // Arrange — "Hello" is 5 UTF-8 bytes - MessageGroups groups = MessageGroups.Create([new ChatMessage(ChatRole.User, "Hello")]); + MessageIndex groups = MessageIndex.Create([new ChatMessage(ChatRole.User, "Hello")]); // Assert Assert.Equal(5, groups.Groups[0].ByteCount); @@ -274,7 +274,7 @@ public class MessageGroupsTests public void Create_ComputesByteCount_MultiByteChars() { // Arrange — "café" has a multi-byte 'é' (2 bytes in UTF-8) → 5 bytes total - MessageGroups groups = MessageGroups.Create([new ChatMessage(ChatRole.User, "café")]); + MessageIndex groups = MessageIndex.Create([new ChatMessage(ChatRole.User, "café")]); // Assert Assert.Equal(5, groups.Groups[0].ByteCount); @@ -286,7 +286,7 @@ public class MessageGroupsTests // Arrange — ToolCall group: assistant (tool call, null text) + tool result "OK" (2 bytes) ChatMessage assistantMsg = new(ChatRole.Assistant, [new FunctionCallContent("call1", "fn")]); ChatMessage toolResult = new(ChatRole.Tool, "OK"); - MessageGroups groups = MessageGroups.Create([assistantMsg, toolResult]); + MessageIndex groups = MessageIndex.Create([assistantMsg, toolResult]); // Assert — single ToolCall group with 2 messages Assert.Single(groups.Groups); @@ -298,7 +298,7 @@ public class MessageGroupsTests public void Create_DefaultTokenCount_IsHeuristic() { // Arrange — "Hello world test data!" = 22 UTF-8 bytes → 22 / 4 = 5 estimated tokens - MessageGroups groups = MessageGroups.Create([new ChatMessage(ChatRole.User, "Hello world test data!")]); + MessageIndex groups = MessageIndex.Create([new ChatMessage(ChatRole.User, "Hello world test data!")]); // Assert Assert.Equal(22, groups.Groups[0].ByteCount); @@ -311,7 +311,7 @@ public class MessageGroupsTests // Arrange — message with no text (e.g., pure function call) ChatMessage msg = new(ChatRole.Assistant, [new FunctionCallContent("call1", "get_weather")]); ChatMessage tool = new(ChatRole.Tool, string.Empty); - MessageGroups groups = MessageGroups.Create([msg, tool]); + MessageIndex groups = MessageIndex.Create([msg, tool]); // Assert Assert.Equal(2, groups.Groups[0].MessageCount); @@ -323,7 +323,7 @@ public class MessageGroupsTests public void TotalAggregates_SumAllGroups() { // Arrange - MessageGroups groups = MessageGroups.Create( + MessageIndex groups = MessageIndex.Create( [ new ChatMessage(ChatRole.User, "AAAA"), // 4 bytes new ChatMessage(ChatRole.Assistant, "BBBB"), // 4 bytes @@ -342,7 +342,7 @@ public class MessageGroupsTests public void IncludedAggregates_ExcludeMarkedGroups() { // Arrange - MessageGroups groups = MessageGroups.Create( + MessageIndex groups = MessageIndex.Create( [ new ChatMessage(ChatRole.User, "AAAA"), // 4 bytes new ChatMessage(ChatRole.Assistant, "BBBB"), // 4 bytes @@ -369,7 +369,7 @@ public class MessageGroupsTests ChatMessage assistantMsg = new(ChatRole.Assistant, [new FunctionCallContent("call1", "fn")]); ChatMessage toolResult = new(ChatRole.Tool, "OK"); - MessageGroups groups = MessageGroups.Create([assistantMsg, toolResult]); + MessageIndex groups = MessageIndex.Create([assistantMsg, toolResult]); // Assert — single group with 2 messages Assert.Single(groups.Groups); @@ -383,7 +383,7 @@ public class MessageGroupsTests public void Create_AssignsTurnIndices_SingleTurn() { // Arrange — System (no turn), User + Assistant = turn 1 - MessageGroups groups = MessageGroups.Create( + MessageIndex groups = MessageIndex.Create( [ new ChatMessage(ChatRole.System, "You are helpful."), new ChatMessage(ChatRole.User, "Hello"), @@ -402,7 +402,7 @@ public class MessageGroupsTests public void Create_AssignsTurnIndices_MultiTurn() { // Arrange — 3 user turns - MessageGroups groups = MessageGroups.Create( + MessageIndex groups = MessageIndex.Create( [ new ChatMessage(ChatRole.System, "System prompt."), new ChatMessage(ChatRole.User, "Q1"), @@ -429,7 +429,7 @@ public class MessageGroupsTests ChatMessage assistantToolCall = new(ChatRole.Assistant, [new FunctionCallContent("call1", "get_weather")]); ChatMessage toolResult = new(ChatRole.Tool, "Sunny"); - MessageGroups groups = MessageGroups.Create( + MessageIndex groups = MessageIndex.Create( [ new ChatMessage(ChatRole.User, "What's the weather?"), assistantToolCall, @@ -449,7 +449,7 @@ public class MessageGroupsTests public void GetTurnGroups_ReturnsGroupsForSpecificTurn() { // Arrange - MessageGroups groups = MessageGroups.Create( + MessageIndex groups = MessageIndex.Create( [ new ChatMessage(ChatRole.System, "System."), new ChatMessage(ChatRole.User, "Q1"), @@ -475,7 +475,7 @@ public class MessageGroupsTests public void IncludedTurnCount_ReflectsExclusions() { // Arrange — 2 turns, exclude all groups in turn 1 - MessageGroups groups = MessageGroups.Create( + MessageIndex groups = MessageIndex.Create( [ new ChatMessage(ChatRole.User, "Q1"), new ChatMessage(ChatRole.Assistant, "A1"), @@ -495,7 +495,7 @@ public class MessageGroupsTests public void TotalTurnCount_ZeroWhenNoUserMessages() { // Arrange — only system messages - MessageGroups groups = MessageGroups.Create( + MessageIndex groups = MessageIndex.Create( [ new ChatMessage(ChatRole.System, "System."), ]); @@ -509,7 +509,7 @@ public class MessageGroupsTests public void IncludedTurnCount_PartialExclusion_StillCountsTurn() { // Arrange — turn 1 has 2 groups, only one excluded - MessageGroups groups = MessageGroups.Create( + MessageIndex groups = MessageIndex.Create( [ new ChatMessage(ChatRole.User, "Q1"), new ChatMessage(ChatRole.Assistant, "A1"), diff --git a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/PipelineCompactionStrategyTests.cs b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/PipelineCompactionStrategyTests.cs index eb5cda0997..c6a842dea4 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/PipelineCompactionStrategyTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/Compaction/PipelineCompactionStrategyTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; using System.Threading; @@ -20,17 +20,17 @@ public class PipelineCompactionStrategyTests // Arrange List executionOrder = []; Mock strategy1 = new(); - strategy1.Setup(s => s.CompactAsync(It.IsAny(), It.IsAny())) + strategy1.Setup(s => s.CompactAsync(It.IsAny(), It.IsAny())) .Callback(() => executionOrder.Add("first")) .ReturnsAsync(false); Mock strategy2 = new(); - strategy2.Setup(s => s.CompactAsync(It.IsAny(), It.IsAny())) + strategy2.Setup(s => s.CompactAsync(It.IsAny(), It.IsAny())) .Callback(() => executionOrder.Add("second")) .ReturnsAsync(false); - PipelineCompactionStrategy pipeline = new(strategy1.Object, strategy2.Object); - MessageGroups groups = MessageGroups.Create([new ChatMessage(ChatRole.User, "Hello")]); + PipelineCompactionStrategy pipeline = new([strategy1.Object, strategy2.Object]); + MessageIndex groups = MessageIndex.Create([new ChatMessage(ChatRole.User, "Hello")]); // Act await pipeline.CompactAsync(groups); @@ -44,11 +44,11 @@ public class PipelineCompactionStrategyTests { // Arrange Mock strategy1 = new(); - strategy1.Setup(s => s.CompactAsync(It.IsAny(), It.IsAny())) + strategy1.Setup(s => s.CompactAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(false); - PipelineCompactionStrategy pipeline = new(strategy1.Object); - MessageGroups groups = MessageGroups.Create([new ChatMessage(ChatRole.User, "Hello")]); + PipelineCompactionStrategy pipeline = new([strategy1.Object]); + MessageIndex groups = MessageIndex.Create([new ChatMessage(ChatRole.User, "Hello")]); // Act bool result = await pipeline.CompactAsync(groups); @@ -62,15 +62,15 @@ public class PipelineCompactionStrategyTests { // Arrange Mock strategy1 = new(); - strategy1.Setup(s => s.CompactAsync(It.IsAny(), It.IsAny())) + strategy1.Setup(s => s.CompactAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(false); Mock strategy2 = new(); - strategy2.Setup(s => s.CompactAsync(It.IsAny(), It.IsAny())) + strategy2.Setup(s => s.CompactAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(true); - PipelineCompactionStrategy pipeline = new(strategy1.Object, strategy2.Object); - MessageGroups groups = MessageGroups.Create([new ChatMessage(ChatRole.User, "Hello")]); + PipelineCompactionStrategy pipeline = new([strategy1.Object, strategy2.Object]); + MessageIndex groups = MessageIndex.Create([new ChatMessage(ChatRole.User, "Hello")]); // Act bool result = await pipeline.CompactAsync(groups); @@ -84,22 +84,22 @@ public class PipelineCompactionStrategyTests { // Arrange Mock strategy1 = new(); - strategy1.Setup(s => s.CompactAsync(It.IsAny(), It.IsAny())) + strategy1.Setup(s => s.CompactAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(true); Mock strategy2 = new(); - strategy2.Setup(s => s.CompactAsync(It.IsAny(), It.IsAny())) + strategy2.Setup(s => s.CompactAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(false); - PipelineCompactionStrategy pipeline = new(strategy1.Object, strategy2.Object); - MessageGroups groups = MessageGroups.Create([new ChatMessage(ChatRole.User, "Hello")]); + PipelineCompactionStrategy pipeline = new([strategy1.Object, strategy2.Object]); + MessageIndex groups = MessageIndex.Create([new ChatMessage(ChatRole.User, "Hello")]); // Act await pipeline.CompactAsync(groups); // Assert — both strategies were called - strategy1.Verify(s => s.CompactAsync(It.IsAny(), It.IsAny()), Times.Once); - strategy2.Verify(s => s.CompactAsync(It.IsAny(), It.IsAny()), Times.Once); + strategy1.Verify(s => s.CompactAsync(It.IsAny(), It.IsAny()), Times.Once); + strategy2.Verify(s => s.CompactAsync(It.IsAny(), It.IsAny()), Times.Once); } [Fact] @@ -107,8 +107,8 @@ public class PipelineCompactionStrategyTests { // Arrange — first strategy reduces to target Mock strategy1 = new(); - strategy1.Setup(s => s.CompactAsync(It.IsAny(), It.IsAny())) - .Callback((groups, _) => + strategy1.Setup(s => s.CompactAsync(It.IsAny(), It.IsAny())) + .Callback((groups, _) => { // Exclude the first group to bring count down groups.Groups[0].IsExcluded = true; @@ -116,16 +116,16 @@ public class PipelineCompactionStrategyTests .ReturnsAsync(true); Mock strategy2 = new(); - strategy2.Setup(s => s.CompactAsync(It.IsAny(), It.IsAny())) + strategy2.Setup(s => s.CompactAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(false); - PipelineCompactionStrategy pipeline = new(strategy1.Object, strategy2.Object) + PipelineCompactionStrategy pipeline = new([strategy1.Object, strategy2.Object]) { EarlyStop = true, TargetIncludedGroupCount = 2, }; - MessageGroups groups = MessageGroups.Create( + MessageIndex groups = MessageIndex.Create( [ new ChatMessage(ChatRole.User, "First"), new ChatMessage(ChatRole.Assistant, "Response"), @@ -137,8 +137,8 @@ public class PipelineCompactionStrategyTests // Assert — strategy2 should not have been called Assert.True(result); - strategy1.Verify(s => s.CompactAsync(It.IsAny(), It.IsAny()), Times.Once); - strategy2.Verify(s => s.CompactAsync(It.IsAny(), It.IsAny()), Times.Never); + strategy1.Verify(s => s.CompactAsync(It.IsAny(), It.IsAny()), Times.Once); + strategy2.Verify(s => s.CompactAsync(It.IsAny(), It.IsAny()), Times.Never); } [Fact] @@ -146,20 +146,20 @@ public class PipelineCompactionStrategyTests { // Arrange — first strategy does NOT bring count to target Mock strategy1 = new(); - strategy1.Setup(s => s.CompactAsync(It.IsAny(), It.IsAny())) + strategy1.Setup(s => s.CompactAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(false); Mock strategy2 = new(); - strategy2.Setup(s => s.CompactAsync(It.IsAny(), It.IsAny())) + strategy2.Setup(s => s.CompactAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(false); - PipelineCompactionStrategy pipeline = new(strategy1.Object, strategy2.Object) + PipelineCompactionStrategy pipeline = new([strategy1.Object, strategy2.Object]) { EarlyStop = true, TargetIncludedGroupCount = 1, }; - MessageGroups groups = MessageGroups.Create( + MessageIndex groups = MessageIndex.Create( [ new ChatMessage(ChatRole.User, "First"), new ChatMessage(ChatRole.User, "Second"), @@ -170,8 +170,8 @@ public class PipelineCompactionStrategyTests await pipeline.CompactAsync(groups); // Assert — both strategies were called since target was never reached - strategy1.Verify(s => s.CompactAsync(It.IsAny(), It.IsAny()), Times.Once); - strategy2.Verify(s => s.CompactAsync(It.IsAny(), It.IsAny()), Times.Once); + strategy1.Verify(s => s.CompactAsync(It.IsAny(), It.IsAny()), Times.Once); + strategy2.Verify(s => s.CompactAsync(It.IsAny(), It.IsAny()), Times.Once); } [Fact] @@ -179,27 +179,27 @@ public class PipelineCompactionStrategyTests { // Arrange Mock strategy1 = new(); - strategy1.Setup(s => s.CompactAsync(It.IsAny(), It.IsAny())) + strategy1.Setup(s => s.CompactAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(true); Mock strategy2 = new(); - strategy2.Setup(s => s.CompactAsync(It.IsAny(), It.IsAny())) + strategy2.Setup(s => s.CompactAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(false); - PipelineCompactionStrategy pipeline = new(strategy1.Object, strategy2.Object) + PipelineCompactionStrategy pipeline = new([strategy1.Object, strategy2.Object]) { EarlyStop = true, // TargetIncludedGroupCount is null }; - MessageGroups groups = MessageGroups.Create([new ChatMessage(ChatRole.User, "Hello")]); + MessageIndex groups = MessageIndex.Create([new ChatMessage(ChatRole.User, "Hello")]); // Act await pipeline.CompactAsync(groups); // Assert — both strategies called because no target to check against - strategy1.Verify(s => s.CompactAsync(It.IsAny(), It.IsAny()), Times.Once); - strategy2.Verify(s => s.CompactAsync(It.IsAny(), It.IsAny()), Times.Once); + strategy1.Verify(s => s.CompactAsync(It.IsAny(), It.IsAny()), Times.Once); + strategy2.Verify(s => s.CompactAsync(It.IsAny(), It.IsAny()), Times.Once); } [Fact] @@ -207,8 +207,8 @@ public class PipelineCompactionStrategyTests { // Arrange — pipeline: first exclude oldest 2 non-system groups, then exclude 2 more Mock phase1 = new(); - phase1.Setup(s => s.CompactAsync(It.IsAny(), It.IsAny())) - .Callback((groups, _) => + phase1.Setup(s => s.CompactAsync(It.IsAny(), It.IsAny())) + .Callback((groups, _) => { int excluded = 0; foreach (MessageGroup group in groups.Groups) @@ -223,8 +223,8 @@ public class PipelineCompactionStrategyTests .ReturnsAsync(true); Mock phase2 = new(); - phase2.Setup(s => s.CompactAsync(It.IsAny(), It.IsAny())) - .Callback((groups, _) => + phase2.Setup(s => s.CompactAsync(It.IsAny(), It.IsAny())) + .Callback((groups, _) => { int excluded = 0; foreach (MessageGroup group in groups.Groups) @@ -238,9 +238,9 @@ public class PipelineCompactionStrategyTests }) .ReturnsAsync(true); - PipelineCompactionStrategy pipeline = new(phase1.Object, phase2.Object); + PipelineCompactionStrategy pipeline = new([phase1.Object, phase2.Object]); - MessageGroups groups = MessageGroups.Create( + MessageIndex groups = MessageIndex.Create( [ new ChatMessage(ChatRole.System, "You are helpful."), new ChatMessage(ChatRole.User, "Q1"), @@ -262,8 +262,8 @@ public class PipelineCompactionStrategyTests Assert.Equal("You are helpful.", included[0].Text); Assert.Equal("Q3", included[1].Text); - phase1.Verify(s => s.CompactAsync(It.IsAny(), It.IsAny()), Times.Once); - phase2.Verify(s => s.CompactAsync(It.IsAny(), It.IsAny()), Times.Once); + phase1.Verify(s => s.CompactAsync(It.IsAny(), It.IsAny()), Times.Once); + phase2.Verify(s => s.CompactAsync(It.IsAny(), It.IsAny()), Times.Once); } [Fact] @@ -271,7 +271,7 @@ public class PipelineCompactionStrategyTests { // Arrange PipelineCompactionStrategy pipeline = new(new List()); - MessageGroups groups = MessageGroups.Create([new ChatMessage(ChatRole.User, "Hello")]); + MessageIndex groups = MessageIndex.Create([new ChatMessage(ChatRole.User, "Hello")]); // Act bool result = await pipeline.CompactAsync(groups); diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Compaction/TruncationCompactionStrategyTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Compaction/TruncationCompactionStrategyTests.cs index 41855884a9..7b24966728 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Compaction/TruncationCompactionStrategyTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Compaction/TruncationCompactionStrategyTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. using System.Threading.Tasks; using Microsoft.Agents.AI.Compaction; @@ -16,7 +16,7 @@ public class TruncationCompactionStrategyTests { // Arrange TruncationCompactionStrategy strategy = new(maxGroups: 5); - MessageGroups groups = MessageGroups.Create( + MessageIndex groups = MessageIndex.Create( [ new ChatMessage(ChatRole.User, "Hello"), new ChatMessage(ChatRole.Assistant, "Hi!"), @@ -35,7 +35,7 @@ public class TruncationCompactionStrategyTests { // Arrange TruncationCompactionStrategy strategy = new(maxGroups: 2); - MessageGroups groups = MessageGroups.Create( + MessageIndex groups = MessageIndex.Create( [ new ChatMessage(ChatRole.User, "Hello"), new ChatMessage(ChatRole.Assistant, "Hi!"), @@ -58,7 +58,7 @@ public class TruncationCompactionStrategyTests ChatMessage msg3 = new(ChatRole.User, "Second"); ChatMessage msg4 = new(ChatRole.Assistant, "Response 2"); - MessageGroups groups = MessageGroups.Create([msg1, msg2, msg3, msg4]); + MessageIndex groups = MessageIndex.Create([msg1, msg2, msg3, msg4]); // Act bool result = await strategy.CompactAsync(groups); @@ -82,7 +82,7 @@ public class TruncationCompactionStrategyTests ChatMessage msg2 = new(ChatRole.Assistant, "Response 1"); ChatMessage msg3 = new(ChatRole.User, "Second"); - MessageGroups groups = MessageGroups.Create([systemMsg, msg1, msg2, msg3]); + MessageIndex groups = MessageIndex.Create([systemMsg, msg1, msg2, msg3]); // Act bool result = await strategy.CompactAsync(groups); @@ -109,7 +109,7 @@ public class TruncationCompactionStrategyTests ChatMessage msg2 = new(ChatRole.Assistant, "Response"); ChatMessage msg3 = new(ChatRole.User, "Second"); - MessageGroups groups = MessageGroups.Create([systemMsg, msg1, msg2, msg3]); + MessageIndex groups = MessageIndex.Create([systemMsg, msg1, msg2, msg3]); // Act bool result = await strategy.CompactAsync(groups); @@ -133,7 +133,7 @@ public class TruncationCompactionStrategyTests ChatMessage toolResult = new(ChatRole.Tool, "Sunny"); ChatMessage finalResponse = new(ChatRole.User, "Thanks!"); - MessageGroups groups = MessageGroups.Create([assistantToolCall, toolResult, finalResponse]); + MessageIndex groups = MessageIndex.Create([assistantToolCall, toolResult, finalResponse]); // Act bool result = await strategy.CompactAsync(groups); @@ -152,7 +152,7 @@ public class TruncationCompactionStrategyTests { // Arrange TruncationCompactionStrategy strategy = new(maxGroups: 1); - MessageGroups groups = MessageGroups.Create( + MessageIndex groups = MessageIndex.Create( [ new ChatMessage(ChatRole.User, "Old"), new ChatMessage(ChatRole.User, "New"), @@ -171,7 +171,7 @@ public class TruncationCompactionStrategyTests { // Arrange TruncationCompactionStrategy strategy = new(maxGroups: 1); - MessageGroups groups = MessageGroups.Create( + MessageIndex groups = MessageIndex.Create( [ new ChatMessage(ChatRole.User, "Already excluded"), new ChatMessage(ChatRole.User, "Included 1"),