Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Python: Fix per-service-call history persistence with server-storing clients#6310

Merged
moonbox3 merged 3 commits into
microsoft:mainfrom
eavanvalkenburg:fix/5798-per-service-call-history-persistence
Jun 9, 2026
Merged

Python: Fix per-service-call history persistence with server-storing clients#6310
moonbox3 merged 3 commits into
microsoft:mainfrom
eavanvalkenburg:fix/5798-per-service-call-history-persistence

Conversation

@eavanvalkenburg

Copy link
Copy Markdown
Member

Motivation and Context

When an Agent was configured with require_per_service_call_history_persistence=True together with a HistoryProvider, and the underlying chat client stored history server-side by default (e.g. OpenAIChatClient, where STORES_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=True and a HistoryProvider exists, the PerServiceCallHistoryPersistingMiddleware is now always installed and owns persistence. service_stores_history only selects how the middleware behaves, never whether it persists:

  • Service does not store: the middleware loads providers and drives the function loop with a local sentinel conversation id.
  • Service stores: the middleware skips loading (the service owns history) and persists each service call while the real conversation id flows through. A warning is logged for any provider with load_messages=True whose 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_options now skips None overrides and strips remaining None values in a single pass, so an unset store is never forwarded to the client and the service decides its own default (STORES_BY_DEFAULT is only an internal behavior hint).
  • store and conversation_id are resolved once from a single combined view (effective_options) instead of probing both the agent-default and runtime dicts separately. The InMemoryHistoryProvider auto-injection and the per-service-call resolution now agree on conversation_id (an agent-level default is honored consistently).

Tests are added to test_agents.py as a scenario matrix (sync + streaming) that asserts the per-service-call persistence timing across storing/non-storing clients and store overrides.

Contribution Checklist

  • The code builds clean without any errors or warnings
  • The PR follows the Contribution Guidelines
  • All unit tests pass, and I have added new tests where possible
  • Is this a breaking change? If yes, add "[BREAKING]" prefix to the title of the PR.

Copilot AI review requested due to automatic review settings June 3, 2026 14:40
@moonbox3 moonbox3 added the python Usage: [Issues, PRs], Target: Python label Jun 3, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 PerServiceCallHistoryPersistingMiddleware when per-service-call persistence is required and a HistoryProvider is present; service-side storage now only changes how the middleware behaves (load+persist vs persist-only).
  • Updates _prepare_run_context to resolve store/conversation_id from a single merged options view and to treat None as “unset” (not forwarded).
  • Adds a scenario-matrix test suite validating persistence timing across storing/non-storing clients, streaming/non-streaming runs, and store overrides.

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.

Comment thread python/packages/core/agent_framework/_clients.py Outdated
@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Python Test Coverage

Python Test Coverage Report •
FileStmtsMissCoverMissing
packages/core/agent_framework
   _agents.py4185187%465, 477, 532, 1030, 1075, 1148–1152, 1248, 1276, 1313, 1335, 1355–1356, 1361, 1408, 1450, 1476, 1478, 1491, 1542, 1544, 1553–1558, 1563, 1565, 1571–1572, 1579, 1581–1582, 1590–1591, 1594–1596, 1606–1611, 1615, 1620, 1622
   _clients.py138794%325, 376, 539–542, 660
   _sessions.py3953391%105–107, 109–110, 127–128, 130–132, 209–210, 300, 561–565, 614, 620, 654, 725, 729, 739, 872, 888, 1021, 1035–1036, 1059, 1081, 1091, 1133
TOTAL38420439188% 

Python Unit Test Overview

Tests Skipped Failures Errors Time
7720 34 💤 0 ❌ 0 🔥 2m 1s ⏱️

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated Code Review

Reviewers: 4 | Confidence: 84% | Result: All clear

Reviewed: Correctness, Security Reliability, Test Coverage, Design Approach


Automated review by eavanvalkenburg's agents

Comment thread python/packages/core/agent_framework/_agents.py Outdated
Comment thread python/packages/core/agent_framework/_agents.py
Comment thread python/packages/core/agent_framework/_sessions.py
Comment thread python/packages/core/agent_framework/_agents.py
Comment thread python/packages/core/agent_framework/_agents.py
Comment thread python/packages/core/agent_framework/_agents.py
eavanvalkenburg and others added 3 commits June 8, 2026 11:02
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 <[email protected]>
…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 <[email protected]>
- Clarify that require_per_service_call_history_persistence is a no-op when no
  HistoryProvider is present (docstrings in _agents.py and _clients.py).
- Warn on every service call when the client stores history server-side but
  returns no conversation_id, so the (uncommon) loss of cross-turn resumability
  cannot fail silently.
- Add tests: storing client + existing conversation_id does not raise and the id
  propagates; two runs on the same session keep persisting with a stable
  service_session_id and no provider loading; storing-without-conversation-id
  warns per call.

Co-authored-by: Copilot <[email protected]>
@eavanvalkenburg eavanvalkenburg force-pushed the fix/5798-per-service-call-history-persistence branch from 8e09ec8 to 2827110 Compare June 8, 2026 09:02
@eavanvalkenburg eavanvalkenburg deleted the fix/5798-per-service-call-history-persistence branch June 30, 2026 09:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

python Usage: [Issues, PRs], Target: Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Python: OpenAI store=True can silently bypass external HistoryProvider persistence

4 participants