microsoft / microsoft/agent-framework

.NET: CompactionProvider stamps input messages in-place with ChatHistory, causing DefaultExcludeChatHistoryFilter to drop the first user message

Open
#6,972 2 comments 2 reactions 1 assignee Claimed by @westey-m View on GitHub
.NET compaction reproduced
Dominant language
Python
Stars
13.6k
Forks
2.3k
Avg merge
2d 45m
Merged PRs (30d)
358

Description

# [Bug]: CompactionProvider stamps input messages in-place with ChatHistory source type, causing DefaultExcludeChatHistoryFilter to drop the first user message on the next turn

## Description

When `CompactionProvider.InvokingCoreAsync` restores state from `ProviderSessionState`, it stamps all cached message objects with `AgentRequestMessageSourceType.ChatHistory` by modifying `AdditionalProperties` **in-place** (without cloning).

Because `AgentSessionStateBagValue` caches the deserialized object reference, the stamped `ChatMessage` instance is the **same object** referenced by `inputMessagesForChatClient` in `ChatClientAgent.RunCoreAsync`. When `NotifyProvidersOfNewMessagesAsync` later applies `DefaultExcludeChatHistoryFilter`, the first user message is silently excluded from `InMemoryChatHistoryProvider` storage. On the next turn, the message is permanently lost — both from the provider and from serialized sessions.

## To Reproduce

Key configuration that triggers the bug:

```csharp
var finalClient = baseAgent.ChatClient.AsBuilder()
.UseNonApprovalRequiredFunctionBypassing()
.UseFunctionInvocation()
.UseAIContextProviders(new CompactionProvider(new PipelineCompactionStrategy()))
.Build();
```

Steps:
1. Create a `ChatClientAgent` with `InMemoryChatHistoryProvider` and the pipeline above
2. Send a message that triggers two tool calls
3. Check the ChatHistoryProvider state — the first user message is missing
4. Send a second message — the agent has no record of the first message

## Observed Behavior

After Turn 1 with tool calls:
- ChatHistoryProvider stores only the response messages (assistant tool calls + tool results + text), **but not the first user message**
- The user message already has `src=ChatHistory` on the first call to the inner pipeline, because `CompactionProvider` stamped it in-place
- Serializing the session and loading it back loses the first message permanently

## Expected Behavior

The first user message should be preserved in `InMemoryChatHistoryProvider` across turns.

## Related

- PR #5149 fixed the opposite scenario (messages **missing** ChatHistory stamps, causing duplication), but the in-place mutation issue described here remains.

## Suggested Fix Direction

Clone `ChatMessage` objects before modifying `AdditionalProperties` in `CompactionProvider.InvokingCoreAsync`, specifically in both stamping blocks (around lines 110-118 and 135-143). A workaround wrapper `SafeCompactionProvider` that clones messages before delegating to the inner `CompactionProvider` is available on request.

## Environment

- Microsoft.Agents.AI: 1.12.0 / 1.13.0-preview
- .NET: 10
- OS: Windows

[BugRepro.zip](https://github.com/user-attachments/files/29755742/BugRepro.zip)

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.