diff --git a/python/packages/core/agent_framework/_agents.py b/python/packages/core/agent_framework/_agents.py index f9a7a0c2205..0ca3f9b397d 100644 --- a/python/packages/core/agent_framework/_agents.py +++ b/python/packages/core/agent_framework/_agents.py @@ -1197,11 +1197,16 @@ async def _prepare_run_context( # Resolve conversation_id from the same combined view so an agent-level default is honored # when the runtime omits it (a live session id still takes precedence below). effective_conversation_id = effective_options.get("conversation_id") - # Auto-inject InMemoryHistoryProvider when session is provided, no context providers - # registered, and no service-side storage indicators + # Auto-inject InMemoryHistoryProvider when a session is provided and no + # loading history provider exists yet. Gating on history providers (not + # any context provider) keeps local history when non-history providers + # like SkillsProvider are present, so multi-turn flows on stateless + # clients don't lose prior messages. if ( session is not None - and not self.context_providers + and not any( + provider.load_messages for provider in self.context_providers if isinstance(provider, HistoryProvider) + ) and not session.service_session_id and not effective_conversation_id and not service_stores_history diff --git a/python/packages/core/tests/core/test_agents.py b/python/packages/core/tests/core/test_agents.py index 4de14fa8077..9d5257d4aa7 100644 --- a/python/packages/core/tests/core/test_agents.py +++ b/python/packages/core/tests/core/test_agents.py @@ -2534,6 +2534,63 @@ async def test_stores_by_default_with_store_false_in_default_options_injects_inm assert any(isinstance(p, InMemoryHistoryProvider) for p in agent.context_providers) +async def test_non_history_context_provider_still_injects_inmemory( + client: SupportsChatGetResponse, +) -> None: + """A non-history context provider must not suppress local history injection. + + Regression for the case where registering a context provider that is not a + HistoryProvider (e.g. SkillsProvider, FileAccessProvider, or a RAG memory + provider) prevented the auto-injected InMemoryHistoryProvider. Without local + history, multi-turn flows on stateless clients (such as the tool-approval + resume turn) drop the prior assistant function_call. + """ + from agent_framework._sessions import InMemoryHistoryProvider + + agent = Agent(client=client, context_providers=[MockContextProvider()]) + session = agent.create_session() + + await agent.run("Hello", session=session) + + # The non-history provider should not block local-history injection. + assert any(isinstance(p, InMemoryHistoryProvider) for p in agent.context_providers) + + +async def test_existing_loading_history_provider_skips_inmemory_injection( + client: SupportsChatGetResponse, +) -> None: + """An existing loading HistoryProvider suppresses injection even with other providers.""" + from agent_framework._sessions import InMemoryHistoryProvider + + existing = InMemoryHistoryProvider("custom", load_messages=True) + agent = Agent(client=client, context_providers=[existing, MockContextProvider()]) + session = agent.create_session() + + await agent.run("Hello", session=session) + + history_providers = [p for p in agent.context_providers if isinstance(p, InMemoryHistoryProvider)] + assert history_providers == [existing] + + +async def test_persist_only_history_provider_still_injects_inmemory( + client: SupportsChatGetResponse, +) -> None: + """A persist-only (load_messages=False) HistoryProvider does not satisfy the loading need.""" + from agent_framework._sessions import InMemoryHistoryProvider + + audit = InMemoryHistoryProvider("audit", load_messages=False) + agent = Agent(client=client, context_providers=[audit]) + session = agent.create_session() + + await agent.run("Hello", session=session) + + loading_providers = [ + p for p in agent.context_providers if isinstance(p, InMemoryHistoryProvider) and p.load_messages + ] + assert len(loading_providers) == 1 + assert loading_providers[0] is not audit + + async def test_shared_local_storage_cross_provider_responses_history_does_not_leak_fc_id() -> None: """Responses-specific replay metadata should stay local to Responses when session storage is shared.""" from openai.types.chat.chat_completion import ChatCompletion, Choice