Skip to content

Python: [BREAKING] update context provider APIs, middleware, and per-service-call history persistence - #4992

Merged
eavanvalkenburg merged 9 commits into
microsoft:mainfrom
eavanvalkenburg:context_provider_update
Apr 1, 2026
Merged

Python: [BREAKING] update context provider APIs, middleware, and per-service-call history persistence#4992
eavanvalkenburg merged 9 commits into
microsoft:mainfrom
eavanvalkenburg:context_provider_update

Conversation

@eavanvalkenburg

@eavanvalkenburg eavanvalkenburg commented Mar 31, 2026

Copy link
Copy Markdown
Member

Motivation and Context

This PR bundles the three related Python context-provider updates that were planned and implemented together:

The history-persistence work follows the related .NET design work in #4762 and #4974, plus the follow-up rename/reframing in #4993, while adapting it to Python's agent/runtime shape.

Closes #4970.

Description

  • Rename the public provider base APIs to ContextProvider and HistoryProvider, while keeping BaseContextProvider and BaseHistoryProvider as deprecated compatibility aliases for now.
  • Allow context providers to add invocation-scoped ChatMiddleware and FunctionMiddleware through SessionContext, forward that middleware into downstream client/tool execution, and explicitly reject provider-added AgentMiddleware.
  • Add require_per_service_call_history_persistence to Agent, BaseChatClient.as_agent(...), and Foundry wrappers, and treat that rename from simulate_service_stored_history as a breaking public API change.
  • Implement per-model-call history persistence in the chat pipeline, and choose between local per-service-call persistence and real service-managed storage based on the effective store behavior.
  • Preserve the per-service-call persistence flag when handoff executors clone agents, so auto-handoff termination keeps the same local-history boundaries.
  • Extend core and orchestrations coverage for provider-added middleware, per-service-call history persistence, response-id suppression for locally continued runs, and handoff termination behavior.
  • Rename the Foundry sample to samples/02-agents/chat_client/require_per_service_call_history_persistence.py and update its README so it demonstrates the key termination edge: without per-service-call persistence the synthesized tool result is written to local history, while with per-service-call persistence it is not, matching service-side storage behavior.

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 March 31, 2026 13:00
@markwallace-microsoft markwallace-microsoft added documentation Usage: [Issues, PRs], Target: documentation in the code base and learn docs python Usage: [Issues, PRs], Target: Python labels Mar 31, 2026
@markwallace-microsoft

markwallace-microsoft commented Mar 31, 2026

Copy link
Copy Markdown
Contributor

Python Test Coverage

Python Test Coverage Report •
FileStmtsMissCoverMissing
packages/a2a/agent_framework_a2a
   _agent.py2101692%352, 357, 359, 475, 496, 517, 537, 551, 565, 577–578, 620–621, 650–652
packages/azure-ai-search/agent_framework_azure_ai_search
   _context_provider.py3571097%120–121, 539, 590–591, 714–715, 893–894, 941
packages/azure-cosmos/agent_framework_azure_cosmos
   _history_provider.py125596%162–164, 261, 268
packages/claude/agent_framework_claude
   _agent.py2792889%328–329, 333, 345, 353–355, 357–358, 388–390, 409, 413, 415, 419, 428, 474, 477, 498, 503–504, 571, 658, 684–687
packages/copilotstudio/agent_framework_copilotstudio
   _agent.py84297%174, 182
