Skip to content

.NET: fix(dotnet): avoid duplicate chat history with background respo… - #9874

Open
feiyun0112 wants to merge 2 commits into
microsoft:mainfrom
feiyun0112:issues/9793
Open

feiyun0112 wants to merge 2 commits into
microsoft:mainfrom
feiyun0112:issues/9793

Conversation

@feiyun0112

Copy link
Copy Markdown
Contributor

…nses and continuation tokens (#9793)

When RequirePerServiceCallChatHistoryPersistence is enabled, runs that use AllowBackgroundResponses or that are resumed with a continuation token persisted the same messages twice: once per service call from PerServiceCallChatHistoryPersistingChatClient and again from the forced end-of-run persistence in ChatClientAgent.

  • GetResponseAsync/GetStreamingResponseAsync no longer notify the providers for continuation/background runs; the agent's forced end-of-run notification owns them.
  • The streaming early-exit path no longer persists the input messages when a continuation token was handed out (or the run is a continuation/background run), since the resumed run persists them.
  • Adds regression tests covering both persistence modes for background runs, RunAsync continuation resume, streaming break-then-resume, and the abandoned stream that emitted a continuation token.

Motivation & Context

Description & Review Guide

  • What are the major changes?
  • What is the impact of these changes?
  • What do you want reviewers to focus on?

Related Issue

Fixes #

AI Assistance

  • No material AI assistance was used.
  • This is an AI-assisted contribution. I reviewed, understood, and verified all submitted content and accept responsibility for it.

AI assistance details:

Contribution Checklist

  • The code builds clean without any errors or warnings
  • All unit tests pass, and I have added new tests where possible
  • The PR follows the Contribution Guidelines
  • This PR links to an agreed issue with no competing open PR, or the Related Issue section documents a trivial-change or repository-automation exception.
  • This is not a breaking change. If it is a breaking change, add the breaking change label (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.

…nses and continuation tokens (microsoft#9793)

When RequirePerServiceCallChatHistoryPersistence is enabled, runs that use
AllowBackgroundResponses or that are resumed with a continuation token persisted
the same messages twice: once per service call from
PerServiceCallChatHistoryPersistingChatClient and again from the forced
end-of-run persistence in ChatClientAgent.

- GetResponseAsync/GetStreamingResponseAsync no longer notify the providers for
  continuation/background runs; the agent's forced end-of-run notification owns
  them.
- The streaming early-exit path no longer persists the input messages when a
  continuation token was handed out (or the run is a continuation/background
  run), since the resumed run persists them.
- Adds regression tests covering both persistence modes for background runs,
  RunAsync continuation resume, streaming break-then-resume, and the abandoned
  stream that emitted a continuation token.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Injected messages reach the model but are omitted from persisted history when per-service-call persistence, message injection, and background responses are enabled, requiring a fix and regression tests for non-streaming and fully consumed streaming runs.

1 open issue
What changed in this PR

Addresses #9793 by preventing duplicate chat-history persistence in .NET background-response and continuation runs, leaving persistence to ChatClientAgent at the end of the run.

Changes:

  • Skips per-service-call provider notifications for background and continuation runs.
  • Avoids persisting inputs on early stream exit when persistence is deferred to a resumed run.
  • Adds regression tests for background completion, continuation resume, and abandoned streams.
File Description
dotnet/​tests/​Microsoft.Agents.AI.UnitTests/​ChatClient/​PerServiceCallChatHistoryPersistingChatClientTests.cs Adds history assertions and regression scenarios for both persistence modes.
dotnet/​src/​Microsoft.Agents.AI/​ChatClient/​PerServiceCallChatHistoryPersistingChatClient.cs Defers background and continuation persistence to the agent and guards early-exit persistence.

Review effort: Balanced

// Continuation/background run — the agent's forced end-of-run handles session
// ConversationId and persistence. Notifying the providers here as well would
// persist the same messages twice.
return response;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't believe this comment is currently actionable without some really hacky changes, since we don't have a way to distinguish between injected messages and non-injected. Let's not address this without more thought.

// Continuation/background runs, and runs that handed out a continuation token, are
// excluded because the (possibly resumed) run persists these inputs itself at the
// end of the run; persisting here too would duplicate them.
if (!loopExitedNormally && !serviceErrorOccurred && !isContinuationOrBackground && !continuationTokenObserved)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This condition introduces a case where the user message is no longer saved:

  1. Start a streaming run with RequirePerServiceCallChatHistoryPersistence = true and AllowBackgroundResponses = true.
  2. Receive an update with some answer text, but no continuation token.
  3. Stop reading the stream with break.

ChatClientAgent does force end-of-run persistence when AllowBackgroundResponses is true, even if no continuation token was returned. However, stopping the stream early skips that code: its finally block only disposes the stream. This new !isContinuationOrBackground check also skips the cleanup that previously saved the input messages. As a result, the user message is lost from saved history, and there is no continuation token to resume with.

I reproduced this by extending RunStreamingAsync_PersistsInputMessages_WhenConsumerAbandonsEnumerationAsync to run with background responses both disabled and enabled. On this PR, 37 tests in the class passed and the new background-enabled case failed because the history provider was never notified. Removing only !isContinuationOrBackground from this cleanup condition made all 38 tests pass.

Please preserve input-message saving when a stream stops before any continuation token has been received, and add this case to the tests. The completed-call guards can still skip the duplicate notifications as intended.

…ound run is abandoned before any token

The early-exit persistence in PerServiceCallChatHistoryPersistingChatClient
was skipped for all continuation/background runs. That is only correct once a
continuation token has been handed out, because the resumed run persists the
inputs itself. If the consumer stops the stream before any token is received,
nothing resumes the run and ChatClientAgent's forced end-of-run persistence
never executes (its finally block only disposes the stream), so the user
message was silently dropped from history.

Gate the early-exit persistence on the observed continuation token only, and
add regression tests for an abandoned background run and for a resumed run
that is abandoned again.
@feiyun0112
feiyun0112 deployed to github-app-auth November 6, 2026 03:09 — with GitHub Actions Active

This branch was successfully deployed

1 active deployment
github-app-auth — 6ee3126e Deployed Nov 6, 2026 by feiyun0112 via add_label #27214
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

.NET Usage: [Issues, PRs], Target: .Net

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants