Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/specs/004-python-function-calling-loop.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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`, `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` |
Expand Down
18 changes: 18 additions & 0 deletions python/packages/foundry/agent_framework_foundry/_chat_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
Content,
FunctionInvocationConfiguration,
FunctionInvocationLayer,
Message,
load_settings,
)
from agent_framework._compaction import CompactionStrategy, TokenizerProtocol
Expand Down Expand Up @@ -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"):
Expand Down
65 changes: 65 additions & 0 deletions python/packages/foundry/tests/foundry/test_foundry_chat_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
Loading