Skip to content

.NET: Fix function call and tool result messages not stored in session when using ChatReducer with AfterMessageAdded trigger#4749

Draft
Copilot wants to merge 2 commits intomainfrom
copilot/fix-function-call-storage-issue
Draft

.NET: Fix function call and tool result messages not stored in session when using ChatReducer with AfterMessageAdded trigger#4749
Copilot wants to merge 2 commits intomainfrom
copilot/fix-function-call-storage-issue

Conversation

Copy link
Contributor

Copilot AI commented Mar 17, 2026

Motivation and Context

When InMemoryChatHistoryProvider is configured with a ChatReducer and ReducerTriggerEvent = AfterMessageAdded, function call and tool result messages from the current run are immediately stripped from the session — even when the reducer's target count far exceeds the actual message count (e.g., MessageCountingChatReducer(20) with only 4 messages).

Fixes #issue.

Description

Root cause: MessageCountingChatReducer (from MEAI) always normalizes the conversation by stripping function call/result pairs regardless of count — it treats them as ephemeral intermediate messages. With the previous AfterMessageAdded implementation, the reducer ran on the combined state (old + newly added messages), so current-turn tool calls were immediately removed.

Before (order of operations in StoreChatHistoryAsync):

  1. Add all new messages (user + function calls + tool results + final assistant) to state.Messages
  2. Run reducer → strips current turn's tool calls

After:

  1. Run reducer on existing (pre-turn) messages only → strips old history as expected
  2. Add new current-turn messages → always preserved in full

This makes AfterMessageAdded behaviorally consistent with BeforeMessagesRetrieval: both reduce old messages while preserving the complete current-turn message set. The practical distinction remains: BeforeMessagesRetrieval limits the context window sent to the LLM; AfterMessageAdded limits what's persisted in storage without affecting the current invocation's context.

Changes:

  • InMemoryChatHistoryProvider.StoreChatHistoryAsync — reorder: reduce existing messages, then append new ones
  • AddMessagesAsync_WithReducer_AfterMessageAdded_InvokesReducerAsync test — updated to pre-populate state with existing messages and verify reducer operates only on those, not newly added ones
  • Added AddMessagesAsync_WithReducer_AfterMessageAdded_PreservesCurrentTurnFunctionCallsAsync — regression test verifying function call and tool result messages are preserved when an aggressive reducer is configured

Contribution Checklist

  • The code builds clean without any errors or warnings
  • The PR follows the Contribution Guidelines
  • All unit tests pass, and I have added new tests where possible
  • Is this a breaking change? If yes, add "[BREAKING]" prefix to the title of the PR.
Original prompt

This section details on the original issue you should resolve

<issue_title>.NET: [Bug]: Function call and tool result messages are not stored in the session when using ChatReducer</issue_title>
<issue_description>### Description

Consider the following code:

#!/usr/bin/env dotnet

#:sdk Microsoft.NET.Sdk

#:property OutputType=Exe
#:property TargetFramework=net10.0
#:property ImplicitUsings=enable
#:property Nullable=enable
#:property NoWarn=$(NoWarn);MEAI001
#:property PublishAot=false

#:package Azure.AI.OpenAI@2.1.0
#:package Azure.Identity@1.18.0
#:package Microsoft.Agents.AI.OpenAI@1.0.0-rc3

using System.ComponentModel;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using OpenAI.Chat;

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-4.1";

[Description("Get the weather for a given location.")]
static string GetWeather([Description("The location to get the weather for.")] string location)
    => $"The weather in {location} is cloudy with a high of 15°C.";

AIAgent agent = new AzureOpenAIClient(
    new Uri(endpoint),
    new DefaultAzureCredential())
    .GetChatClient(deploymentName).AsIChatClient()
    .AsAIAgent(new ChatClientAgentOptions
    {
        Name = "Assistant",
        //ChatHistoryProvider = new InMemoryChatHistoryProvider(new()
        //{
        //    ChatReducer = new MessageCountingChatReducer(20),
        //    ReducerTriggerEvent = InMemoryChatHistoryProviderOptions.ChatReducerTriggerEvent.AfterMessageAdded,
        //}),
        ChatOptions = new ChatOptions
        {
            Tools = [AIFunctionFactory.Create(GetWeather)]
        }
    });

