From a6ea50e3ab2e41ed0ebc256104a897d3f8d11e64 Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Tue, 1 Sep 2026 17:58:41 +0200 Subject: [PATCH 1/4] 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 0000000000..3e76bd775f --- /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 d73bc714c8..c5c43f8dc6 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 d75628cbab..85c875fee3 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 50f0c0ff50..2e92bb87ed 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 2/4] 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 3e76bd775f..c18a81c3cd 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 3/4] 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 c18a81c3cd..b3f9b1ceb5 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 c5c43f8dc6..bdd6036801 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 85c875fee3..7a922b86b0 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 2e92bb87ed..f219c33feb 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 b157ef978b..0079037c4b 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 4/4] 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 b3f9b1ceb5..d17fa3b92a 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 bdd6036801..710d2d9fb0 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 7a922b86b0..43713c8a13 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 f219c33feb..d6b51d7374 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")