Skip to content

Commit 49f593b

Browse files
fix: remove_all_tools missing MCP and reasoning item types (#2700)
1 parent 1005106 commit 49f593b

File tree

2 files changed

+225
-1
lines changed

2 files changed

+225
-1
lines changed

src/agents/extensions/handoff_filters.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
from ..items import (
99
HandoffCallItem,
1010
HandoffOutputItem,
11+
MCPApprovalRequestItem,
12+
MCPApprovalResponseItem,
13+
MCPListToolsItem,
1114
ReasoningItem,
1215
RunItem,
1316
ToolCallItem,
@@ -57,6 +60,9 @@ def _remove_tools_from_items(items: tuple[RunItem, ...]) -> tuple[RunItem, ...]:
5760
or isinstance(item, ToolCallItem)
5861
or isinstance(item, ToolCallOutputItem)
5962
or isinstance(item, ReasoningItem)
63+
or isinstance(item, MCPListToolsItem)
64+
or isinstance(item, MCPApprovalRequestItem)
65+
or isinstance(item, MCPApprovalResponseItem)
6066
):
6167
continue
6268
filtered_items.append(item)
@@ -75,6 +81,11 @@ def _remove_tool_types_from_input(
7581
"tool_search_call",
7682
"tool_search_output",
7783
"web_search_call",
84+
"mcp_call",
85+
"mcp_list_tools",
86+
"mcp_approval_request",
87+
"mcp_approval_response",
88+
"reasoning",
7889
]
7990

8091
filtered_items: list[TResponseInputItem] = []

tests/test_extension_filters.py

Lines changed: 214 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,12 @@
1919
from agents.extensions.handoff_filters import nest_handoff_history, remove_all_tools
2020
from agents.items import (
2121
HandoffOutputItem,
22+
MCPApprovalRequestItem,
23+
MCPApprovalResponseItem,
24+
MCPListToolsItem,
2225
MessageOutputItem,
2326
ReasoningItem,
27+
ToolCallItem,
2428
ToolCallOutputItem,
2529
ToolSearchCallItem,
2630
ToolSearchOutputItem,
@@ -259,7 +263,8 @@ def test_removes_tools_from_new_items_and_history():
259263
),
260264
)
261265
filtered_data = remove_all_tools(handoff_input_data)
262-
assert len(filtered_data.input_history) == 3
266+
# reasoning items are also removed (they become orphaned after tool calls are stripped)
267+
assert len(filtered_data.input_history) == 2
263268
assert len(filtered_data.pre_handoff_items) == 1
264269
assert len(filtered_data.new_items) == 1
265270