var session = await agent.CreateSessionAsync();
var response = await agent.RunAsync("What is the weather like in Taggia?", session);
Console.WriteLine(response);

session.TryGetInMemoryChatHistory(out var messages);
Console.WriteLine($"Messages in session: {messages.Count}");

In particular, the setting of ChatHistoryProvider has been commented out. With this configuration, after calling RunAsync, I have 4 messages in the session:

{
   "messages":[
      {
         "role":"user",
         "contents":[
            {
               "$type":"text",
               "text":"What is the weather like in Taggia?"
            }
         ]
      },
      {
         "authorName":"Assistant",
         "createdAt":"2026-03-05T09:14:51+00:00",
         "role":"assistant",
         "contents":[
            {
               "$type":"functionCall",
               "callId":"call_19muVFBQeEth4xsPu3OAckUP",
               "name":"_Main_g_GetWeather_0_0",
               "arguments":{
                  "location":"Taggia"
               },
               "informationalOnly":true
            }
         ],
         "messageId":"chatcmpl-DFz59TbFUrm2bR3ZeRogTiO4W0vpb"
      },
      {
         "authorName":"Assistant",
         "role":"tool",
         "contents":[
            {
               "$type":"functionResult",
               "callId":"call_19muVFBQeEth4xsPu3OAckUP",
               "result":"The weather in Taggia is cloudy with a high of 15°C."
            }
         ]
      },
      {
         "authorName":"Assistant",
         "createdAt":"2026-03-05T09:14:52+00:00",
         "role":"assistant",
         "contents":[
            {
               "$type":"text",
               "text":"The weather in Taggia is currently cloudy with a high temperature of 15°C."
            }
         ],
         "messageId":"chatcmpl-DFz5A3EVZ0egQlwhVFv11oHYgqnXP"
      }
   ]
}

However, if I uncomment the setting of ChatHistoryProvider to use a ChatReducer, when I call RunAsync, I get only 2 messages:

{
   "messages":[
      {
         "role":"user",
         "contents":[
            {
               "$type":"text",
               "text":"What is the weather like in Taggia?"
            }
         ]
      },
      {
         "authorName":"Assistant",
         "createdAt":"2026-03-05T09:16:15+00:00",
         "role":"assistant",
         "contents":[
            {
               "$type":"text",
               "text":"The weather in Taggia is currently cloudy, with a high temperature of 15°C."
            }
         ],
         "messageId":"chatcmpl-DFz6VO7mqr1FNCQLTgJpYROGGolJo"
      }
   ]
}

Function call and Function result messages aren't present in the session. I have also tried to explicitly set StorageInputRequestMessageFilter and StorageInputResponseMessageFilter properties:

ChatHistoryProvider = new InMemoryChatHistoryProvider(new()
{
    ChatReducer = new MessageCountingChatReducer(20),
    ReducerTriggerEvent = InMemoryChatHistoryProviderOptions.ChatReducerTriggerEvent.AfterMessageAdded,
    StorageInputRequestMessageFilter = messages...

</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

- Fixes microsoft/agent-framework#4494

<!-- START COPILOT CODING AGENT TIPS -->
---

📍 Connect Copilot coding agent with [Jira](https://gh.io/cca-jira-docs), [Azure Boards](https://gh.io/cca-azure-boards-docs) or [Linear](https://gh.io/cca-linear-docs) to delegate work to Copilot in one click without leaving your project management tool.

…g ChatReducer with AfterMessageAdded trigger

Co-authored-by: crickman <66376200+crickman@users.noreply.github.com>
Copilot AI changed the title [WIP] [Bug] Fix function call and tool result messages storage issue .NET: Fix function call and tool result messages not stored in session when using ChatReducer with AfterMessageAdded trigger Mar 17, 2026
Copilot AI requested a review from crickman March 17, 2026 21:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants