|
19 | 19 | from agents.extensions.handoff_filters import nest_handoff_history, remove_all_tools |
20 | 20 | from agents.items import ( |
21 | 21 | HandoffOutputItem, |
| 22 | + MCPApprovalRequestItem, |
| 23 | + MCPApprovalResponseItem, |
| 24 | + MCPListToolsItem, |
22 | 25 | MessageOutputItem, |
23 | 26 | ReasoningItem, |
| 27 | + ToolCallItem, |
24 | 28 | ToolCallOutputItem, |
25 | 29 | ToolSearchCallItem, |
26 | 30 | ToolSearchOutputItem, |
@@ -259,7 +263,8 @@ def test_removes_tools_from_new_items_and_history(): |
259 | 263 | ), |
260 | 264 | ) |
261 | 265 | 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 |
263 | 268 | assert len(filtered_data.pre_handoff_items) == 1 |
264 | 269 | assert len(filtered_data.new_items) == 1 |
265 | 270 |
|
@@ -802,3 +807,211 @@ def test_nest_handoff_history_parse_summary_line_empty_stripped() -> None: |
802 | 807 | assert isinstance(nested.input_history, tuple) |
803 | 808 | final_summary = _as_message(nested.input_history[0]) |
804 | 809 | 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