@@ -802,3 +807,211 @@ def test_nest_handoff_history_parse_summary_line_empty_stripped() -> None:
802807
assert isinstance(nested.input_history, tuple)
803808
final_summary = _as_message(nested.input_history[0])
804809
assert "Hello" in final_summary["content"] or "Reply" in final_summary["content"]
810+
811+
812+
def _get_mcp_call_input_item() -> TResponseInputItem:
813+
return cast(
814+
TResponseInputItem,
815+
{
816+
"id": "mc1",
817+
"arguments": "{}",
818+
"name": "test_tool",
819+
"server_label": "server1",
820+
"type": "mcp_call",
821+
},
822+
)
823+
824+
825+
def _get_mcp_list_tools_input_item() -> TResponseInputItem:
826+
return cast(
827+
TResponseInputItem,
828+
{
829+
"id": "ml1",
830+
"server_label": "server1",
831+
"tools": [],
832+
"type": "mcp_list_tools",
833+
},
834+
)
835+
836+
837+
def _get_mcp_approval_request_input_item() -> TResponseInputItem:
838+
return cast(
839+
TResponseInputItem,
840+
{
841+
"id": "ma1",
842+
"arguments": "{}",
843+
"name": "test_tool",
844+
"server_label": "server1",
845+
"type": "mcp_approval_request",
846+
},
847+
)
848+
849+
850+
def _get_mcp_approval_response_input_item() -> TResponseInputItem:
851+
return cast(
852+
TResponseInputItem,
853+
{
854+
"approval_request_id": "ma1",
855+
"approve": True,
856+
"type": "mcp_approval_response",
857+
},
858+
)
859+
860+
861+
def _get_mcp_call_run_item() -> ToolCallItem:
862+
from openai.types.responses.response_output_item import McpCall
863+
864+
return ToolCallItem(
865+
agent=fake_agent(),
866+
raw_item=McpCall(
867+
id="mc1",
868+
arguments="{}",
869+
name="test_tool",
870+
server_label="server1",
871+
type="mcp_call",
872+
),
873+
)
874+
875+
876+
def _get_mcp_list_tools_run_item() -> MCPListToolsItem:
877+
from openai.types.responses.response_output_item import McpListTools
878+
879+
return MCPListToolsItem(
880+
agent=fake_agent(),
881+
raw_item=McpListTools(
882+
id="ml1",
883+
server_label="server1",
884+
tools=[],
885+
type="mcp_list_tools",
886+
),
887+
)
888+
889+
890+
def _get_mcp_approval_request_run_item() -> MCPApprovalRequestItem:
891+
from openai.types.responses.response_output_item import McpApprovalRequest
892+
893+
return MCPApprovalRequestItem(
894+
agent=fake_agent(),
895+
raw_item=McpApprovalRequest(
896+
id="ma1",
897+
arguments="{}",
898+
name="test_tool",
899+
server_label="server1",
900+
type="mcp_approval_request",
901+
),
902+
)
903+
904+
905+
def _get_mcp_approval_response_run_item() -> MCPApprovalResponseItem:
906+
from openai.types.responses.response_input_param import McpApprovalResponse
907+
908+
return MCPApprovalResponseItem(
909+
agent=fake_agent(),
910+
raw_item=cast(
911+
McpApprovalResponse,
912+
{
913+
"approval_request_id": "ma1",
914+
"approve": True,
915+
"type": "mcp_approval_response",
916+
},
917+
),
918+
)
919+
920+
921+
def test_removes_reasoning_from_input_history() -> None:
922+
"""Reasoning items in raw input history should be removed by remove_all_tools.
923+
924+
When tool calls are stripped, orphaned reasoning items should also be removed
925+
to stay consistent with _remove_tools_from_items which filters ReasoningItem.
926+
"""
927+
handoff_input_data = handoff_data(
928+
input_history=(
929+
_get_message_input_item("Hello"),
930+
_get_reasoning_input_item(),
931+
_get_function_result_input_item("tool output"),
932+
_get_message_input_item("World"),
933+
),
934+
)
935+
filtered_data = remove_all_tools(handoff_input_data)
936+
# reasoning and function_call_output should both be removed, leaving 2 messages
937+
assert len(filtered_data.input_history) == 2
938+
for item in filtered_data.input_history:
939+
assert not isinstance(item, str)
940+
assert item.get("type") != "reasoning"
941+
assert item.get("type") != "function_call_output"
942+
943+
944+
def test_removes_mcp_items_from_input_history() -> None:
945+
"""MCP-related items in raw input history should be removed by remove_all_tools."""
946+
handoff_input_data = handoff_data(
947+
input_history=(
948+
_get_message_input_item("Hello"),
949+
_get_mcp_call_input_item(),
950+
_get_mcp_list_tools_input_item(),
951+
_get_mcp_approval_request_input_item(),
952+
_get_mcp_approval_response_input_item(),
953+
_get_message_input_item("World"),
954+
),
955+
)
956+
filtered_data = remove_all_tools(handoff_input_data)
957+
# All MCP items should be removed, leaving only the 2 message items
958+
assert len(filtered_data.input_history) == 2
959+
for item in filtered_data.input_history:
960+
assert not isinstance(item, str)
961+
itype = item.get("type")
962+
assert itype not in {
963+
"mcp_call",
964+
"mcp_list_tools",
965+
"mcp_approval_request",
966+
"mcp_approval_response",
967+
}
968+
969+
970+
def test_removes_mcp_run_items_from_new_items() -> None:
971+
"""MCP RunItem types should be removed from new_items and pre_handoff_items."""
972+
handoff_input_data = handoff_data(
973+
pre_handoff_items=(
974+
_get_mcp_list_tools_run_item(),
975+
_get_mcp_approval_request_run_item(),
976+
_get_message_output_run_item("kept"),
977+
),
978+
new_items=(
979+
_get_mcp_call_run_item(),
980+
_get_mcp_approval_response_run_item(),
981+
_get_message_output_run_item("also kept"),
982+
),
983+
)
984+
filtered_data = remove_all_tools(handoff_input_data)
985+
# Only message items should remain
986+
assert len(filtered_data.pre_handoff_items) == 1
987+
assert len(filtered_data.new_items) == 1
988+
989+
990+
def test_removes_mixed_mcp_and_function_items() -> None:
991+
"""Both MCP and function tool items should be removed together."""
992+
handoff_input_data = handoff_data(
993+
input_history=(
994+
_get_message_input_item("Start"),
995+
_get_mcp_call_input_item(),
996+
_get_function_result_input_item("fn output"),
997+
_get_reasoning_input_item(),
998+
_get_mcp_approval_response_input_item(),
999+
_get_message_input_item("End"),
1000+
),
1001+
pre_handoff_items=(
1002+
_get_mcp_list_tools_run_item(),
1003+
_get_tool_output_run_item("fn output"),
1004+
_get_reasoning_output_run_item(),
1005+
_get_message_output_run_item("kept"),
1006+
),
1007+
new_items=(
1008+
_get_mcp_call_run_item(),
1009+
_get_mcp_approval_request_run_item(),
1010+
_get_mcp_approval_response_run_item(),
1011+
_get_message_output_run_item("also kept"),
1012+
),
1013+
)
1014+
filtered_data = remove_all_tools(handoff_input_data)
1015+
assert len(filtered_data.input_history) == 2
1016+
assert len(filtered_data.pre_handoff_items) == 1
1017+
assert len(filtered_data.new_items) == 1

0 commit comments

Comments
 (0)