Skip to content

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

Merged
eavanvalkenburg merged 9 commits intomicrosoft: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 intomicrosoft:mainfrom
eavanvalkenburg:context_provider_update

Conversation

@eavanvalkenburg
Copy link
Copy Markdown
Member

@eavanvalkenburg eavanvalkenburg commented Mar 31, 2026

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 Improvements or additions to documentation python labels Mar 31, 2026
@markwallace-microsoft
Copy link
Copy Markdown
Contributor

markwallace-microsoft commented Mar 31, 2026

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 ⏱️

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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.

@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
Copy link
Copy Markdown
Contributor

@TaoChenOSU TaoChenOSU left a comment

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

@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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: allow context providers to add middleware

5 participants