From 152dc1504f90cdeb2bdb4f77bcd2693b9ba3cead Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Thu, 16 Jul 2026 03:07:52 +0800 Subject: [PATCH] Python: keep the approval response under service-side storage so a paused run resumes _prepare_message_for_openai handled function_approval_response and function_approval_request in one case and skipped both under service-side storage. That is right for the request (a server-issued item the server already has, so replaying it inline duplicates it -- #3295) but wrong for the response: the response is the user's approval decision, references the prior request by approval_request_id, and is not itself a server-stored item, so stripping it means the decision never reaches the model and an approval-paused run never resumes (approval-gated tools can never execute under service-side storage). Split the two into separate cases: the approval request keeps the storage skip (like function_call), the approval response is always sent (like function_result). Fixes #7125 --- .../agent_framework_openai/_chat_client.py | 18 +++++++++++- .../tests/openai/test_openai_chat_client.py | 28 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/python/packages/openai/agent_framework_openai/_chat_client.py b/python/packages/openai/agent_framework_openai/_chat_client.py index c9f2e6de300..07ec82a6dd4 100644 --- a/python/packages/openai/agent_framework_openai/_chat_client.py +++ b/python/packages/openai/agent_framework_openai/_chat_client.py @@ -1590,7 +1590,10 @@ def _prepare_message_for_openai( ) if function_call: all_messages.append(function_call) - case "function_approval_response" | "function_approval_request": + case "function_approval_request": + # The model's approval request is a server-issued item; under a + # continuation the server already has it, so replaying it inline + # duplicates it (#3295). Same treatment as function_call above. if request_uses_service_side_storage: continue prepared = self._prepare_content_for_openai( @@ -1600,6 +1603,19 @@ def _prepare_message_for_openai( ) if prepared: all_messages.append(prepared) + case "function_approval_response": + # The approval response is the user's decision that resumes an + # approval-paused run. It references the prior request by id and is + # not itself a server-stored item, so it must reach the model even + # under service-side storage; skipping it leaves the run paused + # forever (#7125). Same treatment as function_result above. + prepared = self._prepare_content_for_openai( + message.role, + content, + replays_local_storage=replays_local_storage, + ) + if prepared: + all_messages.append(prepared) case "mcp_server_tool_call" | "mcp_server_tool_result": # Hosted MCP call/result contents serialize as a single # top-level mcp_call input item; the result side emits an diff --git a/python/packages/openai/tests/openai/test_openai_chat_client.py b/python/packages/openai/tests/openai/test_openai_chat_client.py index 7849f4b67dc..8565ec5f847 100644 --- a/python/packages/openai/tests/openai/test_openai_chat_client.py +++ b/python/packages/openai/tests/openai/test_openai_chat_client.py @@ -2575,6 +2575,34 @@ def test_prepare_message_for_openai_with_function_approval_response() -> None: assert prepared_message["approve"] is True +def test_prepare_message_for_openai_keeps_approval_response_under_service_side_storage() -> None: + """The approval response resumes an approval-paused run and must survive service-side storage. + + It references the prior approval request by id and is not itself a server-stored item, so + stripping it (as the #3295 duplicate-item fix used to) leaves the run paused forever (#7125). + """ + client = OpenAIChatClient(model="test-model", api_key="test-key") + + function_call = Content.from_function_call( + call_id="call_789", + name="execute_command", + arguments='{"command": "ls"}', + ) + approval_response = Content.from_function_approval_response( + approved=True, + id="approval_003", + function_call=function_call, + ) + message = Message(role="user", contents=[approval_response]) + + result = client._prepare_message_for_openai(message, request_uses_service_side_storage=True) + + assert len(result) == 1 + assert result[0]["type"] == "mcp_approval_response" + assert result[0]["approval_request_id"] == "approval_003" + assert result[0]["approve"] is True + + def test_prepare_message_for_openai_includes_reasoning_with_function_call() -> None: """Test _prepare_message_for_openai includes reasoning items alongside function_calls.