Python: Fix per-service-call history persistence with server-storing clients#6310
Open
eavanvalkenburg wants to merge 2 commits into
Open
Conversation
When an Agent set require_per_service_call_history_persistence=True together with a HistoryProvider, and the chat client stored history server-side by default (e.g. OpenAIChatClient, STORES_BY_DEFAULT=True), the external history provider was silently never persisted. Unify persistence on the per-service-call middleware: when the flag is set and a HistoryProvider exists, the middleware is always installed and owns persistence. service_stores_history now only selects middleware behavior: - service does not store: load providers and drive the function loop with a local sentinel conversation id, or - service stores: skip loading (the service owns history) and persist each service call while the real conversation id flows through. Also rationalize chat-options handling in _prepare_run_context: - _merge_options now skips None overrides and strips remaining None values, so an unset `store` is never forwarded and the service decides its own default. - Resolve `store` and `conversation_id` once from a single combined view (effective_options) instead of probing both default and runtime dicts; the auto-injection and per-service-call resolution now agree on conversation_id. Fixes microsoft#5798 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes a Python SDK bug where require_per_service_call_history_persistence=True combined with an external HistoryProvider could silently skip external persistence when the underlying chat client stores history server-side by default (e.g., OpenAI clients with STORES_BY_DEFAULT=True). The fix centralizes history persistence responsibility in the per-service-call middleware and rationalizes option-merging so unset values (notably store=None) are not forwarded to clients.
Changes:
- Always installs
PerServiceCallHistoryPersistingMiddlewarewhen per-service-call persistence is required and aHistoryProvideris present; service-side storage now only changes how the middleware behaves (load+persist vs persist-only). - Updates
_prepare_run_contextto resolvestore/conversation_idfrom a single merged options view and to treatNoneas “unset” (not forwarded). - Adds a scenario-matrix test suite validating persistence timing across storing/non-storing clients, streaming/non-streaming runs, and
storeoverrides.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| python/packages/core/agent_framework/_agents.py | Unifies option resolution and ensures per-service-call persistence is owned by middleware in both local and service-managed cases, with warning logging when load is bypassed. |
| python/packages/core/agent_framework/_sessions.py | Extends per-service-call middleware to support a “service stores history” mode (persist-only; no provider load; no local sentinel behavior). |
| python/packages/core/agent_framework/_clients.py | Updates as_agent() docstring to describe the per-service-call persistence behavior (note: one doc line currently contradicts implementation). |
| python/packages/core/tests/core/test_agents.py | Adds regression tests and a scenario matrix asserting per-service-call persistence timing and store=None non-forwarding. |
Python Test Coverage Report •
Python Unit Test Overview
|
||||||||||||||||||||||||||||||||||||||||
…ce per run Address PR review: when the client stores history server-side, the per-service-call middleware still persists after each model call; only provider loading is skipped. The previous "persist once per run()" wording contradicted the implementation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation and Context
When an
Agentwas configured withrequire_per_service_call_history_persistence=Truetogether with aHistoryProvider, and the underlying chat client stored history server-side by default (e.g.OpenAIChatClient, whereSTORES_BY_DEFAULT=True), the external history provider was silently never persisted. The per-service-call middleware was skipped because the service was assumed to own history, and the once-per-run path also skipped the provider — so neither persisted.Fixes #5798
Description
Unify persistence on the per-service-call middleware. When
require_per_service_call_history_persistence=Trueand aHistoryProviderexists, thePerServiceCallHistoryPersistingMiddlewareis now always installed and owns persistence.service_stores_historyonly selects how the middleware behaves, never whether it persists:load_messages=Truewhose load is bypassed.The observable contract: with the flag on, persistence happens per service call — in a function-call → final-completion run, the function-call turn is persisted before the second call starts.
Rationalize chat-options handling in
_prepare_run_context:_merge_optionsnow skipsNoneoverrides and strips remainingNonevalues in a single pass, so an unsetstoreis never forwarded to the client and the service decides its own default (STORES_BY_DEFAULTis only an internal behavior hint).storeandconversation_idare resolved once from a single combined view (effective_options) instead of probing both the agent-default and runtime dicts separately. TheInMemoryHistoryProviderauto-injection and the per-service-call resolution now agree onconversation_id(an agent-level default is honored consistently).Tests are added to
test_agents.pyas a scenario matrix (sync + streaming) that asserts the per-service-call persistence timing across storing/non-storing clients andstoreoverrides.Contribution Checklist