From 698ba36e28121aba79ed2c5aed787aa33de11d20 Mon Sep 17 00:00:00 2001 From: Manjunath Janardhan Date: Wed, 19 Aug 2026 12:27:13 +0530 Subject: [PATCH] Python: fix: preserve Agent additional_properties in HandoffBuilder clones HandoffAgentExecutor clones each participant agent to attach handoff tools, but the clone rebuilt the Agent without forwarding additional_properties, so middleware and integrations observing context.agent.additional_properties during handoff runs saw an empty dict while the original agent retained its configuration. Pass a deepcopy of the original agent's additional_properties into the clone so handoff-executed agents keep their configured metadata and the original agent stays untouched. Fixes #7750 --- .../_handoff.py | 1 + .../orchestrations/tests/test_handoff.py | 48 +++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/python/packages/orchestrations/agent_framework_orchestrations/_handoff.py b/python/packages/orchestrations/agent_framework_orchestrations/_handoff.py index 4c0de8001be..b2db50f76aa 100644 --- a/python/packages/orchestrations/agent_framework_orchestrations/_handoff.py +++ b/python/packages/orchestrations/agent_framework_orchestrations/_handoff.py @@ -300,6 +300,7 @@ def _clone_chat_agent(self, agent: Agent[Any]) -> Agent[Any]: middleware=agent.middleware, require_per_service_call_history_persistence=agent.require_per_service_call_history_persistence, default_options=cloned_options, # type: ignore[assignment] + additional_properties=deepcopy(agent.additional_properties), ) def _apply_auto_tools(self, agent: Agent, targets: Sequence[HandoffConfiguration]) -> None: diff --git a/python/packages/orchestrations/tests/test_handoff.py b/python/packages/orchestrations/tests/test_handoff.py index 21964ebddb4..d190bbd30d6 100644 --- a/python/packages/orchestrations/tests/test_handoff.py +++ b/python/packages/orchestrations/tests/test_handoff.py @@ -9,6 +9,7 @@ import pytest from agent_framework import ( Agent, + AgentContext, AgentResponse, AgentResponseUpdate, ChatOptions, @@ -21,6 +22,7 @@ ResponseStream, WorkflowEvent, WorkflowRunState, + agent_middleware, function_middleware, resolve_agent_id, tool, @@ -805,6 +807,52 @@ async def tracking_middleware(context: FunctionInvocationContext, call_next): assert tracking_middleware in cloned_middleware, "User function middleware should be preserved on cloned agent" +async def test_handoff_clone_preserves_additional_properties() -> None: + """Handoff clones should preserve additional_properties from the original agent.""" + tracked_properties: list[dict[str, Any]] = [] + + @agent_middleware + async def observe_properties(context: AgentContext, call_next): + agent = cast(Agent, context.agent) + tracked_properties.append(dict(agent.additional_properties)) + await call_next() + + coordinator = Agent( + id="coordinator", + name="coordinator", + client=MockChatClient(name="coordinator"), + additional_properties={"tenant": "contoso", "trace_tag": "triage"}, + middleware=[observe_properties], + require_per_service_call_history_persistence=True, + ) + specialist = Agent( + id="specialist", + name="specialist", + client=MockChatClient(name="specialist"), + require_per_service_call_history_persistence=True, + ) + + workflow = ( + HandoffBuilder( + participants=_as_handoff_agents(coordinator, specialist), + termination_condition=lambda conversation: any(msg.role == "assistant" for msg in conversation), + ) + .with_start_agent(_as_handoff_agent(coordinator)) + .build() + ) + + await _drain(workflow.run("hello", stream=True)) + + assert tracked_properties == [{"tenant": "contoso", "trace_tag": "triage"}] + + # The clone owns its own copy; the original agent's properties stay untouched. + executor = workflow.executors[resolve_agent_id(coordinator)] + assert isinstance(executor, HandoffAgentExecutor) + cloned_additional_properties = cast(Agent, executor.agent).additional_properties + assert cloned_additional_properties == {"tenant": "contoso", "trace_tag": "triage"} + assert cloned_additional_properties is not coordinator.additional_properties + + def test_clean_conversation_for_handoff_keeps_text_only_history() -> None: """Tool-control messages must be excluded from persisted handoff history.""" function_call = Content.from_function_call(