microsoft / microsoft/agent-framework
.NET: [Bug]: Opening user message is dropped from InMemoryChatHistoryProvider when compaction is enabled and the first agent action is a tool call
- Dominant language
- Python
- Stars
- 13.6k
- Forks
- 2.3k
- Avg merge
- 2d 45m
- Merged PRs (30d)
- 358
Description
### Description
# What happened
When a CompactionProvider is registered and the model’s first response is a tool call, the opening user message is not persisted in InMemoryChatHistoryProvider.
After the run completes, the stored history contains:
assistant: FunctionCallContent
tool: FunctionResultContent
assistant: final response
The original user message is missing.
The compaction threshold does not need to be reached. The issue occurs merely when the CompactionProvider is present in the pipeline.
This can result in the opening request being permanently lost when the session is serialized and later rehydrated.
I reproduced this with:
- Microsoft.Agents.AI 1.9.0
- Microsoft.Agents.AI 1.21.0
- .NET 10
The issue does not occur when:
- Compaction is disabled.
- The first model response is a normal text response rather than a tool call.
# What did I expect to happen
The opening user message should be stored in InMemoryChatHistoryProvider regardless of whether:
- A CompactionProvider is registered.
- The first agent action is a tool call.
- Compaction is actually triggered.
After the run, the history should begin with the original user message, followed by the assistant tool call, tool result, and final assistant response.
Steps to reproduce the issue
1. Create an agent using an IChatClient.
2. Add function invocation middleware.
3. Register a CompactionProvider with a threshold high enough that compaction will not occur.
4. Configure the chat client to return a tool call as its first response.
5. Run the agent with an opening user message.
6. Read the session using TryGetInMemoryChatHistory.
7. Observe that the opening user message is absent.
### Code Sample
```markdown
using System.Runtime.CompilerServices;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Compaction;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
#pragma warning disable MAAI001 // Compaction APIs are evaluation-only.
public sealed class FirstUserMessageTests
{
[Fact]
public async Task OpeningUserMessageSurvivesCompactionAndFirstActionToolCall()
{
var tool = AIFunctionFactory.Create(
(string path) => "file contents",
"read_file");
// This threshold is deliberately too high to trigger compaction.
// The bug occurs simply because the provider is registered.
var strategy = new PipelineCompactionStrategy(
[
new TruncationCompactionStrategy(
CompactionTriggers.TokensExceed(800_000),
minimumPreservedGroups: 24,
target: index => index.IncludedTokenCount <= 400_000),
]);
var agent = new ScriptedChatClient()
.AsBuilder()
.UseFunctionInvocation()
.UseAIContextProviders(
new CompactionProvider(
strategy,
stateKey: null,
NullLoggerFactory.Instance))
.BuildAIAgent(new ChatClientAgentOptions
{
ChatOptions = new ChatOptions
{
Tools = [tool],
AllowMultipleToolCalls = true,
},
});
var session = await agent.CreateSessionAsync();
await agent.RunAsync("First question", session);
Assert.True(
session.TryGetInMemoryChatHistory(out var messages),
"No in-memory chat history was stored.");
// Fails: the history contains the tool call, tool result, and final
// assistant response, but not the opening user message.
Assert.Contains(
messages!,
message =>
message.Role == ChatRole.User &&
message.Text.Contains("First question"));
}
private sealed class ScriptedChatClient : IChatClient
{
private int _callCount;
public ChatClientMetadata Metadata { get; } = new("test");
public Task GetResponseAsync(
IEnumerable messages,
ChatOptions? options = null,
CancellationToken cancellationToken = default)
{
if (Interlocked.Increment(ref _callCount) == 1)
{
var toolCall = new FunctionCallContent(
"call_1",
"read_file",
new Dictionary
{
["path"] = "example.cs",
});
return Task.FromResult(
new ChatResponse(
new ChatMessage(ChatRole.Assistant, [toolCall]))
{
FinishReason = ChatFinishReason.ToolCalls,
});
}
return Task.FromResult(
new ChatResponse(
new ChatMessage(ChatRole.Assistant, "Final answer"))
{
FinishReason = ChatFinishReason.Stop,
});
}
public async IAsyncEnumerable
GetStreamingResponseAsync(
IEnumerable messages,
ChatOptions? options = null,
[EnumeratorCancellation]
CancellationToken cancellationToken = default)
{
var response = await GetResponseAsync(
messages,
options,
cancellationToken);
foreach (var message in response.Messages)
{
yield return new ChatResponseUpdate(
message.Role,
message.Contents);
}
}
public object? GetService(
Type serviceType,
object? serviceKey = null) => null;
public void Dispose()
{
}
}
}
The failing assertion reports a collection equivalent to:
[
assistant FunctionCallContent,
tool FunctionResultContent,
assistant "Final answer"
]
There is no ChatRole.User message containing "First question".
```
### Error Messages / Stack Traces
```markdown
```
### Package Versions
Microsoft.Agents.AI: 1.21.0, Microsoft.Agents.AI.Abstractions: 1.21.0 (transitive dependency)
### .NET Version
.NET SDK 10.0.102
### Additional Context
_No response_
Contributor guide
Assessment
This issue has not been assessed yet.