Microsoft Agent Framework Microsoft.Extensions.AI Created: 12 Jul 2026 Updated: 12 Jul 2026

Reducing Chat History with UseChatReducer

As a conversation grows, sending the entire chat history to the model on every request becomes expensive and eventually hits token limits. Microsoft.Extensions.AI addresses this with chat reduction: the UseChatReducer extension method adds a ReducingChatClient to the ChatClientBuilder pipeline. Every time GetResponseAsync/GetStreamingResponseAsync is called, the ReducingChatClient shrinks the message list via IChatReducer.ReduceAsync before it is sent to the actual model. As a result, even as the conversation history keeps growing, fewer messages (and fewer tokens) are sent to the model on each call.

Note that this feature is currently experimental, so the compiler warning MEAI001 must be suppressed to use it.

Two Built-in IChatReducer Implementations

  1. MessageCountingChatReducer: preserves the system message and keeps only the most recent N messages from the rest (a sliding-window approach). Older messages beyond the limit are discarded entirely.
  2. SummarizingChatReducer: summarizes older messages using an IChatClient. While the number of messages decreases, the context is largely preserved because it is folded into a summary instead of being dropped.

References

  1. Chat reduction (experimental)
  2. ReducingChatClientBuilderExtensions.UseChatReducer
  3. MessageCountingChatReducer
  4. SummarizingChatReducer

Step-by-Step Walkthrough

1. Setup

The API key is read from the OPEN_AI_KEY environment variable, and a base IChatClient is created targeting the gpt-4o-mini model. This base client will later be wrapped with different reducers to demonstrate both strategies.

2. Example 1: Limiting Message Count with MessageCountingChatReducer

A MessageCountingChatReducer is created with targetCount: 4. This means the system message is always kept, and at most 4 additional messages (roughly the last 2 question/answer turns) are forwarded to the model; anything older beyond that limit is dropped completely.

The reducer is attached to the pipeline via UseChatReducer, so the ReducingChatClient applies it automatically on every call. In the example, the reducer is also invoked directly (without making a network call) purely to observe how many messages would actually be sent to the model at each turn, which is useful for demonstrating the sliding-window effect. Note that the local history list keeps growing regardless—the reducer only shrinks what is sent to the model, not the local in-memory history (unless you explicitly replace it with the reduced result).

3. Example 2: Summarizing Old Messages with SummarizingChatReducer

A SummarizingChatReducer is created with targetCount: 2 and threshold: 3. This means nothing happens until the total message count exceeds targetCount + threshold = 5. Once that threshold is crossed, the reducer kicks in: it summarizes the older messages (using the provided chat client) and collapses the history back down to about 2 messages (a summary plus any preserved messages such as the system message). Unlike MessageCountingChatReducer, information is not discarded—it is compressed into a summary, so context is retained even as the message count shrinks.

Full Code Example

// UseChatReducer() and related IChatReducer implementations are still experimental,
// so we suppress the MEAI001 warning.

#pragma warning disable MEAI001

using Microsoft.Extensions.AI;
using OpenAI;

namespace MicrosoftAgentFrameworkLesson.ConsoleApp
{
// UseChatReducer: an extension method that adds a ReducingChatClient to the
// ChatClientBuilder pipeline. Every time GetResponseAsync/GetStreamingResponseAsync is
// called, the ReducingChatClient shrinks the message list via IChatReducer.ReduceAsync
// before it is sent to the actual model; this way, even as the chat history grows, fewer
// messages (and fewer tokens) are sent to the model each time.
// Microsoft.Extensions.AI provides two ready-made IChatReducer implementations:
// - MessageCountingChatReducer: preserves the system message and keeps only the most
// recent N messages from the rest (sliding window logic). Older messages beyond the
// limit are discarded entirely.
// - SummarizingChatReducer: summarizes old messages using an IChatClient; while the
// message count decreases, context is largely preserved.
// Source: https://learn.microsoft.com/en-us/dotnet/ai/ichatclient#chat-reduction-experimental
// Source: https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.ai.reducingchatclientbuilderextensions.usechatreducer
// Source: https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.ai.messagecountingchatreducer
// Source: https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.ai.summarizingchatreducer
public static class ChatReducerDemo
{
public static async Task RunAsync()
{
#region Setup

var apiKey = Environment.GetEnvironmentVariable("OPEN_AI_KEY")
?? throw new InvalidOperationException("Please set the OPEN_AI_KEY environment variable.");

IChatClient baseClient = new OpenAIClient(apiKey)
.GetChatClient("gpt-4o-mini")
.AsIChatClient();

Console.WriteLine("====== UseChatReducer (Reducing Chat History) Example ======\n");

#endregion

#region Example 1: Limiting Message Count with MessageCountingChatReducer

Console.WriteLine("--- Example 1: MessageCountingChatReducer ---");

// The system message is always preserved; beyond it, at most 4 messages
// (roughly the last 2 question/answer turns) are sent to the model, and
// anything older than that is discarded.
var countingReducer = new MessageCountingChatReducer(targetCount: 4);

IChatClient countingClient = baseClient
.AsBuilder()
.UseChatReducer(countingReducer)
.Build();

List<ChatMessage> history =
[
new(ChatRole.System, "You are a helpful assistant that gives short and clear answers.")
];

string[] questions =
[
"Hello, my name is Furkan.",
"My favorite color is blue.",
"My favorite programming language is C#.",
"What was my name?",
"Do you remember my favorite color?"
];

foreach (var question in questions)
{
history.Add(new ChatMessage(ChatRole.User, question));

// The ReducingChatClient performs this reduction automatically on every
// request; here we call the same reducer directly to observe how many
// messages will actually be sent to the model (this is free since it
// doesn't make a network call).
var reduced = await countingReducer.ReduceAsync(history, CancellationToken.None);
Console.WriteLine($"[Local history: {history.Count} messages -> Sent to model: {reduced.Count()} messages]");

ChatResponse response = await countingClient.GetResponseAsync(history);
Console.WriteLine($"Q: {question}");
Console.WriteLine($"A: {response.Text}");
Console.WriteLine();

// Note: history keeps growing here; the reducer only shrinks the request
// sent to the model. If you also want to limit local memory, you can
// replace history with 'reduced'.
history.AddMessages(response);
}

#endregion

#region Example 2: Summarizing Old Messages with SummarizingChatReducer

Console.WriteLine("--- Example 2: SummarizingChatReducer ---");

// Once the message count exceeds (targetCount + threshold =) 5, the oldest
// messages are collapsed into a single summary message using the provided
// chat client. Unlike MessageCountingChatReducer, information is not deleted
// entirely; it remains in context by being summarized.
/*
In this example, targetCount: 2, threshold: 3 means:
Nothing happens until the total message count exceeds 2 + 3 = 5.
As soon as the message count exceeds 5, the reducer kicks in, summarizes the
old messages, and collapses the history back down to 2 messages (a summary
plus any preserved messages such as the system message).
*/
var summarizingReducer = new SummarizingChatReducer(
chatClient: baseClient,
targetCount: 2,
threshold: 3);

IChatClient summarizingClient = baseClient
.AsBuilder()
.UseChatReducer(summarizingReducer)
.Build();

List<ChatMessage> history2 =
[
new(ChatRole.System, "You are an assistant that knows the user and gives consistent answers.")
];

string[] questions2 =
[
"Hello, I live in Istanbul.",
"My biggest hobby is reading books.",
"I finished 5 novels last month.",
"Try to guess my favorite author.",
"Do you remember where I live?"
];

foreach (var question in questions2)
{
history2.Add(new ChatMessage(ChatRole.User, question));

Console.WriteLine($"Q: {question}");
ChatResponse response2 = await summarizingClient.GetResponseAsync(history2);
Console.WriteLine($"A: {response2.Text}");
Console.WriteLine();

history2.AddMessages(response2);
}

Console.WriteLine($"[Total number of messages in local history: {history2.Count}]");

#endregion
}
}
}

Summary

UseChatReducer lets you keep sending long-running conversations to the model without exceeding token limits or paying for redundant context on every call. MessageCountingChatReducer is a simple, cheap sliding-window strategy that discards old messages outright, which is ideal when older context is no longer relevant. SummarizingChatReducer trades an extra model call for preserving the gist of older context through summarization, which is ideal when the conversation needs to "remember" details from far earlier turns. Both reducers plug into the same ChatClientBuilder pipeline via UseChatReducer, so switching strategies is just a matter of swapping the IChatReducer implementation.


Share this lesson: