From a0260522ac8a9d82648a9c8cc00cb88699191d90 Mon Sep 17 00:00:00 2001 From: Evan Mattson Date: Thu, 6 Aug 2026 12:27:47 +0900 Subject: [PATCH 1/2] Python: Make Foundry encrypted reasoning opt-in --- .../specs/004-python-function-calling-loop.md | 3 + .../agent_framework_foundry/_chat_client.py | 18 +++++ .../tests/foundry/test_foundry_chat_client.py | 65 +++++++++++++++++++ 3 files changed, 86 insertions(+) diff --git a/docs/specs/004-python-function-calling-loop.md b/docs/specs/004-python-function-calling-loop.md index 258777338b..2558f4a6e7 100644 --- a/docs/specs/004-python-function-calling-loop.md +++ b/docs/specs/004-python-function-calling-loop.md @@ -331,6 +331,8 @@ that manually replay messages own the equivalent rule: do not resend an approval - Service-managed continuation may omit inline reasoning/call items only when the hosted service already owns them. - Missing non-reconstructable reasoning fails explicitly before a provider request instead of silently dropping the content. +- Foundry clients do not request `reasoning.encrypted_content` implicitly; callers may opt in explicitly when the + selected deployment supports encrypted reasoning. - Compaction preserves or excludes the complete reasoning/call/result group atomically. ### Approval request and resume @@ -478,6 +480,7 @@ that manually replay messages own the equivalent rule: do not resend an approval | OpenAI end-to-end hosted approval | Hosted request parses, response sends, and continuation completes. | `test_end_to_end_mcp_approval_flow` | | Stored function call/result | Service-side storage drops server-issued calls but keeps new outputs. | `test_prepare_options_with_conversation_id_strips_server_issued_items`, `test_prepare_messages_for_openai_full_conversation_with_reasoning` | | Stateless reasoning replay | Replay reconstructs reasoning, call, and result together; missing required reasoning fails before the request. | `test_tool_loop_store_false_replays_encrypted_reasoning_group`, `test_stateless_request_rejects_non_replayable_reasoning_bound_mcp_output`, `test_prepare_messages_for_openai_full_conversation_with_reasoning` | +| Foundry encrypted reasoning opt-in | Foundry clients omit `reasoning.encrypted_content` by default and preserve an explicit caller opt-in. | `packages/foundry/tests/foundry/test_foundry_chat_client.py::test_get_response_does_not_request_encrypted_reasoning_by_default`, `test_get_response_preserves_explicit_encrypted_reasoning_opt_in`, `packages/foundry/tests/foundry/test_foundry_agent.py::test_foundry_agent_basic_call_does_not_request_unsupported_encrypted_reasoning`, `test_foundry_agent_preserves_caller_requested_encrypted_reasoning` | | Opaque reasoning signature replay | Provider-specific opaque reasoning metadata is captured and restored on reconstructed calls. | `packages/gemini/tests/test_gemini_client.py::test_function_call_part_captures_thought_signature_as_reasoning_content`, `test_reconstructed_function_call_replays_thought_signature_from_reasoning_content` | | Chat Completions approval wrappers | Framework approval wrappers are not sent as chat messages. | `packages/openai/tests/openai/test_openai_chat_completion_client.py` approval serialization tests | | AG-UI approval result event | Approved result emits once with content and persists in snapshot. | `packages/ag-ui/tests/ag_ui/test_approval_result_event.py::test_approval_resume_emits_tool_call_result`, `test_approval_resume_result_has_content`, `test_approval_resume_snapshot_replaces_approval_payload_with_tool_result`, `test_approval_resume_zero_updates_emits_tool_result` | diff --git a/python/packages/foundry/agent_framework_foundry/_chat_client.py b/python/packages/foundry/agent_framework_foundry/_chat_client.py index 67f9ba595d..cbc76852d1 100644 --- a/python/packages/foundry/agent_framework_foundry/_chat_client.py +++ b/python/packages/foundry/agent_framework_foundry/_chat_client.py @@ -13,6 +13,7 @@ Content, FunctionInvocationConfiguration, FunctionInvocationLayer, + Message, load_settings, ) from agent_framework._compaction import CompactionStrategy, TokenizerProtocol @@ -252,6 +253,23 @@ def __init__( ) self.project_client = project_client + @override + async def _prepare_options( + self, + messages: Sequence[Message], + options: Mapping[str, Any], + ) -> dict[str, Any]: + """Prepare Foundry options without implicitly requesting encrypted reasoning.""" + caller_requested_encrypted_reasoning = "reasoning.encrypted_content" in (options.get("include") or []) + run_options = await super()._prepare_options(messages, options) + if not caller_requested_encrypted_reasoning and isinstance(run_options.get("include"), list): + include = [item for item in run_options["include"] if item != "reasoning.encrypted_content"] + if include: + run_options["include"] = include + else: + run_options.pop("include") + return run_options + @override def _check_model_presence(self, options: dict[str, Any]) -> None: if not options.get("model"): diff --git a/python/packages/foundry/tests/foundry/test_foundry_chat_client.py b/python/packages/foundry/tests/foundry/test_foundry_chat_client.py index c8fc8ad154..db03045dbe 100644 --- a/python/packages/foundry/tests/foundry/test_foundry_chat_client.py +++ b/python/packages/foundry/tests/foundry/test_foundry_chat_client.py @@ -440,6 +440,71 @@ async def test_get_response_with_invalid_input() -> None: await client.get_response(messages=[]) +async def test_get_response_does_not_request_encrypted_reasoning_by_default() -> None: + """Foundry chat calls must not opt into encrypted reasoning unless requested.""" + mock_response = MagicMock( + id="response_123", + model="test-model", + created_at=1000000000, + metadata={}, + output_parsed=None, + output=[], + usage=None, + finish_reason=None, + conversation=None, + status="completed", + ) + + async def create_response(**kwargs: Any) -> Any: + if "reasoning.encrypted_content" in kwargs.get("include", []): + raise ValueError("Encrypted content is not supported with this model.") + return _as_raw(mock_response) + + mock_openai_client = _make_mock_openai_client() + mock_openai_client.responses.with_raw_response.create.side_effect = create_response + project_client = MagicMock() + project_client.get_openai_client.return_value = mock_openai_client + client = FoundryChatClient(project_client=project_client, model="test-model") + + response = await client.get_response([Message(role="user", contents=["Hello"])]) + + assert response.response_id == "response_123" + + +async def test_get_response_preserves_explicit_encrypted_reasoning_opt_in() -> None: + """Capable Foundry deployments can receive an explicit encrypted-reasoning opt-in.""" + mock_response = MagicMock( + id="response_123", + model="test-model", + created_at=1000000000, + metadata={}, + output_parsed=None, + output=[], + usage=None, + finish_reason=None, + conversation=None, + status="completed", + ) + + async def create_response(**kwargs: Any) -> Any: + if "reasoning.encrypted_content" not in kwargs.get("include", []): + raise ValueError("Encrypted reasoning opt-in was not forwarded.") + return _as_raw(mock_response) + + mock_openai_client = _make_mock_openai_client() + mock_openai_client.responses.with_raw_response.create.side_effect = create_response + project_client = MagicMock() + project_client.get_openai_client.return_value = mock_openai_client + client = FoundryChatClient(project_client=project_client, model="test-model") + + response = await client.get_response( + [Message(role="user", contents=["Hello"])], + options={"include": ["reasoning.encrypted_content"]}, + ) + + assert response.response_id == "response_123" + + async def test_web_search_tool_with_location() -> None: mock_openai_client = _make_mock_openai_client() project_client = MagicMock() From 71e71dce562d8b97c379305b14f031a658323c80 Mon Sep 17 00:00:00 2001 From: Evan Mattson Date: Thu, 6 Aug 2026 12:50:19 +0900 Subject: [PATCH 2/2] Python: Opt hosted replay test into encrypted reasoning --- docs/specs/004-python-function-calling-loop.md | 2 +- python/packages/foundry_hosting/tests/test_responses_int.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/specs/004-python-function-calling-loop.md b/docs/specs/004-python-function-calling-loop.md index 2558f4a6e7..8462dc76ac 100644 --- a/docs/specs/004-python-function-calling-loop.md +++ b/docs/specs/004-python-function-calling-loop.md @@ -480,7 +480,7 @@ that manually replay messages own the equivalent rule: do not resend an approval | OpenAI end-to-end hosted approval | Hosted request parses, response sends, and continuation completes. | `test_end_to_end_mcp_approval_flow` | | Stored function call/result | Service-side storage drops server-issued calls but keeps new outputs. | `test_prepare_options_with_conversation_id_strips_server_issued_items`, `test_prepare_messages_for_openai_full_conversation_with_reasoning` | | Stateless reasoning replay | Replay reconstructs reasoning, call, and result together; missing required reasoning fails before the request. | `test_tool_loop_store_false_replays_encrypted_reasoning_group`, `test_stateless_request_rejects_non_replayable_reasoning_bound_mcp_output`, `test_prepare_messages_for_openai_full_conversation_with_reasoning` | -| Foundry encrypted reasoning opt-in | Foundry clients omit `reasoning.encrypted_content` by default and preserve an explicit caller opt-in. | `packages/foundry/tests/foundry/test_foundry_chat_client.py::test_get_response_does_not_request_encrypted_reasoning_by_default`, `test_get_response_preserves_explicit_encrypted_reasoning_opt_in`, `packages/foundry/tests/foundry/test_foundry_agent.py::test_foundry_agent_basic_call_does_not_request_unsupported_encrypted_reasoning`, `test_foundry_agent_preserves_caller_requested_encrypted_reasoning` | +| Foundry encrypted reasoning opt-in | Foundry clients omit `reasoning.encrypted_content` by default and preserve an explicit caller opt-in. | `packages/foundry/tests/foundry/test_foundry_chat_client.py::test_get_response_does_not_request_encrypted_reasoning_by_default`, `test_get_response_preserves_explicit_encrypted_reasoning_opt_in`, `packages/foundry/tests/foundry/test_foundry_agent.py::test_foundry_agent_basic_call_does_not_request_unsupported_encrypted_reasoning`, `test_foundry_agent_preserves_caller_requested_encrypted_reasoning`, `packages/foundry_hosting/tests/test_responses_int.py::TestReasoningHostedMcpReplay::test_second_turn_replays_mcp_call_with_encrypted_reasoning` | | Opaque reasoning signature replay | Provider-specific opaque reasoning metadata is captured and restored on reconstructed calls. | `packages/gemini/tests/test_gemini_client.py::test_function_call_part_captures_thought_signature_as_reasoning_content`, `test_reconstructed_function_call_replays_thought_signature_from_reasoning_content` | | Chat Completions approval wrappers | Framework approval wrappers are not sent as chat messages. | `packages/openai/tests/openai/test_openai_chat_completion_client.py` approval serialization tests | | AG-UI approval result event | Approved result emits once with content and persists in snapshot. | `packages/ag-ui/tests/ag_ui/test_approval_result_event.py::test_approval_resume_emits_tool_call_result`, `test_approval_resume_result_has_content`, `test_approval_resume_snapshot_replaces_approval_payload_with_tool_result`, `test_approval_resume_zero_updates_emits_tool_result` | diff --git a/python/packages/foundry_hosting/tests/test_responses_int.py b/python/packages/foundry_hosting/tests/test_responses_int.py index 1094d3e663..41055ffd7b 100644 --- a/python/packages/foundry_hosting/tests/test_responses_int.py +++ b/python/packages/foundry_hosting/tests/test_responses_int.py @@ -623,6 +623,7 @@ async def foundry_responses_boundary(request: httpx.Request) -> httpx.Response: default_options={ # pyrefly: ignore[bad-argument-type] "store": False, "reasoning": {"effort": "low", "summary": "auto"}, + "include": ["reasoning.encrypted_content"], }, ) server = ResponsesHostServer(agent, store=InMemoryResponseProvider())