Skip to content

Python: Fix duplicate messages in Handoff workflow#4714

Open
LEDazzio01 wants to merge 1 commit intomicrosoft:mainfrom
LEDazzio01:fix/handoff-duplicate-messages-4695
Open

Python: Fix duplicate messages in Handoff workflow#4714
LEDazzio01 wants to merge 1 commit intomicrosoft:mainfrom
LEDazzio01:fix/handoff-duplicate-messages-4695

Conversation

@LEDazzio01
Copy link
Contributor

Motivation and Context

Fixes #4695

When using HandoffBuilder with AzureOpenAIChatClient (or any Chat Completions-based client), the refund agent's conversation history shows every message duplicated. The user reports 18 messages where only 9 are expected.

Root Cause

HandoffBuilder._clone_chat_agent() preserves the original agent's context_providers, including InMemoryHistoryProvider. The HandoffAgentExecutor manages its own conversation state via _full_conversation and _cache. When the executor calls agent.run(self._cache, session=self._session):

  1. _cache already contains the full conversation from _full_conversation
  2. InMemoryHistoryProvider.before_run() loads all previously stored messages from session state into context_messages
  3. session_messages = context_messages + input_messages → the entire conversation appears twice

This is the same root cause identified in #4411. PR #4412 proposed the same fix but was closed without merging.

Description

  • Filter out BaseHistoryProvider instances from the cloned agent's context_providers in _clone_chat_agent(), since the executor already manages conversation history
  • Add a no-op InMemoryHistoryProvider placeholder (load_messages=False, store_inputs=False, store_outputs=False) to prevent the agent from auto-injecting a default one at runtime
  • Add a regression test (test_no_duplicate_messages_after_handoff_and_resume) that verifies no duplicate messages are sent to the agent after a handoff + user reply sequence, using a CapturingChatClient that records the exact messages passed to each API call

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? No

Copilot AI review requested due to automatic review settings March 15, 2026 21:49
@markwallace-microsoft markwallace-microsoft added documentation Improvements or additions to documentation python labels Mar 15, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes duplicated conversation history being sent to Chat Completions-based agents in HandoffBuilder workflows by preventing history providers from re-injecting previously stored messages, and adds a regression test to validate the behavior.

Changes:

  • Strip BaseHistoryProvider instances from cloned handoff agents’ context_providers and add a no-op InMemoryHistoryProvider placeholder to prevent default history auto-injection.
  • Add a regression test using a CapturingChatClient to ensure messages are not duplicated after a handoff + resume.
  • Add two contribution log markdown files under contribution-logs/.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

File Description
python/packages/orchestrations/agent_framework_orchestrations/_handoff.py Prevents duplicate message injection by filtering history providers during agent cloning.
python/packages/orchestrations/tests/test_handoff.py Adds a regression test that inspects messages passed to the specialist client after resume.
contribution-logs/2026-03-14.md Adds a contribution status log entry (appears unrelated to handoff fix).
contribution-logs/2026-03-15.md Adds a contribution status log entry (appears unrelated to handoff fix).

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +1220 to +1231
second_call_messages = specialist_client.captured_calls[1]
user_texts = [m.text for m in second_call_messages if m.role == "user"]
assistant_texts = [m.text for m in second_call_messages if m.role == "assistant"]

# Each user message should appear at most once
assert len(user_texts) == len(set(user_texts)), (
f"Duplicate user messages detected in specialist's second call: {user_texts}"
)
# Each assistant message should appear at most once
assert len(assistant_texts) == len(set(assistant_texts)), (
f"Duplicate assistant messages detected in specialist's second call: {assistant_texts}"
)
Comment on lines +1 to +12
# 📋 Open Source Contribution Status — March 15, 2026

**Date:** March 15, 2026
**Contributor:** [@LEDazzio01](https://github.com/LEDazzio01)

---

## Today's Actions

### microsoft/agent-framework — Issues #4681, #4663, #4695

| Time | Action |
Comment on lines +1 to +12
# 📋 Open Source Contribution Status — March 14, 2026

**Date:** March 14, 2026
**Contributor:** [@LEDazzio01](https://github.com/LEDazzio01)

---

## Today's Actions

### microsoft/agent-framework — Issues #4701 & #4695

| Time | Action |
Comment on lines +371 to +383
# Filter out history providers to prevent duplicate messages.
# The HandoffAgentExecutor manages conversation history via _full_conversation,
# so history providers would re-inject previously stored messages on each
# agent.run() call, causing the entire conversation to appear twice.
# A no-op InMemoryHistoryProvider placeholder prevents the agent from
# auto-injecting a default one at runtime.
filtered_providers = [
p for p in agent.context_providers
if not isinstance(p, BaseHistoryProvider)
]
if not any(isinstance(p, BaseHistoryProvider) for p in filtered_providers):
filtered_providers.append(
InMemoryHistoryProvider(
Strip BaseHistoryProvider instances from cloned agents in
HandoffBuilder._clone_chat_agent(). The HandoffAgentExecutor manages
conversation state via _full_conversation, so InMemoryHistoryProvider
was re-injecting previously stored messages on each agent.run() call,
causing the entire conversation to appear twice in the API request.

A no-op InMemoryHistoryProvider placeholder (load_messages=False,
store_inputs=False, store_outputs=False) prevents the agent from
auto-injecting a default one at runtime.

Fixes microsoft#4695
@LEDazzio01 LEDazzio01 force-pushed the fix/handoff-duplicate-messages-4695 branch from 2edacac to eb95a94 Compare March 15, 2026 22:02
@LEDazzio01
Copy link
Contributor Author

Addressed all 4 Copilot review comments and force-pushed a clean commit rebased onto upstream/main:

  1. Redundant any() guard — Simplified to always append the no-op InMemoryHistoryProvider placeholder, since filtered_providers can never contain a BaseHistoryProvider after the list comprehension.
  2. Test assertions — Replaced text-only comparison with structural fingerprints using (role, tuple((content.type, content.text))) to avoid false negatives on non-text messages and false positives on legitimately identical text.
  3. Contribution log files — Removed from the PR by rebasing onto upstream/main. These were stale files from the fork's main branch.

@LEDazzio01
Copy link
Contributor Author

Thanks for the review, Copilot! All feedback has been addressed in the rebased commit:

  1. Redundant guard simplified — The if not any(isinstance(p, BaseHistoryProvider) ...) check was removed. The no-op InMemoryHistoryProvider is now unconditionally appended after filtering, since filtered_providers will never contain a BaseHistoryProvider at that point.

  2. Test assertions improved — The regression test now uses structural fingerprints (m.role, tuple((c.type, c.text) for c in m.contents)) instead of comparing .text values with set(...). This correctly handles non-text messages and avoids false-failing on legitimately identical text.

  3. Unrelated files removed — The contribution-logs/ files have been removed from this branch to keep the PR focused on the handoff fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Python: [Bug]: Duplicate messages in Handoff workflow

3 participants