packages/core/agent_framework
   _agents.py4105187%460, 469, 524, 1016, 1068, 1142–1146, 1206, 1234, 1289, 1309–1310, 1315, 1362, 1404, 1426, 1428, 1441, 1447, 1492, 1494, 1503–1508, 1513, 1515, 1521–1522, 1529, 1531–1532, 1540–1541, 1544–1546, 1556–1561, 1565, 1570, 1572
   _clients.py138794%324, 376, 532–535, 650
   _compaction.py6346889%104, 150–162, 169–171, 177–178, 193–196, 203–205, 229, 243, 250, 265–266, 320, 327, 343, 393, 417, 447, 494, 500, 502, 521, 565–570, 582, 654, 656, 671, 715, 777, 899, 960, 962, 978, 985, 990, 1005, 1013, 1107, 1110, 1128, 1144, 1240, 1267, 1272
   _sessions.py2793089%88–90, 92–95, 112–113, 115–119, 196–197, 287, 548–552, 594, 597, 631, 680, 684, 694, 851, 867
   _skills.py4121596%176, 888, 890–892, 946, 985–986, 1059, 1064, 1113, 1118, 1270–1271, 1519
   _tools.py9488890%190–191, 364, 366, 379, 404–406, 414, 432, 446, 453, 460, 483, 485, 492, 500, 539, 583, 587, 619–621, 623, 629, 674–676, 678, 701, 727, 731, 769–771, 775, 797, 909–915, 951, 963, 965, 967, 970–973, 994, 998, 1002, 1016–1018, 1359, 1381, 1468–1474, 1603, 1607, 1653, 1714–1715, 1830, 1850, 1852, 1908, 1971, 2143–2144, 2164, 2220–2221, 2281, 2360–2361, 2428, 2433, 2440
   _types.py10888792%58, 67–68, 122, 127, 146, 148, 152, 156, 158, 160, 162, 180, 184, 210, 232, 237, 242, 246, 276, 686–687, 846–847, 1232, 1304, 1339, 1359, 1369, 1421, 1553–1555, 1745, 1836–1841, 1866, 2044, 2056, 2079, 2332, 2357, 2365, 2460, 2691, 2897, 2966, 2977, 2979–2983, 2985, 2988–2996, 3006, 3211–3213, 3216–3218, 3222, 3227, 3231, 3315–3317, 3346, 3423–3427, 3517
   observability.py7498488%377, 379–380, 383, 386, 389–390, 395–396, 402–403, 409–410, 417, 419–421, 424–426, 431–432, 438–439, 445–446, 453, 610–611, 739, 743–745, 747, 751–752, 756, 794, 796, 807–809, 811–813, 817, 825, 949–950, 1112, 1351–1352, 1450–1455, 1462–1465, 1469–1477, 1484, 1549–1553, 1606–1607, 1741, 1833, 2031, 2249, 2251
packages/core/agent_framework/_workflows
   _agent.py3597080%66, 74–80, 116–117, 207, 253, 255, 318, 320, 374–375, 381–382, 388, 390, 395, 455–456, 465, 472, 498, 531–533, 535, 537, 539, 544, 549, 596, 626, 643, 682–685, 691, 697, 701–702, 705–711, 715–716, 724, 785, 792, 798–799, 810, 842, 849, 870, 879, 883, 885–887, 894
packages/foundry/agent_framework_foundry
   _agent.py1383475%181–182, 186–188, 193–196, 291, 313–314, 326–328, 330–331, 333–339, 341–342, 344, 346, 350–351, 538–539, 542, 584
   _memory_provider.py920100% 
packages/github_copilot/agent_framework_github_copilot
   _agent.py248996%44–45, 51, 558–559, 574, 577, 644, 673
packages/mem0/agent_framework_mem0
   _context_provider.py77198%128
packages/orchestrations/agent_framework_orchestrations
   _handoff.py3845785%106–107, 109, 162–172, 174, 176, 178, 183, 277, 283, 315, 338, 397, 422, 479, 511, 569–570, 600, 608, 612–613, 651–653, 658–660, 778, 781, 794, 856, 861, 868, 878, 880, 899, 901, 983–984, 1016–1017, 1099, 1106, 1178–1179, 1181
packages/redis/agent_framework_redis
   _context_provider.py1785171%239, 242, 248–249, 251–252, 254–256, 258–260, 262–264, 266–277, 279–280, 282, 285–290, 318, 320, 324–325, 328–331, 352, 363, 408, 410–411, 415–416
   _history_provider.py59198%191
TOTAL26925319088% 

Python Unit Test Overview

Tests Skipped Failures Errors Time
5314 20 💤 0 ❌ 0 🔥 1m 22s ⏱️

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 updates the Python Agent Framework context-provider surface area by renaming the public provider base classes, enabling context providers to add invocation-scoped middleware, and adding an opt-in mode to simulate service-stored history semantics per model call (to better match service-managed storage behavior).

Changes:

  • Rename provider base APIs to ContextProvider / HistoryProvider, keeping BaseContextProvider / BaseHistoryProvider as deprecated aliases.
  • Allow providers to add invocation-scoped chat/function middleware via SessionContext, and forward that middleware into downstream execution (while rejecting provider-added agent middleware).
  • Add simulate_service_stored_history support and implement per-model-call history-provider simulation, including orchestration handoff clone preservation and expanded test coverage.

Reviewed changes

Copilot reviewed 40 out of 40 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
python/samples/05-end-to-end/hosted_agents/README.md Updates sample docs to reference ContextProvider.
python/samples/05-end-to-end/hosted_agents/agent_with_text_search_rag/main.py Switches sample provider inheritance/imports to ContextProvider.
python/samples/02-agents/conversations/custom_history_provider.py Switches sample history provider inheritance/imports to HistoryProvider.
python/samples/02-agents/context_providers/simple_context_provider.py Switches sample provider inheritance/imports to ContextProvider.
python/samples/02-agents/chat_client/simulate_service_stored_history.py Adds a new sample demonstrating simulated service-stored history behavior.
python/samples/02-agents/chat_client/README.md Documents the new history-simulation sample and prerequisites.
python/samples/01-get-started/04_memory.py Updates get-started memory sample to ContextProvider.
python/packages/redis/tests/test_providers.py Updates Redis provider tests to reference HistoryProvider naming.
python/packages/redis/agent_framework_redis/_history_provider.py Renames Redis history provider base class usage to HistoryProvider.
python/packages/redis/agent_framework_redis/_context_provider.py Renames Redis context provider base class usage to ContextProvider.
python/packages/orchestrations/tests/test_handoff.py Adds coverage for simulated history behavior preservation in handoff clones; updates provider naming.
python/packages/orchestrations/agent_framework_orchestrations/_handoff.py Preserves simulate_service_stored_history when cloning agents; cleans up middleware import.
python/packages/openai/agent_framework_openai/_assistant_provider.py Updates assistant provider API typing to ContextProvider.
python/packages/mem0/agent_framework_mem0/_context_provider.py Updates Mem0 provider to ContextProvider.
python/packages/github_copilot/agent_framework_github_copilot/_agent.py Updates agent wrapper typing to ContextProvider.
python/packages/foundry/agent_framework_foundry/_memory_provider.py Updates Foundry memory provider to ContextProvider.
python/packages/foundry/agent_framework_foundry/_agent.py Updates Foundry agent APIs to accept ContextProvider.
python/packages/core/tests/core/test_sessions.py Adds tests for provider-added middleware and deprecated alias behavior; updates naming.
python/packages/core/tests/core/test_middleware_with_agent.py Adds tests for provider-added middleware forwarding and rejection of provider-added agent middleware.
python/packages/core/tests/core/test_agents.py Adds tests for per-call history simulation + response-id behavior; updates naming.
python/packages/core/AGENTS.md Updates documentation to ContextProvider / HistoryProvider.
python/packages/core/agent_framework/observability.py Refactors tracing wrapper to share logic via _trace_agent_invocation while preserving overloads.
python/packages/core/agent_framework/_workflows/_agent.py Updates workflow agent typing/checks to ContextProvider / HistoryProvider.
python/packages/core/agent_framework/_tools.py Clears simulated conversation id sentinel before returning final responses.
python/packages/core/agent_framework/_skills.py Updates SkillsProvider base class to ContextProvider.
python/packages/core/agent_framework/_sessions.py Introduces ContextProvider/HistoryProvider, provider-added middleware support, and history-simulation chat middleware + deprecated aliases.
python/packages/core/agent_framework/_compaction.py Updates CompactionProvider base class to ContextProvider.
python/packages/core/agent_framework/_clients.py Adds simulate_service_stored_history option to BaseChatClient.as_agent(...).
python/packages/core/agent_framework/_agents.py Implements provider-added middleware forwarding and per-model-call history simulation wiring + response-id suppression behavior.
python/packages/core/agent_framework/init.py Re-exports ContextProvider and HistoryProvider (and keeps deprecated aliases).
python/packages/copilotstudio/agent_framework_copilotstudio/_agent.py Updates agent wrapper typing to ContextProvider.
python/packages/claude/agent_framework_claude/_agent.py Updates agent wrapper typing to ContextProvider.
python/packages/azure-cosmos/agent_framework_azure_cosmos/_history_provider.py Updates Cosmos history provider base class to HistoryProvider.
python/packages/azure-ai/agent_framework_azure_ai/_project_provider.py Updates Azure AI project provider APIs to ContextProvider.
python/packages/azure-ai/agent_framework_azure_ai/_client.py Updates Azure AI client as_agent typing to ContextProvider.
python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py Updates Azure AI chat client as_agent typing to ContextProvider.
python/packages/azure-ai/agent_framework_azure_ai/_agent_provider.py Updates Azure AI agent provider APIs to ContextProvider.
python/packages/azure-ai-search/agent_framework_azure_ai_search/_context_provider.py Updates Azure AI Search provider base class to ContextProvider.
python/packages/a2a/tests/test_a2a_agent.py Updates A2A tests to use ContextProvider.
python/packages/a2a/agent_framework_a2a/_agent.py Updates A2A agent history-provider type checks to HistoryProvider.

Comment thread python/packages/core/agent_framework/_agents.py Outdated
Comment thread python/samples/02-agents/chat_client/README.md Outdated
@eavanvalkenburg eavanvalkenburg changed the title Python: update context provider APIs, middleware, and history simulation [BREAKING] Python: update context provider APIs, middleware, and per-service-call history persistence Mar 31, 2026

@TaoChenOSU TaoChenOSU 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: 76%

✓ Correctness

✗ Security Reliability

This PR renames BaseContextProvider→ContextProvider and BaseHistoryProvider→HistoryProvider (with deprecated aliases), introduces per-service-call history persistence via a new PerServiceCalHistoryPersistingMiddleware, and allows context providers to inject chat/function middleware. The major security/reliability concern is a class name typo ('PerServiceCalHistoryPersistingMiddleware' instead of 'PerServiceCallHistoryPersistingMiddleware') that appears in both the class definition and public API surface. There is also a reliability issue in the streaming path of the middleware where with_result_hook is called without reassigning to context.result—while with_result_hook mutates in-place, the lambda closure captures service_call_context by reference in a way that may cause issues with concurrent streaming. Additionally, the deprecated BaseContextProvider/BaseHistoryProvider aliases use @deprecated which emits warnings on class definition (Python 3.13+) or instantiation (typing_extensions), but the init.py import of these names with type: ignore suppression could mask other issues.

✗ Test Coverage

This PR introduces the require_per_service_call_history_persistence feature with PerServiceCalHistoryPersistingMiddleware, renames BaseContextProvider/BaseHistoryProvider to ContextProvider/HistoryProvider (with deprecated aliases), adds SessionContext.extend_middleware()/get_middleware(), and refactors Agent.run() internals. Test coverage for the new feature is good at the integration level (non-streaming, streaming, service-stores-by-default bypass, response_id suppression), but several important units and edge cases lack tests: the PerServiceCalHistoryPersistingMiddleware class has no direct unit tests, the helper functions _split_service_call_messages, _response_contains_follow_up_request, is_local_history_conversation_id, and _clear_local_history_conversation_id have no direct tests, and the _resolve_per_service_call_history_providers method's error path (raising AgentInvalidRequestException when conversation_id is set) is exercised only indirectly via test_per_service_call_persistence_rejects_real_service_conversation_id which actually tests a different error (ChatClientInvalidResponseException from the middleware, not AgentInvalidRequestException from _resolve). There is also a typo in the class name (PerServiceCalHistoryPersistingMiddleware — missing second 'l' in 'Call').

✗ Design Approach

The PR successfully renames BaseContextProvider/BaseHistoryProvider to ContextProvider/HistoryProvider with proper deprecation shims, and the observability refactoring is clean. The main design concern is the require_per_service_call_history_persistence feature's sentinel mechanism: LOCAL_HISTORY_CONVERSATION_ID is injected into response.conversation_id by PerServiceCalHistoryPersistingMiddleware, then must be detected and cleared by _tools.py's function loop and filtered in three separate places in _agents.py. Overloading a semantic public field (conversation_id) as an inter-layer control signal creates a hidden cross-cutting contract that is fragile and hard to maintain — a dedicated out-of-band flag on ChatResponse (e.g. local_continuation: bool) would isolate the concern to the middleware layer without leaking into the function invocation loop. There is also a typo in the middleware class name ('Cal' vs 'Call') and a getattr-based coupling from BaseAgent._run_after_providers to a subclass attribute.

Flagged Issues

  • Class name typo: 'PerServiceCalHistoryPersistingMiddleware' should be 'PerServiceCallHistoryPersistingMiddleware' (missing 'l' in 'Call'). This typo propagates through the public API surface—class definition in _sessions.py, import in _agents.py, and init.py exports. Once released as stable API, the misspelling becomes a permanent breaking-change liability.
  • LOCAL_HISTORY_CONVERSATION_ID sentinel overloads response.conversation_id as an inter-layer control signal. The middleware writes it, _tools.py must clear it at two function-loop exit points, and _agents.py must filter it in _finalize_response, _post_hook, and _propagate_conversation_id. Any future code touching conversation_id must also know about this sentinel. A dedicated flag on ChatResponse (e.g. use_local_continuation: bool = False) would confine the concern to the middleware layer without bleeding into the function invocation loop.
  • PerServiceCalHistoryPersistingMiddleware has no unit tests—its process(), _prepare_service_call_context(), _persist_service_call_response(), _strip_local_conversation_id(), and _finalize_response() methods are only exercised indirectly through high-level Agent.run() integration tests, making it hard to verify edge-case correctness (e.g., streaming finalization path, ValueError branches for wrong result types, real conversation_id rejection).
  • The _resolve_per_service_call_history_providers error path that raises AgentInvalidRequestException when conversation_id is set while require_per_service_call_history_persistence is True has no test. The existing test_per_service_call_persistence_rejects_real_service_conversation_id tests a different code path (ChatClientInvalidResponseException from the middleware, not AgentInvalidRequestException from _resolve).

Suggestions

  • Consider adding a guard in _prepare_service_call_context to handle the case where self._session is None or has been invalidated, since the middleware stores a reference to the session at construction time but the session could theoretically be replaced between middleware instantiation and execution.
  • The _strip_local_conversation_id method creates a new dict from context.options on every call even when no mutation is needed. Consider checking before copying.
  • Add unit tests for the helper functions is_local_history_conversation_id, _response_contains_follow_up_request, _split_service_call_messages, and _clear_local_history_conversation_id—these are pure functions with simple contracts that are easy to test and important to the feature's correctness.
  • Add a test that verifies the deprecated BaseHistoryProvider alias emits DeprecationWarning on instantiation. The diff adds test_base_context_provider_warns_and_is_compatible for BaseContextProvider, but BaseHistoryProvider's warning is not tested (test_base_provider_aliases_preserve_subtyping only checks isubclass).
  • BaseAgent._run_after_providers uses getattr(self, 'require_per_service_call_history_persistence', False) to access an attribute defined only on Agent/ChatAgent. Either move the attribute to BaseAgent with a default of False, or extract the skip logic into a protected hook that subclasses can override.
  • _split_service_call_messages relies on the _attribution key in message.additional_properties as an implicit contract for separating context messages from input messages. This convention is load-bearing across modules and should at minimum be defined as a named constant.

Automated review by TaoChenOSU's agents

Comment thread python/packages/core/agent_framework/_sessions.py
Comment thread python/packages/core/agent_framework/_sessions.py
Comment thread python/packages/core/agent_framework/_agents.py
Comment thread python/packages/core/tests/core/test_sessions.py
Comment thread python/packages/core/tests/core/test_agents.py
Comment thread python/packages/core/agent_framework/_agents.py Outdated
Comment thread python/packages/core/agent_framework/_sessions.py
Comment thread python/packages/core/agent_framework/_tools.py Outdated
@eavanvalkenburg
eavanvalkenburg force-pushed the context_provider_update branch 2 times, most recently from e6c099e to 34b4468 Compare April 1, 2026 06:35
eavanvalkenburg and others added 9 commits April 1, 2026 12:41
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@eavanvalkenburg
eavanvalkenburg force-pushed the context_provider_update branch from c3e4a27 to e2fe01c Compare April 1, 2026 10:41
@eavanvalkenburg eavanvalkenburg changed the title [BREAKING] Python: update context provider APIs, middleware, and per-service-call history persistence Python: [BREAKING] update context provider APIs, middleware, and per-service-call history persistence Apr 1, 2026
@eavanvalkenburg
eavanvalkenburg added this pull request to the merge queue Apr 1, 2026
Merged via the queue into microsoft:main with commit b065a4c Apr 1, 2026
32 checks passed
@eavanvalkenburg
eavanvalkenburg deleted the context_provider_update 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

documentation Usage: [Issues, PRs], Target: documentation in the code base and learn docs python Usage: [Issues, PRs], Target: Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: allow context providers to add middleware

5 participants