From 3c31ac28b53562b1db91f2c42a86d083b8d7dd14 Mon Sep 17 00:00:00 2001 From: Evan Mattson <35585003+moonbox3@users.noreply.github.com> Date: Tue, 14 Apr 2026 13:52:03 +0900 Subject: [PATCH] Python: Fix HandoffBuilder dropping function-level middleware when cloning agents (#5220) * Fix HandoffBuilder dropping function-level middleware when cloning agents (#5173) _clone_chat_agent() was using agent.agent_middleware (agent-level only) instead of agent.middleware (all types), which silently dropped any function middleware registered on the original agent. Changed to use agent.middleware to preserve all middleware types (agent, function, and chat) during cloning. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Python: Fix HandoffBuilder dropping function-level middleware when cloning agents Fixes #5173 * Fix false-positive middleware regression test (#5173) The test used isinstance(m, FunctionMiddleware) which matched _AutoHandoffMiddleware (always appended during build) instead of the user's @function_middleware decorator. Assert directly that tracking_middleware is present in the cloned agent's middleware list. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address review feedback for #5173: Python: [Bug]: HandoffBuilder drops function-level middleware when cloning agents --------- Co-authored-by: Copilot Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../_handoff.py | 2 +- .../orchestrations/tests/test_handoff.py | 43 ++++++++++++++++++- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/python/packages/orchestrations/agent_framework_orchestrations/_handoff.py b/python/packages/orchestrations/agent_framework_orchestrations/_handoff.py index e71fdd3882..c3e156096c 100644 --- a/python/packages/orchestrations/agent_framework_orchestrations/_handoff.py +++ b/python/packages/orchestrations/agent_framework_orchestrations/_handoff.py @@ -287,7 +287,7 @@ class HandoffAgentExecutor(AgentExecutor): name=agent.name, description=agent.description, context_providers=agent.context_providers, - middleware=agent.agent_middleware, + middleware=agent.middleware, require_per_service_call_history_persistence=agent.require_per_service_call_history_persistence, default_options=cloned_options, # type: ignore[assignment] ) diff --git a/python/packages/orchestrations/tests/test_handoff.py b/python/packages/orchestrations/tests/test_handoff.py index 225b408422..a512d9b9df 100644 --- a/python/packages/orchestrations/tests/test_handoff.py +++ b/python/packages/orchestrations/tests/test_handoff.py @@ -18,11 +18,16 @@ from agent_framework import ( ResponseStream, WorkflowEvent, WorkflowRunState, + function_middleware, resolve_agent_id, tool, ) from agent_framework._clients import BaseChatClient -from agent_framework._middleware import ChatMiddlewareLayer, FunctionInvocationContext, MiddlewareTermination +from agent_framework._middleware import ( + ChatMiddlewareLayer, + FunctionInvocationContext, + MiddlewareTermination, +) from agent_framework._tools import FunctionInvocationLayer, FunctionTool from agent_framework.orchestrations import HandoffAgentUserRequest, HandoffBuilder, HandoffSentEvent from pytest import param @@ -745,6 +750,42 @@ async def test_handoff_clone_preserves_per_service_call_history_persistence() -> assert all(message.role != "tool" for message in stored_messages) +async def test_handoff_clone_preserves_all_middleware_types() -> None: + """Handoff clones should preserve function and agent middleware from the original agent.""" + + @function_middleware + async def tracking_middleware(context: FunctionInvocationContext, call_next): + await call_next() + + agent_a = Agent( + id="agent_a", + name="agent_a", + client=MockChatClient(name="agent_a", handoff_to="agent_b"), + middleware=[tracking_middleware], + require_per_service_call_history_persistence=True, + ) + agent_b = Agent( + id="agent_b", + name="agent_b", + client=MockChatClient(name="agent_b"), + default_options={"tool_choice": "none"}, + require_per_service_call_history_persistence=True, + ) + + workflow = ( + HandoffBuilder(participants=[agent_a, agent_b], termination_condition=lambda _: False) + .with_start_agent(agent_a) + .add_handoff(agent_a, [agent_b]) + .add_handoff(agent_b, [agent_a]) + .build() + ) + + executor = workflow.executors[resolve_agent_id(agent_a)] + assert isinstance(executor, HandoffAgentExecutor) + cloned_middleware = executor._agent.middleware or [] + assert tracking_middleware in cloned_middleware, "User function middleware should be preserved on cloned agent" + + 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(