From baf0ea5252eb3faa232b811c1c4d95771afd10ed Mon Sep 17 00:00:00 2001
From: Ben Thompson <113354590+BenGearset@users.noreply.github.com>
Date: Tue, 1 Sep 2026 14:03:45 +0000
Subject: [PATCH 1/5] .NET: Replace deprecated AWSSDK.Extensions.Bedrock.MEAI
with AWS.Bedrock.MEAI (#7983)
* `AWSSDK.Extensions.Bedrock.MEAI` -> `AWS.Bedrock.MEAI`
* Update `AgentWithMemory_Step03_MemoryUsingValkey_Bedrock` project
---
dotnet/Directory.Packages.props | 2 +-
.../AgentWithMemory_Step03_MemoryUsingValkey_Bedrock.csproj | 2 +-
.../README.md | 4 ++--
3 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props
index f75a3f2dd38..a776b2df827 100644
--- a/dotnet/Directory.Packages.props
+++ b/dotnet/Directory.Packages.props
@@ -142,7 +142,7 @@
-
+
diff --git a/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step03_MemoryUsingValkey_Bedrock/AgentWithMemory_Step03_MemoryUsingValkey_Bedrock.csproj b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step03_MemoryUsingValkey_Bedrock/AgentWithMemory_Step03_MemoryUsingValkey_Bedrock.csproj
index 274ac15c97c..d16c3b03b31 100644
--- a/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step03_MemoryUsingValkey_Bedrock/AgentWithMemory_Step03_MemoryUsingValkey_Bedrock.csproj
+++ b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step03_MemoryUsingValkey_Bedrock/AgentWithMemory_Step03_MemoryUsingValkey_Bedrock.csproj
@@ -9,7 +9,7 @@
-
+
diff --git a/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step03_MemoryUsingValkey_Bedrock/README.md b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step03_MemoryUsingValkey_Bedrock/README.md
index 06d4012bd92..7d49ca13801 100644
--- a/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step03_MemoryUsingValkey_Bedrock/README.md
+++ b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step03_MemoryUsingValkey_Bedrock/README.md
@@ -1,11 +1,11 @@
# Agent with Memory Using Valkey + Amazon Bedrock
-This sample demonstrates using Valkey for persistent chat history with the Agent Framework, powered by Amazon Bedrock via the `AWSSDK.Extensions.Bedrock.MEAI` adapter.
+This sample demonstrates using Valkey for persistent chat history with the Agent Framework, powered by Amazon Bedrock via the `AWS.Bedrock.MEAI` adapter.
## Components
- **ValkeyChatHistoryProvider** — Persists conversation history across sessions using Valkey lists. Works with any Valkey or Redis OSS server (no search module required).
-- **Amazon Bedrock** — Provides the LLM via `AWSSDK.Extensions.Bedrock.MEAI`, which implements `IChatClient` from `Microsoft.Extensions.AI`.
+- **Amazon Bedrock** — Provides the LLM via `AWS.Bedrock.MEAI`, which implements `IChatClient` from `Microsoft.Extensions.AI`.
## Prerequisites
From a6ea50e3ab2e41ed0ebc256104a897d3f8d11e64 Mon Sep 17 00:00:00 2001
From: eavanvalkenburg
Date: Tue, 1 Sep 2026 17:58:41 +0200
Subject: [PATCH 2/5] Python: select Foundry hosting history source
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a4c73599-9f66-4c22-bd66-96fe676e0ce6
---
...9-python-foundry-hosting-history-source.md | 119 +++++++++++
python/packages/foundry_hosting/README.md | 37 ++++
.../_responses.py | 85 ++++++--
.../foundry_hosting/tests/test_responses.py | 186 +++++++++++++++++-
4 files changed, 407 insertions(+), 20 deletions(-)
create mode 100644 docs/decisions/0039-python-foundry-hosting-history-source.md
diff --git a/docs/decisions/0039-python-foundry-hosting-history-source.md b/docs/decisions/0039-python-foundry-hosting-history-source.md
new file mode 100644
index 00000000000..3e76bd775ff
--- /dev/null
+++ b/docs/decisions/0039-python-foundry-hosting-history-source.md
@@ -0,0 +1,119 @@
+---
+status: proposed
+contact: eavanvalkenburg
+date: 2026-09-01
+deciders: eavanvalkenburg, moonbox3
+consulted: sachinkahawala
+---
+
+# Select the conversation history source for Python Foundry hosting
+
+## Context and Problem Statement
+
+`ResponsesHostServer` currently replays the AgentServer response transcript into every agent run. An `AgentSession`
+can also restore a downstream `service_session_id`, causing the model service to combine its stored conversation with
+the replayed AgentServer transcript. This duplicates prior turns and compounds on each request.
+
+Conversation data can exist in four places:
+
+1. the AgentServer `ResponseProviderProtocol`;
+2. an Agent Framework `HistoryProvider`;
+3. `AgentSession.state`, persisted by a `SessionStore` and used by `InMemoryHistoryProvider`; and
+4. the downstream model service when `store=True`.
+
+The host must prevent duplicate model history without removing the regular agent storage choices.
+
+## Decision Drivers
+
+- Feed one canonical conversation transcript into each model call.
+- Keep AgentServer response persistence independent from the model's history source.
+- Preserve the normal agent choice between `HistoryProvider` and downstream service storage.
+- Retain AgentServer response history as the default hosting behavior.
+- Avoid forcing the measured Foundry `store=False` streaming latency on users who select regular agent history.
+- Make existing sessions containing a downstream service ID safe after upgrade.
+
+## Considered Options
+
+### Always use AgentServer response history
+
+- Good: one simple default and parity with current .NET Foundry hosting.
+- Good: the selected response provider controls the transcript used by the model.
+- Bad: users cannot use normal agent history providers or service-side continuation.
+- Bad: raw benchmarking measured a 5.43-second median streaming penalty for `store=False` on a Foundry project
+ endpoint using `gpt-5.4-nano`; the OpenAI public endpoint did not show this penalty.
+
+### Clear the service ID but leave downstream storage enabled
+
+- Good: avoids duplicated input and preserves Foundry streaming latency.
+- Bad: creates an untracked stored response or conversation on every model call.
+- Bad: service-side storage incurs retention and cost but is never used for continuation.
+
+### Add separate AgentServer, history-provider, service, and automatic modes
+
+- Good: makes each possible authority explicit at the hosting layer.
+- Bad: duplicates history-selection behavior already implemented by `Agent`.
+- Bad: an automatic mode changes authority based on provider output, making retention and recovery unpredictable.
+
+### Select AgentServer history or regular agent history
+
+- Good: the host makes only the decision it owns: whether AgentServer history supersedes normal agent behavior.
+- Good: regular agent mode preserves service storage, in-session history, and external history providers.
+- Good: `ResponseProviderProtocol`, `HistoryProvider`, and `SessionStore` remain independent extension points.
+- Neutral: AgentServer still manages protocol-level Responses persistence in regular agent mode, according to the outer
+ request, but does not replay that transcript into the model.
+
+## Decision Outcome
+
+Add `history_source: Literal["agent_server", "agent"] = "agent_server"` to `ResponsesHostServer`.
+
+With `history_source="agent_server"`:
+
+- load-enabled `HistoryProvider` instances are rejected;
+- an agent-level default `conversation_id` is rejected;
+- the configured response provider transcript and current input are passed to the agent;
+- downstream `store=False` overrides agent defaults on every run;
+- a restored `service_session_id` is cleared before the run;
+- a client that still returns a service ID fails the response and the contaminated session is not saved; and
+- a transient `InMemoryHistoryProvider` supports intra-run function calls but is removed before session persistence.
+
+With `history_source="agent"`:
+
+- only current request input is passed by hosting;
+- load-enabled history providers are allowed;
+- downstream storage options are not changed; and
+- normal `Agent` behavior selects service storage, an explicit history provider, or automatic in-session history.
+
+The AgentServer response provider continues to control Responses API persistence and retrieval in both modes, according
+to the outer request. The session-store provider also remains independent. Consequently, regular agent mode can combine
+`InMemoryHistoryProvider` with the default `FoundryAgentSessionStore` to persist model history in Foundry without using
+the AgentServer response transcript as model input.
+
+## Developer Experience
+
+```python
+# Default: AgentServer response history is model history.
+ResponsesHostServer(agent)
+
+# Regular Agent history and downstream storage behavior.
+ResponsesHostServer(agent, history_source="agent")
+```
+
+Passing `store=None` or omitting `store` continues to select the environment's default AgentServer response provider.
+It does not disable response persistence.
+
+## Consequences
+
+- Good: existing applications keep AgentServer history as their default.
+- Good: applications can retain service-side storage and its current Foundry latency characteristics.
+- Good: the API does not introduce a second history-selection state machine.
+- Bad: default mode mutates the supplied `RawAgent` by installing a transient history provider.
+- Bad: regular agent history and AgentServer response history may differ, which response-oriented evaluations must
+ document.
+- Neutral: switching an existing conversation between modes may require resetting its persisted session/history.
+
+## More Information
+
+- [Issue #7955](https://github.com/microsoft/agent-framework/issues/7955)
+- [Closed Python PR #7957](https://github.com/microsoft/agent-framework/pull/7957)
+- [Merged .NET PR #7525](https://github.com/microsoft/agent-framework/pull/7525)
+- [Merged .NET follow-up PR #7572](https://github.com/microsoft/agent-framework/pull/7572)
diff --git a/python/packages/foundry_hosting/README.md b/python/packages/foundry_hosting/README.md
index d73bc714c8a..c5c43f8dc60 100644
--- a/python/packages/foundry_hosting/README.md
+++ b/python/packages/foundry_hosting/README.md
@@ -2,6 +2,43 @@
This package provides the integration of Agent Framework agents and workflows with the Foundry Agent Server, which can be hosted on Foundry infrastructure.
+## Conversation history
+
+`ResponsesHostServer` uses AgentServer response history as the model's conversation history by default:
+
+```python
+server = ResponsesHostServer(agent)
+```
+
+In this mode, the configured AgentServer response provider supplies the prior transcript. Hosting rejects
+`HistoryProvider` instances with `load_messages=True` and agents configured with a default `conversation_id`, adds a
+transient in-memory provider for function-call loops, forces downstream `store=False`, and clears restored downstream
+service IDs. These safeguards ensure the model receives the transcript once.
+
+To preserve the agent's regular history and service-storage behavior, select the agent as the history source:
+
+```python
+server = ResponsesHostServer(agent, history_source="agent")
+```
+
+Hosting then passes only current request input, allows load-enabled history providers, and does not override the
+agent's downstream `store` option. For example, `InMemoryHistoryProvider` stores messages in `AgentSession.state`, which
+the default `FoundryAgentSessionStore` persists in Foundry:
+
+```python
+agent = Agent(
+ client=client,
+ context_providers=[InMemoryHistoryProvider()],
+ default_options={"store": False},
+)
+server = ResponsesHostServer(agent, history_source="agent")
+```
+
+The `store` argument remains independent: it selects the AgentServer response provider used for Responses API
+persistence and retrieval. Omitting it or passing `None` selects the environment default. With
+`history_source="agent_server"`, that response provider also supplies model history; with `history_source="agent"`, it
+does not.
+
## State store
### Local persistence
diff --git a/python/packages/foundry_hosting/agent_framework_foundry_hosting/_responses.py b/python/packages/foundry_hosting/agent_framework_foundry_hosting/_responses.py
index d75628cbabf..85c875fee38 100644
--- a/python/packages/foundry_hosting/agent_framework_foundry_hosting/_responses.py
+++ b/python/packages/foundry_hosting/agent_framework_foundry_hosting/_responses.py
@@ -336,6 +336,7 @@ def __init__(
agent_session_store_provider: StoreProvider[SessionStore] | None = None,
checkpoint_store_provider: ContextScopedStoreProvider[CheckpointStorage] | None = None,
function_approval_store_provider: StoreProvider[FunctionApprovalStore] | None = None,
+ history_source: Literal["agent_server", "agent"] = "agent_server",
**kwargs: Any,
) -> None:
"""Initialize a ResponsesHostServer.
@@ -351,14 +352,21 @@ def __init__(
If not provided, a default `CheckpointStoreProvider` will be used.
function_approval_store_provider: Optional provider for function approval storage.
If not provided, a default `FunctionApprovalStoreProvider` will be used.
+ history_source: Source of conversation history supplied to the model for regular agents.
+ `"agent_server"` (default) uses the transcript from the configured response store,
+ rejects load-enabled agent history providers, and disables downstream service storage.
+ `"agent"` passes only the current request input and preserves the agent's normal
+ history-provider or service-storage behavior. AgentServer still manages Responses
+ API persistence through `store` in both modes.
**kwargs: Additional keyword arguments.
Note:
- 1. The agent must not have a history provider with `load_messages=True`,
- because history is managed by the hosting infrastructure.
- 2. The agent must not have any context providers that maintain context
- in memory, because the hosting environment may get deactivated between
- requests, and any in-memory context would be lost.
+ 1. When `history_source="agent_server"`, the agent must not have a history provider
+ with `load_messages=True`, because history is managed by the hosting infrastructure.
+ 2. Context providers must not keep required state only on their Python instances,
+ because the hosting environment may get deactivated between requests. Provider
+ state carried by `AgentSession`, including `InMemoryHistoryProvider` messages in
+ `history_source="agent"` mode, is persisted by the configured session store.
3. Resiliency (resilient_background=True) is ONLY supported for workflows; constructing this
server with a non-workflow agent and `resilient_background=True` raises `RuntimeError`.
When resiliency is enabled, and the server crashes mid-response:
@@ -374,18 +382,35 @@ def __init__(
collected, and that isn't guaranteed to have happened in time.
Raises:
+ ValueError: If `history_source` is not supported.
RuntimeError: If `resilient_background=True` is requested for a non-workflow agent, or if
`steerable_conversations=True` is requested for a workflow agent.
"""
+ if history_source not in ("agent_server", "agent"):
+ raise ValueError("history_source must be either 'agent_server' or 'agent'.")
+
super().__init__(prefix=prefix, options=options, store=store, **kwargs)
- for provider in getattr(agent, "context_providers", []):
- if isinstance(provider, HistoryProvider) and provider.load_messages:
- if _is_hosted_responses_history_sentinel(provider):
- continue
+ self._uses_agent_server_history = history_source == "agent_server"
+ if self._uses_agent_server_history:
+ for provider in getattr(agent, "context_providers", []):
+ if isinstance(provider, HistoryProvider) and provider.load_messages:
+ if _is_hosted_responses_history_sentinel(provider):
+ continue
+ raise RuntimeError(
+ "AgentServer response history is enabled, but the agent has a HistoryProvider "
+ "with load_messages=True. Remove that provider or construct ResponsesHostServer "
+ "with history_source='agent' to use the agent's regular history setup."
+ )
+ default_options = getattr(agent, "default_options", None)
+ typed_default_options: Mapping[str, Any] = (
+ cast(Mapping[str, Any], default_options) if isinstance(default_options, Mapping) else {}
+ )
+ if typed_default_options.get("conversation_id") is not None:
raise RuntimeError(
- "There shouldn't be a history provider with `load_messages=True` already present. "
- "History is managed by the hosting infrastructure."
+ "AgentServer response history is enabled, but the agent has a default conversation_id. "
+ "Remove that option or construct ResponsesHostServer with history_source='agent' to resume "
+ "the downstream service conversation."
)
self._is_workflow_agent = False
@@ -398,7 +423,7 @@ def __init__(
self._is_workflow_agent = True
self._uses_hosted_responses_history = False
- if not self._is_workflow_agent and isinstance(agent, RawAgent):
+ if self._uses_agent_server_history and not self._is_workflow_agent and isinstance(agent, RawAgent):
self._uses_hosted_responses_history = True
if not any(
_is_hosted_responses_history_sentinel(provider)
@@ -615,21 +640,29 @@ async def _handle_inner_agent(
request_interrupted = False
try:
- if self._uses_hosted_responses_history:
+ if self._uses_agent_server_history:
session.state.pop(_HOSTED_RESPONSES_HISTORY_SOURCE_ID, None)
+ # A restored service ID belongs to the downstream model service. Replaying the
+ # AgentServer transcript while resuming that service history would duplicate every
+ # prior turn, so AgentServer-history mode always starts the model call statelessly.
+ session.service_session_id = None
input_items = await context.get_input_items()
input_messages = await _items_to_messages(input_items, approval_storage=approval_storage)
- history = await context.get_history()
+ history_messages: list[Message] = []
+ if self._uses_agent_server_history:
+ history = await context.get_history()
+ history_messages = await _output_items_to_messages(history, approval_storage=approval_storage)
run_kwargs: dict[str, Any] = {
- "messages": [
- *(await _output_items_to_messages(history, approval_storage=approval_storage)),
- *input_messages,
- ],
+ "messages": [*history_messages, *input_messages],
"session": session,
}
chat_options, are_options_set = _to_chat_options(request)
+ if self._uses_agent_server_history:
+ # The response provider already owns the transcript used for this run. Keep the
+ # downstream service stateless so it cannot become a second history source.
+ chat_options["store"] = False
if are_options_set and not isinstance(self._agent, RawAgent):
logger.warning("Agent doesn't support runtime options. They will be ignored.")
@@ -662,8 +695,22 @@ async def _handle_inner_agent(
finally:
if self._uses_hosted_responses_history:
session.state.pop(_HOSTED_RESPONSES_HISTORY_SOURCE_ID, None)
+
+ # A service ID here means the client stored the turn despite the forced `store=False`.
+ # Do not persist a session that could resume that unreconciled history on a later turn.
+ stored_output_violation = self._uses_agent_server_history and session.service_session_id is not None
+ if stored_output_violation:
+ misconfigured = RuntimeError(
+ "The agent's chat client stored this turn server-side while AgentServer response history "
+ "is supplying the conversation. Configure the client to honor store=False, or construct "
+ "ResponsesHostServer with history_source='agent' to use the agent's regular history setup."
+ )
+ logger.error("%s", misconfigured)
+ if request_failure is None and not request_interrupted:
+ request_failure = misconfigured
try:
- await session_storage.set(session_save_id, session)
+ if not stored_output_violation:
+ await session_storage.set(session_save_id, session)
except Exception as save_error:
save_failure = save_error
if request_interrupted:
diff --git a/python/packages/foundry_hosting/tests/test_responses.py b/python/packages/foundry_hosting/tests/test_responses.py
index 50f0c0ff505..2e92bb87ed0 100644
--- a/python/packages/foundry_hosting/tests/test_responses.py
+++ b/python/packages/foundry_hosting/tests/test_responses.py
@@ -12,6 +12,7 @@
import asyncio
import json
+import logging
import uuid
from collections.abc import AsyncGenerator, AsyncIterator, Awaitable, Callable, Mapping, Sequence
from dataclasses import dataclass
@@ -193,6 +194,44 @@ async def stream_response() -> AsyncIterator[ChatResponseUpdate]:
return ResponseStream(stream_response(), finalizer=ChatResponse.from_updates)
+class _ServiceStorageRecordingClient(BaseChatClient):
+ """Record service-storage options and mimic a client that returns a conversation ID."""
+
+ STORES_BY_DEFAULT = True
+
+ def __init__(self, *, honors_store: bool = True) -> None:
+ super().__init__()
+ self._honors_store = honors_store
+ self.calls: list[list[Message]] = []
+ self.store_options: list[Any] = []
+ self.conversation_ids: list[Any] = []
+
+ def _inner_get_response(
+ self,
+ *,
+ messages: Sequence[Message],
+ stream: bool,
+ options: Mapping[str, Any],
+ **kwargs: Any,
+ ) -> Awaitable[ChatResponse] | ResponseStream[ChatResponseUpdate, ChatResponse]:
+ del kwargs
+ assert stream is True, "The inner agent only runs in stream mode in Foundry Hosted Agents."
+ self.calls.append(list(messages))
+ self.store_options.append(options.get("store"))
+ self.conversation_ids.append(options.get("conversation_id"))
+ stores_response = options.get("store") is not False if self._honors_store else True
+ conversation_id = "service-thread-1" if stores_response else None
+
+ async def stream_response() -> AsyncIterator[ChatResponseUpdate]:
+ yield ChatResponseUpdate(
+ contents=[Content.from_text("recorded")],
+ role="assistant",
+ conversation_id=conversation_id,
+ )
+
+ return ResponseStream(stream_response(), finalizer=ChatResponse.from_updates)
+
+
class _PerServiceCallHistoryProvider(HistoryProvider):
def __init__(self) -> None:
super().__init__("per_service_call_history", load_messages=False)
@@ -725,9 +764,45 @@ async def save_messages(
response=AgentResponse(messages=[Message(role="assistant", contents=[Content.from_text("hi")])])
)
agent.context_providers = [hp]
- with pytest.raises(RuntimeError, match="history provider"):
+ with pytest.raises(RuntimeError, match="HistoryProvider"):
ResponsesHostServer(agent)
+ def test_init_allows_history_provider_with_load_messages_for_agent_history(self) -> None:
+ hp = InMemoryHistoryProvider()
+ agent = _make_agent(
+ response=AgentResponse(messages=[Message(role="assistant", contents=[Content.from_text("hi")])])
+ )
+ agent.context_providers = [hp]
+
+ ResponsesHostServer(agent, history_source="agent")
+
+ assert agent.context_providers == [hp]
+
+ def test_init_rejects_invalid_history_source(self) -> None:
+ agent = _make_agent(
+ response=AgentResponse(messages=[Message(role="assistant", contents=[Content.from_text("hi")])])
+ )
+
+ with pytest.raises(ValueError, match="history_source"):
+ ResponsesHostServer(agent, history_source="invalid") # type: ignore[arg-type]
+
+ def test_init_rejects_default_conversation_id_for_agent_server_history(self) -> None:
+ agent = Agent(
+ client=_ServiceStorageRecordingClient(),
+ default_options={"conversation_id": "service-thread"},
+ )
+
+ with pytest.raises(RuntimeError, match="default conversation_id"):
+ ResponsesHostServer(agent)
+
+ def test_init_allows_default_conversation_id_for_agent_history(self) -> None:
+ agent = Agent(
+ client=_ServiceStorageRecordingClient(),
+ default_options={"conversation_id": "service-thread"},
+ )
+
+ ResponsesHostServer(agent, history_source="agent")
+
def test_init_rejects_resilient_background_for_non_workflow_agent(self, tmp_path: Path) -> None:
agent = _make_agent(
response=AgentResponse(messages=[Message(role="assistant", contents=[Content.from_text("hi")])])
@@ -845,6 +920,115 @@ async def test_responses_history_is_not_duplicated_by_default_local_history(self
assert InMemoryHistoryProvider.DEFAULT_SOURCE_ID not in stored.state
assert "_foundry_responses_history" not in stored.state
+ async def test_agent_server_history_disables_service_storage(self) -> None:
+ client = _ServiceStorageRecordingClient()
+ agent = Agent(
+ client=client,
+ name="Service Storage Agent",
+ default_options={"store": True},
+ )
+ store = SessionStore()
+ server = _make_server(agent, session_store=store)
+
+ first = await _post(server, input_text="first")
+ second = await _post(server, input_text="second", previous_response_id=first.json()["id"])
+
+ assert second.json()["status"] == "completed"
+ assert [[message.text for message in call] for call in client.calls] == [
+ ["first"],
+ ["first", "recorded", "second"],
+ ]
+ assert client.store_options == [False, False]
+ assert client.conversation_ids == [None, None]
+
+ stored = await store.get(second.json()["id"])
+ assert stored is not None
+ assert stored.service_session_id is None
+
+ async def test_agent_server_history_clears_restored_service_session_id(self) -> None:
+ client = _ServiceStorageRecordingClient()
+ agent = Agent(client=client, name="Migrated Agent")
+ store = SessionStore()
+ server = _make_server(agent, session_store=store)
+ first = await _post(server, input_text="first")
+ first_id = first.json()["id"]
+ stale_session = await store.get(first_id)
+ assert stale_session is not None
+ stale_session.service_session_id = "contaminated-service-thread"
+ await store.set(first_id, stale_session)
+ client.store_options.clear()
+ client.conversation_ids.clear()
+
+ response = await _post(server, input_text="next", previous_response_id=first_id)
+
+ assert response.json()["status"] == "completed"
+ assert client.store_options == [False]
+ assert client.conversation_ids == [None]
+ stored = await store.get(response.json()["id"])
+ assert stored is not None
+ assert stored.service_session_id is None
+
+ async def test_agent_history_preserves_service_storage(self) -> None:
+ client = _ServiceStorageRecordingClient()
+ agent = Agent(
+ client=client,
+ name="Agent Managed Service Storage",
+ default_options={"store": True},
+ )
+ store = SessionStore()
+ server = _make_server(agent, session_store=store, history_source="agent")
+
+ first = await _post(server, input_text="first")
+ second = await _post(server, input_text="second", previous_response_id=first.json()["id"])
+
+ assert second.json()["status"] == "completed"
+ assert [[message.text for message in call] for call in client.calls] == [["first"], ["second"]]
+ assert client.store_options == [True, True]
+ assert client.conversation_ids == [None, "service-thread-1"]
+ stored = await store.get(second.json()["id"])
+ assert stored is not None
+ assert stored.service_session_id == "service-thread-1"
+
+ async def test_agent_history_uses_in_memory_history_from_session_store(self) -> None:
+ client = _RecordingHistoryClient()
+ history = InMemoryHistoryProvider()
+ agent = Agent(
+ client=client,
+ name="Agent Managed In-Memory History",
+ context_providers=[history],
+ default_options={"store": False},
+ )
+ store = SessionStore()
+ server = _make_server(agent, session_store=store, history_source="agent")
+
+ first = await _post(server, input_text="first")
+ second = await _post(server, input_text="second", previous_response_id=first.json()["id"])
+
+ assert second.json()["status"] == "completed"
+ assert [[message.text for message in call] for call in client.calls] == [
+ ["first"],
+ ["first", "recorded", "second"],
+ ]
+ stored = await store.get(second.json()["id"])
+ assert stored is not None
+ assert history.source_id in stored.state
+
+ async def test_client_that_ignores_disabled_storage_fails_without_saving_session(
+ self,
+ caplog: pytest.LogCaptureFixture,
+ ) -> None:
+ client = _ServiceStorageRecordingClient(honors_store=False)
+ agent = Agent(client=client, name="Ignores Store Agent")
+ store = SessionStore()
+ server = _make_server(agent, session_store=store)
+
+ with caplog.at_level(logging.ERROR):
+ response = await _post(server, input_text="first")
+
+ assert response.json()["status"] == "failed"
+ assert "stored this turn server-side" in caplog.text
+ assert await store.get(response.json()["id"]) is None
+
async def test_per_service_call_persistence_preserves_function_loop_history(self) -> None:
provider = _PerServiceCallHistoryProvider()
client = _FunctionLoopRecordingClient(provider)
From 98c216ce5d8b5b54d2366fb26c864656ede315c7 Mon Sep 17 00:00:00 2001
From: eavanvalkenburg
Date: Tue, 1 Sep 2026 18:03:40 +0200
Subject: [PATCH 3/5] Docs: correct history source ADR metadata
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a4c73599-9f66-4c22-bd66-96fe676e0ce6
---
docs/decisions/0039-python-foundry-hosting-history-source.md | 1 -
1 file changed, 1 deletion(-)
diff --git a/docs/decisions/0039-python-foundry-hosting-history-source.md b/docs/decisions/0039-python-foundry-hosting-history-source.md
index 3e76bd775ff..c18a81c3cdf 100644
--- a/docs/decisions/0039-python-foundry-hosting-history-source.md
+++ b/docs/decisions/0039-python-foundry-hosting-history-source.md
@@ -3,7 +3,6 @@ status: proposed
contact: eavanvalkenburg
date: 2026-09-01
deciders: eavanvalkenburg, moonbox3
-consulted: sachinkahawala
---
# Select the conversation history source for Python Foundry hosting
From a77eeb50b4a88ed6be7c8d0ef1cd24d2703e5ece Mon Sep 17 00:00:00 2001
From: eavanvalkenburg
Date: Tue, 1 Sep 2026 18:18:22 +0200
Subject: [PATCH 4/5] Python: harden hosted history selection
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a4c73599-9f66-4c22-bd66-96fe676e0ce6
---
...9-python-foundry-hosting-history-source.md | 16 +--
python/packages/foundry_hosting/README.md | 6 +-
.../_responses.py | 27 ++++-
.../foundry_hosting/tests/test_responses.py | 50 ++++++++-
.../tests/test_responses_int.py | 100 ++++++++++++++----
5 files changed, 166 insertions(+), 33 deletions(-)
diff --git a/docs/decisions/0039-python-foundry-hosting-history-source.md b/docs/decisions/0039-python-foundry-hosting-history-source.md
index c18a81c3cdf..b3f9b1ceb5f 100644
--- a/docs/decisions/0039-python-foundry-hosting-history-source.md
+++ b/docs/decisions/0039-python-foundry-hosting-history-source.md
@@ -27,8 +27,9 @@ The host must prevent duplicate model history without removing the regular agent
- Feed one canonical conversation transcript into each model call.
- Keep AgentServer response persistence independent from the model's history source.
- Preserve the normal agent choice between `HistoryProvider` and downstream service storage.
+- Let applications choose storage that satisfies their compliance, residency, retention, deletion, encryption, and
+ audit requirements.
- Retain AgentServer response history as the default hosting behavior.
-- Avoid forcing the measured Foundry `store=False` streaming latency on users who select regular agent history.
- Make existing sessions containing a downstream service ID safe after upgrade.
## Considered Options
@@ -38,12 +39,12 @@ The host must prevent duplicate model history without removing the regular agent
- Good: one simple default and parity with current .NET Foundry hosting.
- Good: the selected response provider controls the transcript used by the model.
- Bad: users cannot use normal agent history providers or service-side continuation.
-- Bad: raw benchmarking measured a 5.43-second median streaming penalty for `store=False` on a Foundry project
- endpoint using `gpt-5.4-nano`; the OpenAI public endpoint did not show this penalty.
+- Bad: applications cannot choose a history backend that meets their data-governance requirements independently of
+ AgentServer protocol storage.
### Clear the service ID but leave downstream storage enabled
-- Good: avoids duplicated input and preserves Foundry streaming latency.
+- Good: avoids duplicated input.
- Bad: creates an untracked stored response or conversation on every model call.
- Bad: service-side storage incurs retention and cost but is never used for continuation.
@@ -58,6 +59,7 @@ The host must prevent duplicate model history without removing the regular agent
- Good: the host makes only the decision it owns: whether AgentServer history supersedes normal agent behavior.
- Good: regular agent mode preserves service storage, in-session history, and external history providers.
- Good: `ResponseProviderProtocol`, `HistoryProvider`, and `SessionStore` remain independent extension points.
+- Good: applications can select the storage boundary and lifecycle required by their compliance policies.
- Neutral: AgentServer still manages protocol-level Responses persistence in regular agent mode, according to the outer
request, but does not replay that transcript into the model.
@@ -70,7 +72,8 @@ With `history_source="agent_server"`:
- load-enabled `HistoryProvider` instances are rejected;
- an agent-level default `conversation_id` is rejected;
- the configured response provider transcript and current input are passed to the agent;
-- downstream `store=False` overrides agent defaults on every run;
+- clients advertising `STORES_BY_DEFAULT=True` receive a downstream `store=False` override;
+- for other clients, an explicit agent-level `store` option is removed and no storage option is forwarded;
- a restored `service_session_id` is cleared before the run;
- a client that still returns a service ID fails the response and the contaminated session is not saved; and
- a transient `InMemoryHistoryProvider` supports intra-run function calls but is removed before session persistence.
@@ -103,7 +106,8 @@ It does not disable response persistence.
## Consequences
- Good: existing applications keep AgentServer history as their default.
-- Good: applications can retain service-side storage and its current Foundry latency characteristics.
+- Good: applications can choose service-side, session-backed, or external history storage to meet data-governance
+ requirements.
- Good: the API does not introduce a second history-selection state machine.
- Bad: default mode mutates the supplied `RawAgent` by installing a transient history provider.
- Bad: regular agent history and AgentServer response history may differ, which response-oriented evaluations must
diff --git a/python/packages/foundry_hosting/README.md b/python/packages/foundry_hosting/README.md
index c5c43f8dc60..bdd60368011 100644
--- a/python/packages/foundry_hosting/README.md
+++ b/python/packages/foundry_hosting/README.md
@@ -12,8 +12,10 @@ server = ResponsesHostServer(agent)
In this mode, the configured AgentServer response provider supplies the prior transcript. Hosting rejects
`HistoryProvider` instances with `load_messages=True` and agents configured with a default `conversation_id`, adds a
-transient in-memory provider for function-call loops, forces downstream `store=False`, and clears restored downstream
-service IDs. These safeguards ensure the model receives the transcript once.
+transient in-memory provider for function-call loops, and clears restored downstream service IDs. For clients that
+advertise `STORES_BY_DEFAULT=True`, hosting forces downstream `store=False`; for other clients it removes an explicit
+agent-level `store` option and does not forward one. These safeguards ensure the model receives the transcript once
+without sending unsupported storage options.
To preserve the agent's regular history and service-storage behavior, select the agent as the history source:
diff --git a/python/packages/foundry_hosting/agent_framework_foundry_hosting/_responses.py b/python/packages/foundry_hosting/agent_framework_foundry_hosting/_responses.py
index 85c875fee38..7a922b86b0a 100644
--- a/python/packages/foundry_hosting/agent_framework_foundry_hosting/_responses.py
+++ b/python/packages/foundry_hosting/agent_framework_foundry_hosting/_responses.py
@@ -392,6 +392,7 @@ def __init__(
super().__init__(prefix=prefix, options=options, store=store, **kwargs)
self._uses_agent_server_history = history_source == "agent_server"
+ self._client_stores_by_default = False
if self._uses_agent_server_history:
for provider in getattr(agent, "context_providers", []):
if isinstance(provider, HistoryProvider) and provider.load_messages:
@@ -412,6 +413,17 @@ def __init__(
"Remove that option or construct ResponsesHostServer with history_source='agent' to resume "
"the downstream service conversation."
)
+ agent_client = getattr(agent, "client", None)
+ storage_capability_owner = agent_client if agent_client is not None else agent
+ self._client_stores_by_default = getattr(storage_capability_owner, "STORES_BY_DEFAULT", False) is True
+ if not self._client_stores_by_default and isinstance(default_options, dict):
+ cast(dict[str, Any], default_options).pop("store", None)
+ elif isinstance(agent, RawAgent):
+ # A caller may reuse an agent that was previously attached to an AgentServer-history
+ # host. Restore regular agent behavior by removing only the host-owned sentinel.
+ agent.context_providers[:] = [
+ provider for provider in agent.context_providers if not _is_hosted_responses_history_sentinel(provider)
+ ]
self._is_workflow_agent = False
if isinstance(agent, WorkflowAgent):
@@ -660,12 +672,21 @@ async def _handle_inner_agent(
}
chat_options, are_options_set = _to_chat_options(request)
if self._uses_agent_server_history:
- # The response provider already owns the transcript used for this run. Keep the
- # downstream service stateless so it cannot become a second history source.
- chat_options["store"] = False
+ if self._client_stores_by_default:
+ # The response provider already owns the transcript used for this run. Keep a
+ # storing downstream service stateless so it cannot become a second history source.
+ chat_options["store"] = False
+ else:
+ # Do not pass a storage option to clients that do not advertise support for it.
+ chat_options.pop("store", None)
if are_options_set and not isinstance(self._agent, RawAgent):
logger.warning("Agent doesn't support runtime options. They will be ignored.")
+ if self._uses_agent_server_history and self._client_stores_by_default:
+ # Request generation options are unsupported for custom agents, but the
+ # host-owned storage directive must still reach an agent that advertises
+ # service-side storage.
+ run_kwargs["options"] = {"store": False}
else:
run_kwargs["options"] = chat_options
diff --git a/python/packages/foundry_hosting/tests/test_responses.py b/python/packages/foundry_hosting/tests/test_responses.py
index 2e92bb87ed0..f219c33febf 100644
--- a/python/packages/foundry_hosting/tests/test_responses.py
+++ b/python/packages/foundry_hosting/tests/test_responses.py
@@ -175,6 +175,7 @@ class _RecordingHistoryClient(BaseChatClient):
def __init__(self) -> None:
super().__init__()
self.calls: list[list[Message]] = []
+ self.options: list[dict[str, Any]] = []
def _inner_get_response(
self,
@@ -184,9 +185,10 @@ def _inner_get_response(
options: Mapping[str, Any],
**kwargs: Any,
) -> Awaitable[ChatResponse] | ResponseStream[ChatResponseUpdate, ChatResponse]:
- del options, kwargs
+ del kwargs
assert stream is True, "The inner agent only runs in stream mode in Foundry Hosted Agents."
self.calls.append(list(messages))
+ self.options.append(dict(options))
async def stream_response() -> AsyncIterator[ChatResponseUpdate]:
yield ChatResponseUpdate(contents=[Content.from_text("recorded")], role="assistant")
@@ -803,6 +805,23 @@ def test_init_allows_default_conversation_id_for_agent_history(self) -> None:
ResponsesHostServer(agent, history_source="agent")
+ def test_init_agent_history_removes_hosted_history_sentinel_from_reused_agent(self) -> None:
+ agent = Agent(client=_ServiceStorageRecordingClient())
+ ResponsesHostServer(agent)
+ assert any(
+ provider.source_id == "_foundry_responses_history"
+ for provider in agent.context_providers
+ if isinstance(provider, HistoryProvider)
+ )
+
+ ResponsesHostServer(agent, history_source="agent")
+
+ assert not any(
+ provider.source_id == "_foundry_responses_history"
+ for provider in agent.context_providers
+ if isinstance(provider, HistoryProvider)
+ )
+
def test_init_rejects_resilient_background_for_non_workflow_agent(self, tmp_path: Path) -> None:
agent = _make_agent(
response=AgentResponse(messages=[Message(role="assistant", contents=[Content.from_text("hi")])])
@@ -945,6 +964,35 @@ async def test_agent_server_history_disables_service_storage(self) -> None:
assert stored is not None
assert stored.service_session_id is None
+ async def test_agent_server_history_removes_store_for_non_storing_client(self) -> None:
+ client = _RecordingHistoryClient()
+ agent = Agent(
+ client=client,
+ name="Non-Storing Agent",
+ default_options={"store": True},
+ )
+ server = _make_server(agent, session_store=SessionStore())
+
+ response = await _post(server, input_text="first")
+
+ assert response.json()["status"] == "completed"
+ assert "store" not in agent.default_options
+ assert "store" not in client.options[0]
+
+ async def test_agent_server_history_preserves_storage_directive_for_custom_agent_options(self) -> None:
+ agent = _make_agent(
+ response=AgentResponse(messages=[Message(role="assistant", contents=[Content.from_text("ok")])]),
+ raw_agent=False,
+ )
+ agent.client = MagicMock()
+ agent.client.STORES_BY_DEFAULT = True
+ server = _make_server(agent, session_store=SessionStore())
+
+ response = await _post(server, input_text="first", temperature=0.5)
+
+ assert response.json()["status"] == "completed"
+ assert agent.run.call_args.kwargs["options"] == {"store": False}
+
async def test_agent_server_history_clears_restored_service_session_id(self) -> None:
client = _ServiceStorageRecordingClient()
agent = Agent(client=client, name="Migrated Agent")
diff --git a/python/packages/foundry_hosting/tests/test_responses_int.py b/python/packages/foundry_hosting/tests/test_responses_int.py
index b157ef978b6..0079037c4b9 100644
--- a/python/packages/foundry_hosting/tests/test_responses_int.py
+++ b/python/packages/foundry_hosting/tests/test_responses_int.py
@@ -34,6 +34,7 @@
Agent,
Content,
Executor,
+ InMemoryHistoryProvider,
Message,
SlidingWindowStrategy,
WorkflowBuilder,
@@ -73,7 +74,6 @@ def server() -> ResponsesHostServer:
agent = Agent(
client=client, # ty: ignore[invalid-argument-type]
instructions="You are a concise assistant. Keep answers very short (one or two sentences).",
- default_options={"store": False}, # pyrefly: ignore[bad-argument-type]
)
return ResponsesHostServer(agent, store=InMemoryResponseProvider())
@@ -85,19 +85,37 @@ async def get_weather(location: Annotated[str, "The city name"]) -> str:
return f"The weather in {location} is 72°F and sunny."
-@pytest.fixture
-def server_with_tools() -> ResponsesHostServer:
- """Create a ResponsesHostServer whose agent has a tool."""
+@pytest.fixture(params=["agent_server", "agent"], ids=["agent-server-history", "agent-history"])
+def history_server(request: pytest.FixtureRequest) -> ResponsesHostServer:
+ """Create a real Foundry server for each model-history source."""
client = FoundryChatClient(credential=AzureCliCredential()) # pyrefly: ignore[bad-argument-type]
+ agent = Agent(
+ client=client, # ty: ignore[invalid-argument-type]
+ instructions="You are a concise assistant. Keep answers very short (one or two sentences).",
+ default_options={"store": True}, # pyrefly: ignore[bad-argument-type]
+ )
+ return ResponsesHostServer(
+ agent,
+ store=InMemoryResponseProvider(),
+ history_source=request.param,
+ )
+
+@pytest.fixture(params=["agent_server", "agent"], ids=["agent-server-history", "agent-history"])
+def history_server_with_tools(request: pytest.FixtureRequest) -> ResponsesHostServer:
+ """Create a real Foundry tool-calling server for each model-history source."""
+ client = FoundryChatClient(credential=AzureCliCredential()) # pyrefly: ignore[bad-argument-type]
agent = Agent(
client=client, # ty: ignore[invalid-argument-type]
instructions="You are a concise assistant. Use the provided tools when appropriate. Keep answers very short.",
tools=[get_weather],
- default_options={"store": False}, # pyrefly: ignore[bad-argument-type]
+ default_options={"store": True}, # pyrefly: ignore[bad-argument-type]
+ )
+ return ResponsesHostServer(
+ agent,
+ store=InMemoryResponseProvider(),
+ history_source=request.param,
)
-
- return ResponsesHostServer(agent, store=InMemoryResponseProvider())
# ---------------------------------------------------------------------------
@@ -445,11 +463,11 @@ async def test_explicit_user_assistant_user_conversation(self, server: Responses
@pytest.mark.flaky
@pytest.mark.integration
@skip_if_foundry_hosting_integration_tests_disabled
- async def test_two_turn_conversation(self, server: ResponsesHostServer) -> None:
+ async def test_two_turn_conversation(self, history_server: ResponsesHostServer) -> None:
"""Turn 1: introduce context. Turn 2: ask about it using previous_response_id."""
# Turn 1
resp1 = await _post_json(
- server,
+ history_server,
{
"input": "My favorite color is blue. Remember that.",
"stream": False,
@@ -463,7 +481,7 @@ async def test_two_turn_conversation(self, server: ResponsesHostServer) -> None:
# Turn 2 — references turn 1
resp2 = await _post_json(
- server,
+ history_server,
{
"input": "What is my favorite color?",
"stream": False,
@@ -482,11 +500,11 @@ async def test_two_turn_conversation(self, server: ResponsesHostServer) -> None:
@pytest.mark.flaky
@pytest.mark.integration
@skip_if_foundry_hosting_integration_tests_disabled
- async def test_three_turn_conversation(self, server: ResponsesHostServer) -> None:
+ async def test_three_turn_conversation(self, history_server: ResponsesHostServer) -> None:
"""Three sequential turns to verify history accumulates correctly."""
# Turn 1
resp1 = await _post_json(
- server,
+ history_server,
{
"input": "I have a pet dog named Max.",
"stream": False,
@@ -497,7 +515,7 @@ async def test_three_turn_conversation(self, server: ResponsesHostServer) -> Non
# Turn 2
resp2 = await _post_json(
- server,
+ history_server,
{
"input": "I also have a cat named Luna.",
"stream": False,
@@ -509,7 +527,7 @@ async def test_three_turn_conversation(self, server: ResponsesHostServer) -> Non
# Turn 3 — should remember both pets
resp3 = await _post_json(
- server,
+ history_server,
{
"input": "What are my pets' names?",
"stream": False,
@@ -527,11 +545,11 @@ async def test_three_turn_conversation(self, server: ResponsesHostServer) -> Non
@pytest.mark.flaky
@pytest.mark.integration
@skip_if_foundry_hosting_integration_tests_disabled
- async def test_multi_turn_streaming(self, server: ResponsesHostServer) -> None:
+ async def test_multi_turn_streaming(self, history_server: ResponsesHostServer) -> None:
"""Multi-turn conversation with streaming on the second turn."""
# Turn 1 — non-streaming
resp1 = await _post_json(
- server,
+ history_server,
{
"input": "My favorite number is 42.",
"stream": False,
@@ -542,7 +560,7 @@ async def test_multi_turn_streaming(self, server: ResponsesHostServer) -> None:
# Turn 2 — streaming
resp2 = await _post_json(
- server,
+ history_server,
{
"input": "What is my favorite number?",
"stream": True,
@@ -562,6 +580,46 @@ async def test_multi_turn_streaming(self, server: ResponsesHostServer) -> None:
done_events = [e for e in events if e["event"] == "response.output_text.done"]
assert "42" in done_events[0]["data"]["text"]
+ @pytest.mark.flaky
+ @pytest.mark.integration
+ @skip_if_foundry_hosting_integration_tests_disabled
+ async def test_agent_history_with_in_memory_provider(self) -> None:
+ """Regular agent mode can persist in-session history while the model service stays stateless."""
+ agent = Agent(
+ client=FoundryChatClient(credential=AzureCliCredential()), # ty: ignore[invalid-argument-type]
+ instructions="Answer questions using the supplied conversation history. Keep answers very short.",
+ context_providers=[InMemoryHistoryProvider()],
+ default_options={"store": False}, # pyrefly: ignore[bad-argument-type]
+ )
+ server = ResponsesHostServer(
+ agent,
+ store=InMemoryResponseProvider(),
+ history_source="agent",
+ )
+
+ first = await _post_json(
+ server,
+ {
+ "input": "My favorite city is Lisbon. Remember that.",
+ "stream": False,
+ },
+ )
+ assert first.status_code == 200
+
+ second = await _post_json(
+ server,
+ {
+ "input": "What is my favorite city?",
+ "stream": False,
+ "previous_response_id": first.json()["id"],
+ },
+ )
+
+ assert second.status_code == 200
+ output_messages = [item for item in second.json()["output"] if item["type"] == "message"]
+ assert len(output_messages) == 1
+ assert "lisbon" in output_messages[0]["content"][0]["text"].lower()
+
class TestReasoningHostedMcpReplay:
"""Regression coverage for stateless reasoning + hosted MCP replay."""
@@ -761,10 +819,10 @@ class TestToolCalling:
@pytest.mark.flaky
@pytest.mark.integration
@skip_if_foundry_hosting_integration_tests_disabled
- async def test_tool_call_non_streaming(self, server_with_tools: ResponsesHostServer) -> None:
+ async def test_tool_call_non_streaming(self, history_server_with_tools: ResponsesHostServer) -> None:
"""Agent invokes a tool and returns a final answer (non-streaming)."""
resp = await _post_json(
- server_with_tools,
+ history_server_with_tools,
{
"input": "What is the weather in Seattle?",
"stream": False,
@@ -784,10 +842,10 @@ async def test_tool_call_non_streaming(self, server_with_tools: ResponsesHostSer
@pytest.mark.flaky
@pytest.mark.integration
@skip_if_foundry_hosting_integration_tests_disabled
- async def test_tool_call_streaming(self, server_with_tools: ResponsesHostServer) -> None:
+ async def test_tool_call_streaming(self, history_server_with_tools: ResponsesHostServer) -> None:
"""Agent invokes a tool and returns a final answer (streaming)."""
resp = await _post_json(
- server_with_tools,
+ history_server_with_tools,
{
"input": "What is the weather in Seattle?",
"stream": True,
From 5713d33ca739a32646220766ec1c8741635d57b6 Mon Sep 17 00:00:00 2001
From: eavanvalkenburg
Date: Tue, 1 Sep 2026 21:02:31 +0200
Subject: [PATCH 5/5] Python: document hosted agent ownership
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a4c73599-9f66-4c22-bd66-96fe676e0ce6
---
...9-python-foundry-hosting-history-source.md | 2 ++
python/packages/foundry_hosting/README.md | 3 ++
.../_responses.py | 12 +++----
.../foundry_hosting/tests/test_responses.py | 31 +++++--------------
4 files changed, 16 insertions(+), 32 deletions(-)
diff --git a/docs/decisions/0039-python-foundry-hosting-history-source.md b/docs/decisions/0039-python-foundry-hosting-history-source.md
index b3f9b1ceb5f..d17fa3b92ab 100644
--- a/docs/decisions/0039-python-foundry-hosting-history-source.md
+++ b/docs/decisions/0039-python-foundry-hosting-history-source.md
@@ -110,6 +110,8 @@ It does not disable response persistence.
requirements.
- Good: the API does not introduce a second history-selection state machine.
- Bad: default mode mutates the supplied `RawAgent` by installing a transient history provider.
+- Neutral: a `ResponsesHostServer` owns its supplied agent instance; reusing that agent with another host or invoking it
+ directly after server construction is unsupported.
- Bad: regular agent history and AgentServer response history may differ, which response-oriented evaluations must
document.
- Neutral: switching an existing conversation between modes may require resetting its persisted session/history.
diff --git a/python/packages/foundry_hosting/README.md b/python/packages/foundry_hosting/README.md
index bdd60368011..710d2d9fb02 100644
--- a/python/packages/foundry_hosting/README.md
+++ b/python/packages/foundry_hosting/README.md
@@ -17,6 +17,9 @@ advertise `STORES_BY_DEFAULT=True`, hosting forces downstream `store=False`; for
agent-level `store` option and does not forward one. These safeguards ensure the model receives the transcript once
without sending unsupported storage options.
+`ResponsesHostServer` owns the supplied agent instance and may add hosting-specific context providers. Do not reuse that
+agent with another host or invoke it directly after constructing the server.
+
To preserve the agent's regular history and service-storage behavior, select the agent as the history source:
```python
diff --git a/python/packages/foundry_hosting/agent_framework_foundry_hosting/_responses.py b/python/packages/foundry_hosting/agent_framework_foundry_hosting/_responses.py
index 7a922b86b0a..43713c8a135 100644
--- a/python/packages/foundry_hosting/agent_framework_foundry_hosting/_responses.py
+++ b/python/packages/foundry_hosting/agent_framework_foundry_hosting/_responses.py
@@ -367,13 +367,15 @@ def __init__(
because the hosting environment may get deactivated between requests. Provider
state carried by `AgentSession`, including `InMemoryHistoryProvider` messages in
`history_source="agent"` mode, is persisted by the configured session store.
- 3. Resiliency (resilient_background=True) is ONLY supported for workflows; constructing this
+ 3. The server owns the supplied agent instance and may add hosting-specific providers.
+ Do not reuse the same agent with another host or invoke it directly after construction.
+ 4. Resiliency (resilient_background=True) is ONLY supported for workflows; constructing this
server with a non-workflow agent and `resilient_background=True` raises `RuntimeError`.
When resiliency is enabled, and the server crashes mid-response:
- Background responses are automatically re-invoked on server restart (client won't see the crash).
- Stream events are preserved for client reconnection.
- State is maintained across crashes.
- 4. Steering (steerable_conversations=True) is ONLY supported for non-workflow agents; constructing
+ 5. Steering (steerable_conversations=True) is ONLY supported for non-workflow agents; constructing
this server with a workflow agent and `steerable_conversations=True` raises `RuntimeError`.
Steering a workflow is conceptually undefined -- a workflow's graph may have loops or parallel
branches with no single well-defined "current point" to cancel and resume from, unlike an
@@ -418,12 +420,6 @@ def __init__(
self._client_stores_by_default = getattr(storage_capability_owner, "STORES_BY_DEFAULT", False) is True
if not self._client_stores_by_default and isinstance(default_options, dict):
cast(dict[str, Any], default_options).pop("store", None)
- elif isinstance(agent, RawAgent):
- # A caller may reuse an agent that was previously attached to an AgentServer-history
- # host. Restore regular agent behavior by removing only the host-owned sentinel.
- agent.context_providers[:] = [
- provider for provider in agent.context_providers if not _is_hosted_responses_history_sentinel(provider)
- ]
self._is_workflow_agent = False
if isinstance(agent, WorkflowAgent):
diff --git a/python/packages/foundry_hosting/tests/test_responses.py b/python/packages/foundry_hosting/tests/test_responses.py
index f219c33febf..d6b51d7374d 100644
--- a/python/packages/foundry_hosting/tests/test_responses.py
+++ b/python/packages/foundry_hosting/tests/test_responses.py
@@ -786,12 +786,12 @@ def test_init_rejects_invalid_history_source(self) -> None:
)
with pytest.raises(ValueError, match="history_source"):
- ResponsesHostServer(agent, history_source="invalid") # type: ignore[arg-type]
+ ResponsesHostServer(agent, history_source=cast(Any, "invalid"))
def test_init_rejects_default_conversation_id_for_agent_server_history(self) -> None:
agent = Agent(
client=_ServiceStorageRecordingClient(),
- default_options={"conversation_id": "service-thread"},
+ default_options={"conversation_id": "service-thread"}, # pyrefly: ignore[bad-argument-type]
)
with pytest.raises(RuntimeError, match="default conversation_id"):
@@ -800,28 +800,11 @@ def test_init_rejects_default_conversation_id_for_agent_server_history(self) ->
def test_init_allows_default_conversation_id_for_agent_history(self) -> None:
agent = Agent(
client=_ServiceStorageRecordingClient(),
- default_options={"conversation_id": "service-thread"},
+ default_options={"conversation_id": "service-thread"}, # pyrefly: ignore[bad-argument-type]
)
ResponsesHostServer(agent, history_source="agent")
- def test_init_agent_history_removes_hosted_history_sentinel_from_reused_agent(self) -> None:
- agent = Agent(client=_ServiceStorageRecordingClient())
- ResponsesHostServer(agent)
- assert any(
- provider.source_id == "_foundry_responses_history"
- for provider in agent.context_providers
- if isinstance(provider, HistoryProvider)
- )
-
- ResponsesHostServer(agent, history_source="agent")
-
- assert not any(
- provider.source_id == "_foundry_responses_history"
- for provider in agent.context_providers
- if isinstance(provider, HistoryProvider)
- )
-
def test_init_rejects_resilient_background_for_non_workflow_agent(self, tmp_path: Path) -> None:
agent = _make_agent(
response=AgentResponse(messages=[Message(role="assistant", contents=[Content.from_text("hi")])])
@@ -944,7 +927,7 @@ async def test_agent_server_history_disables_service_storage(self) -> None:
agent = Agent(
client=client,
name="Service Storage Agent",
- default_options={"store": True},
+ default_options={"store": True}, # pyrefly: ignore[bad-argument-type]
)
store = SessionStore()
server = _make_server(agent, session_store=store)
@@ -969,7 +952,7 @@ async def test_agent_server_history_removes_store_for_non_storing_client(self) -
agent = Agent(
client=client,
name="Non-Storing Agent",
- default_options={"store": True},
+ default_options={"store": True}, # pyrefly: ignore[bad-argument-type]
)
server = _make_server(agent, session_store=SessionStore())
@@ -1021,7 +1004,7 @@ async def test_agent_history_preserves_service_storage(self) -> None:
agent = Agent(
client=client,
name="Agent Managed Service Storage",
- default_options={"store": True},
+ default_options={"store": True}, # pyrefly: ignore[bad-argument-type]
)
store = SessionStore()
server = _make_server(agent, session_store=store, history_source="agent")
@@ -1044,7 +1027,7 @@ async def test_agent_history_uses_in_memory_history_from_session_store(self) ->
client=client,
name="Agent Managed In-Memory History",
context_providers=[history],
- default_options={"store": False},
+ default_options={"store": False}, # pyrefly: ignore[bad-argument-type]
)
store = SessionStore()
server = _make_server(agent, session_store=store, history_source="agent")