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 <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Evan Mattson
2026-04-14 13:52:03 +09:00
committed by GitHub
Unverified
parent 1b95e8585d
commit 3c31ac28b5
2 changed files with 43 additions and 2 deletions
@@ -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]
)
@@ -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(