Python: Fix duplicate messages in Handoff workflow#4714
Python: Fix duplicate messages in Handoff workflow#4714LEDazzio01 wants to merge 1 commit intomicrosoft:mainfrom
Conversation
There was a problem hiding this comment.
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
BaseHistoryProviderinstances from cloned handoff agents’context_providersand add a no-opInMemoryHistoryProviderplaceholder to prevent default history auto-injection. - Add a regression test using a
CapturingChatClientto 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.
| 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}" | ||
| ) |
contribution-logs/2026-03-15.md
Outdated
| # 📋 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 | |
contribution-logs/2026-03-14.md
Outdated
| # 📋 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 | |
| # 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
2edacac to
eb95a94
Compare
|
Addressed all 4 Copilot review comments and force-pushed a clean commit rebased onto
|
|
Thanks for the review, Copilot! All feedback has been addressed in the rebased commit:
|
Motivation and Context
Fixes #4695
When using
HandoffBuilderwithAzureOpenAIChatClient(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'scontext_providers, includingInMemoryHistoryProvider. TheHandoffAgentExecutormanages its own conversation state via_full_conversationand_cache. When the executor callsagent.run(self._cache, session=self._session):_cachealready contains the full conversation from_full_conversationInMemoryHistoryProvider.before_run()loads all previously stored messages from session state intocontext_messagessession_messages = context_messages + input_messages→ the entire conversation appears twiceThis is the same root cause identified in #4411. PR #4412 proposed the same fix but was closed without merging.
Description
BaseHistoryProviderinstances from the cloned agent'scontext_providersin_clone_chat_agent(), since the executor already manages conversation historyInMemoryHistoryProviderplaceholder (load_messages=False,store_inputs=False,store_outputs=False) to prevent the agent from auto-injecting a default one at runtimetest_no_duplicate_messages_after_handoff_and_resume) that verifies no duplicate messages are sent to the agent after a handoff + user reply sequence, using aCapturingChatClientthat records the exact messages passed to each API callContribution Checklist