From f0ca60d117dfa0b3f64a0cd79c151929048be327 Mon Sep 17 00:00:00 2001 From: Dorothy Rex Date: Tue, 4 Aug 2026 17:05:44 +0000 Subject: [PATCH 1/4] Fix Foundry hosted session continuation --- .../foundry/agent_framework_foundry/_agent.py | 14 +++++ .../tests/foundry/test_foundry_agent.py | 55 +++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/python/packages/foundry/agent_framework_foundry/_agent.py b/python/packages/foundry/agent_framework_foundry/_agent.py index 2013d0401a..bab4315fde 100644 --- a/python/packages/foundry/agent_framework_foundry/_agent.py +++ b/python/packages/foundry/agent_framework_foundry/_agent.py @@ -138,6 +138,16 @@ def _uses_foundry_agent_session(conversation_id: Any) -> bool: ) +def _get_foundry_agent_session_id(response: Any) -> str | None: + """Return a hosted agent session id from a Foundry response payload.""" + agent_session_id = getattr(response, "agent_session_id", None) + if isinstance(agent_session_id, str) and agent_session_id: + return agent_session_id + + session_id = getattr(getattr(response, "session", None), "id", None) + return session_id if isinstance(session_id, str) and session_id else None + + def _build_agent_reference(agent_name: str, agent_version: str | None) -> dict[str, str]: """Build the Responses API ``agent_reference`` payload for non-preview Foundry agent calls. @@ -428,6 +438,8 @@ def _parse_response_from_openai( parsed_response = super()._parse_response_from_openai(response, options) if _uses_foundry_agent_session(options.get("conversation_id")): parsed_response.conversation_id = None + elif agent_session_id := _get_foundry_agent_session_id(response): + parsed_response.conversation_id = agent_session_id return parsed_response @override @@ -449,6 +461,8 @@ def _parse_chunk_from_openai( ) if _uses_foundry_agent_session(options.get("conversation_id")): update.conversation_id = None + elif agent_session_id := _get_foundry_agent_session_id(getattr(event, "response", None)): + update.conversation_id = agent_session_id return update @override diff --git a/python/packages/foundry/tests/foundry/test_foundry_agent.py b/python/packages/foundry/tests/foundry/test_foundry_agent.py index e6fc1cde13..402240e87a 100644 --- a/python/packages/foundry/tests/foundry/test_foundry_agent.py +++ b/python/packages/foundry/tests/foundry/test_foundry_agent.py @@ -804,6 +804,34 @@ def test_raw_foundry_agent_chat_client_parse_response_suppresses_conversation_id assert result.conversation_id is None +@pytest.mark.parametrize( + "response", + [ + SimpleNamespace(agent_session_id="agent-session-123"), + SimpleNamespace(session=SimpleNamespace(id="agent-session-123")), + ], +) +def test_raw_foundry_agent_chat_client_parse_response_uses_hosted_agent_session_id(response: Any) -> None: + """Test that first-turn hosted agent responses persist the service session id.""" + + mock_project = MagicMock() + mock_project.get_openai_client.return_value = MagicMock() + + client = RawFoundryAgentChatClient( + project_client=mock_project, + agent_name="test-agent", + ) + + parsed = ChatResponse(conversation_id="resp_123") + with patch( + "agent_framework_openai._chat_client.RawOpenAIChatClient._parse_response_from_openai", + return_value=parsed, + ): + result = client._parse_response_from_openai(response=response, options={}) + + assert result.conversation_id == "agent-session-123" + + def test_raw_foundry_agent_chat_client_parse_chunk_suppresses_conversation_id_for_agent_sessions() -> None: """Test that agent-session stream updates do not overwrite session.service_session_id.""" @@ -829,6 +857,33 @@ def test_raw_foundry_agent_chat_client_parse_chunk_suppresses_conversation_id_fo assert result.conversation_id is None +def test_raw_foundry_agent_chat_client_parse_chunk_uses_hosted_agent_session_id() -> None: + """Test that first-turn hosted agent stream updates persist the service session id.""" + + mock_project = MagicMock() + mock_project.get_openai_client.return_value = MagicMock() + + client = RawFoundryAgentChatClient( + project_client=mock_project, + agent_name="test-agent", + ) + + event = MagicMock(type="response.created") + event.response = SimpleNamespace(agent_session_id="agent-session-123") + parsed = ChatResponseUpdate(conversation_id="resp_123") + with patch( + "agent_framework_openai._chat_client.RawOpenAIChatClient._parse_chunk_from_openai", + return_value=parsed, + ): + result = client._parse_chunk_from_openai( + event=event, + options={}, + function_call_ids={}, + ) + + assert result.conversation_id == "agent-session-123" + + def test_raw_foundry_agent_chat_client_check_model_presence_is_noop() -> None: """Test that _check_model_presence does nothing (model is on service).""" From a735626836c7869a2b057c37a51c858b27eb15c7 Mon Sep 17 00:00:00 2001 From: Dorothy Rex Date: Tue, 4 Aug 2026 17:22:28 +0000 Subject: [PATCH 2/4] Respect store option for Foundry session IDs --- .../foundry/agent_framework_foundry/_agent.py | 6 +- .../tests/foundry/test_foundry_agent.py | 62 ++++++++++++++++++- 2 files changed, 64 insertions(+), 4 deletions(-) diff --git a/python/packages/foundry/agent_framework_foundry/_agent.py b/python/packages/foundry/agent_framework_foundry/_agent.py index bab4315fde..d48c2a269a 100644 --- a/python/packages/foundry/agent_framework_foundry/_agent.py +++ b/python/packages/foundry/agent_framework_foundry/_agent.py @@ -438,7 +438,7 @@ def _parse_response_from_openai( parsed_response = super()._parse_response_from_openai(response, options) if _uses_foundry_agent_session(options.get("conversation_id")): parsed_response.conversation_id = None - elif agent_session_id := _get_foundry_agent_session_id(response): + elif options.get("store") is not False and (agent_session_id := _get_foundry_agent_session_id(response)): parsed_response.conversation_id = agent_session_id return parsed_response @@ -461,7 +461,9 @@ def _parse_chunk_from_openai( ) if _uses_foundry_agent_session(options.get("conversation_id")): update.conversation_id = None - elif agent_session_id := _get_foundry_agent_session_id(getattr(event, "response", None)): + elif options.get("store") is not False and ( + agent_session_id := _get_foundry_agent_session_id(getattr(event, "response", None)) + ): update.conversation_id = agent_session_id return update diff --git a/python/packages/foundry/tests/foundry/test_foundry_agent.py b/python/packages/foundry/tests/foundry/test_foundry_agent.py index 402240e87a..4580b8ef6f 100644 --- a/python/packages/foundry/tests/foundry/test_foundry_agent.py +++ b/python/packages/foundry/tests/foundry/test_foundry_agent.py @@ -832,6 +832,30 @@ def test_raw_foundry_agent_chat_client_parse_response_uses_hosted_agent_session_ assert result.conversation_id == "agent-session-123" +def test_raw_foundry_agent_chat_client_parse_response_respects_store_false_for_agent_session_id() -> None: + """Test that hosted agent session ids are not promoted when storage is disabled.""" + + mock_project = MagicMock() + mock_project.get_openai_client.return_value = MagicMock() + + client = RawFoundryAgentChatClient( + project_client=mock_project, + agent_name="test-agent", + ) + + parsed = ChatResponse(conversation_id=None) + with patch( + "agent_framework_openai._chat_client.RawOpenAIChatClient._parse_response_from_openai", + return_value=parsed, + ): + result = client._parse_response_from_openai( + response=SimpleNamespace(agent_session_id="agent-session-123"), + options={"store": False}, + ) + + assert result.conversation_id is None + + def test_raw_foundry_agent_chat_client_parse_chunk_suppresses_conversation_id_for_agent_sessions() -> None: """Test that agent-session stream updates do not overwrite session.service_session_id.""" @@ -857,7 +881,14 @@ def test_raw_foundry_agent_chat_client_parse_chunk_suppresses_conversation_id_fo assert result.conversation_id is None -def test_raw_foundry_agent_chat_client_parse_chunk_uses_hosted_agent_session_id() -> None: +@pytest.mark.parametrize( + "response", + [ + SimpleNamespace(agent_session_id="agent-session-123"), + SimpleNamespace(session=SimpleNamespace(id="agent-session-123")), + ], +) +def test_raw_foundry_agent_chat_client_parse_chunk_uses_hosted_agent_session_id(response: Any) -> None: """Test that first-turn hosted agent stream updates persist the service session id.""" mock_project = MagicMock() @@ -869,7 +900,7 @@ def test_raw_foundry_agent_chat_client_parse_chunk_uses_hosted_agent_session_id( ) event = MagicMock(type="response.created") - event.response = SimpleNamespace(agent_session_id="agent-session-123") + event.response = response parsed = ChatResponseUpdate(conversation_id="resp_123") with patch( "agent_framework_openai._chat_client.RawOpenAIChatClient._parse_chunk_from_openai", @@ -884,6 +915,33 @@ def test_raw_foundry_agent_chat_client_parse_chunk_uses_hosted_agent_session_id( assert result.conversation_id == "agent-session-123" +def test_raw_foundry_agent_chat_client_parse_chunk_respects_store_false_for_agent_session_id() -> None: + """Test that hosted agent session stream ids are not promoted when storage is disabled.""" + + mock_project = MagicMock() + mock_project.get_openai_client.return_value = MagicMock() + + client = RawFoundryAgentChatClient( + project_client=mock_project, + agent_name="test-agent", + ) + + event = MagicMock(type="response.created") + event.response = SimpleNamespace(agent_session_id="agent-session-123") + parsed = ChatResponseUpdate(conversation_id=None) + with patch( + "agent_framework_openai._chat_client.RawOpenAIChatClient._parse_chunk_from_openai", + return_value=parsed, + ): + result = client._parse_chunk_from_openai( + event=event, + options={"store": False}, + function_call_ids={}, + ) + + assert result.conversation_id is None + + def test_raw_foundry_agent_chat_client_check_model_presence_is_noop() -> None: """Test that _check_model_presence does nothing (model is on service).""" From 348e7df586f569edb7c186674df9a0b894d1249f Mon Sep 17 00:00:00 2001 From: Dorothy Rex Date: Tue, 4 Aug 2026 18:07:57 +0000 Subject: [PATCH 3/4] Cover Foundry hosted session ID precedence --- python/packages/foundry/tests/foundry/test_foundry_agent.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/python/packages/foundry/tests/foundry/test_foundry_agent.py b/python/packages/foundry/tests/foundry/test_foundry_agent.py index 4580b8ef6f..21e931568e 100644 --- a/python/packages/foundry/tests/foundry/test_foundry_agent.py +++ b/python/packages/foundry/tests/foundry/test_foundry_agent.py @@ -809,6 +809,7 @@ def test_raw_foundry_agent_chat_client_parse_response_suppresses_conversation_id [ SimpleNamespace(agent_session_id="agent-session-123"), SimpleNamespace(session=SimpleNamespace(id="agent-session-123")), + SimpleNamespace(agent_session_id="agent-session-123", session=SimpleNamespace(id="session-id-should-not-win")), ], ) def test_raw_foundry_agent_chat_client_parse_response_uses_hosted_agent_session_id(response: Any) -> None: @@ -886,6 +887,7 @@ def test_raw_foundry_agent_chat_client_parse_chunk_suppresses_conversation_id_fo [ SimpleNamespace(agent_session_id="agent-session-123"), SimpleNamespace(session=SimpleNamespace(id="agent-session-123")), + SimpleNamespace(agent_session_id="agent-session-123", session=SimpleNamespace(id="session-id-should-not-win")), ], ) def test_raw_foundry_agent_chat_client_parse_chunk_uses_hosted_agent_session_id(response: Any) -> None: From 15b16a75ddb2065d4a34412433fe399215990330 Mon Sep 17 00:00:00 2001 From: ScarabSystems Date: Thu, 6 Aug 2026 05:57:19 -0400 Subject: [PATCH 4/4] Handle Foundry session mapping payloads Accept the SDK deserialization shape where the extra Foundry session object is retained as a mapping, so first-turn hosted agent responses promote session.id instead of keeping the transient response id. Add real OpenAI Response model coverage for both non-streaming and streaming parser paths, alongside the existing object-shaped session coverage. Validation: uv run --directory python/packages/foundry pytest tests/foundry/test_foundry_agent.py -k 'parse_response_uses_hosted_agent_session_id or parse_chunk_uses_hosted_agent_session_id' -q; uv run --directory python/packages/foundry poe syntax; uv run --directory python/packages/foundry poe pyright; uv run --directory python/packages/foundry poe test; uv run --directory python poe check -P foundry. --- .../foundry/agent_framework_foundry/_agent.py | 6 +++++- .../foundry/tests/foundry/test_foundry_agent.py | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/python/packages/foundry/agent_framework_foundry/_agent.py b/python/packages/foundry/agent_framework_foundry/_agent.py index d48c2a269a..7e10bd4998 100644 --- a/python/packages/foundry/agent_framework_foundry/_agent.py +++ b/python/packages/foundry/agent_framework_foundry/_agent.py @@ -144,7 +144,11 @@ def _get_foundry_agent_session_id(response: Any) -> str | None: if isinstance(agent_session_id, str) and agent_session_id: return agent_session_id - session_id = getattr(getattr(response, "session", None), "id", None) + session = getattr(response, "session", None) + if isinstance(session, Mapping): + session_id = cast(Mapping[str, Any], session).get("id") + else: + session_id = getattr(session, "id", None) return session_id if isinstance(session_id, str) and session_id else None diff --git a/python/packages/foundry/tests/foundry/test_foundry_agent.py b/python/packages/foundry/tests/foundry/test_foundry_agent.py index 21e931568e..27831f7901 100644 --- a/python/packages/foundry/tests/foundry/test_foundry_agent.py +++ b/python/packages/foundry/tests/foundry/test_foundry_agent.py @@ -37,6 +37,7 @@ from azure.identity import AzureCliCredential from azure.identity.aio import AzureCliCredential as AsyncAzureCliCredential from openai import AsyncOpenAI +from openai.types.responses.response import Response as OpenAIResponse from agent_framework_foundry._agent import ( FoundryAgent, @@ -804,11 +805,26 @@ def test_raw_foundry_agent_chat_client_parse_response_suppresses_conversation_id assert result.conversation_id is None +def _openai_response_with_session_id(session_id: str) -> OpenAIResponse: + return OpenAIResponse.model_validate({ + "id": "resp_123", + "created_at": 0, + "model": "test-model", + "object": "response", + "output": [], + "parallel_tool_calls": False, + "tool_choice": "auto", + "tools": [], + "session": {"id": session_id}, + }) + + @pytest.mark.parametrize( "response", [ SimpleNamespace(agent_session_id="agent-session-123"), SimpleNamespace(session=SimpleNamespace(id="agent-session-123")), + _openai_response_with_session_id("agent-session-123"), SimpleNamespace(agent_session_id="agent-session-123", session=SimpleNamespace(id="session-id-should-not-win")), ], ) @@ -887,6 +903,7 @@ def test_raw_foundry_agent_chat_client_parse_chunk_suppresses_conversation_id_fo [ SimpleNamespace(agent_session_id="agent-session-123"), SimpleNamespace(session=SimpleNamespace(id="agent-session-123")), + _openai_response_with_session_id("agent-session-123"), SimpleNamespace(agent_session_id="agent-session-123", session=SimpleNamespace(id="session-id-should-not-win")), ], )