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

* Rename provider base APIs

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Allow provider-added chat and function middleware

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Simulate service-stored history per model call

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix typing regressions in CI

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix response ID suppression review feedback

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Rename per-service-call history persistence APIs

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address context persistence review feedback

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Stabilize markdown sample docs

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Persist service continuation state per call

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Eduard van Valkenburg
2026-04-01 18:13:11 +02:00
committed by GitHub
Unverified
parent 38de991481
commit b065a4ce51
37 changed files with 1836 additions and 396 deletions
@@ -39,7 +39,7 @@ from dataclasses import dataclass
from typing import Any
from agent_framework import Agent, SupportsAgentRun
from agent_framework._middleware import FunctionInvocationContext, FunctionMiddleware
from agent_framework._middleware import FunctionInvocationContext, FunctionMiddleware, MiddlewareTermination
from agent_framework._sessions import AgentSession
from agent_framework._tools import FunctionTool, tool
from agent_framework._types import AgentResponse, Content, Message
@@ -138,8 +138,6 @@ class _AutoHandoffMiddleware(FunctionMiddleware):
await call_next()
return
from agent_framework._middleware import MiddlewareTermination
# Short-circuit execution and provide deterministic response payload for the tool call.
# Parse the result using the default parser to ensure in a form that can be passed directly to LLM APIs.
context.result = FunctionTool.parse_result({
@@ -375,6 +373,7 @@ class HandoffAgentExecutor(AgentExecutor):
description=agent.description,
context_providers=agent.context_providers,
middleware=agent.agent_middleware,
require_per_service_call_history_persistence=agent.require_per_service_call_history_persistence,
default_options=cloned_options, # type: ignore[assignment]
)
@@ -8,10 +8,11 @@ from unittest.mock import AsyncMock, MagicMock
import pytest
from agent_framework import (
Agent,
BaseContextProvider,
ChatResponse,
ChatResponseUpdate,
Content,
ContextProvider,
InMemoryHistoryProvider,
Message,
ResponseStream,
WorkflowEvent,
@@ -695,6 +696,48 @@ def test_handoff_clone_disables_provider_side_storage() -> None:
assert executor._agent.default_options.get("store") is False
async def test_handoff_clone_preserves_per_service_call_history_persistence() -> None:
"""Handoff clones should keep per-service-call history persistence active for auto-handoff termination."""
triage_history = InMemoryHistoryProvider()
triage = Agent(
id="triage",
name="triage",
client=MockChatClient(name="triage", handoff_to="specialist"),
context_providers=[triage_history],
require_per_service_call_history_persistence=True,
)
specialist = Agent(
id="specialist",
name="specialist",
client=MockChatClient(name="specialist"),
default_options={"tool_choice": "none"},
)
workflow = (
HandoffBuilder(participants=[triage, specialist], termination_condition=lambda _: False)
.with_start_agent(triage)
.add_handoff(triage, [specialist])
.add_handoff(specialist, [triage])
.build()
)
await _drain(workflow.run("start", stream=True))
executor = workflow.executors[resolve_agent_id(triage)]
assert isinstance(executor, HandoffAgentExecutor)
assert executor._agent.require_per_service_call_history_persistence is True
provider_state = executor._session.state[triage_history.source_id]
stored_messages = await triage_history.get_messages(
executor._session.session_id,
state=provider_state,
)
assert [message.role for message in stored_messages] == ["user", "assistant"]
assert any(content.type == "function_call" for content in stored_messages[-1].contents)
assert all(message.role != "tool" for message in stored_messages)
async def test_handoff_clears_stale_service_session_id_before_run() -> None:
"""Stale service session IDs must be dropped before each handoff agent turn."""
triage = MockHandoffAgent(name="triage", handoff_to="specialist")
@@ -997,7 +1040,7 @@ async def test_context_provider_preserved_during_handoff():
# Track whether context provider methods were called
provider_calls: list[str] = []
class TestContextProvider(BaseContextProvider):
class TestContextProvider(ContextProvider):
"""A test context provider that tracks its invocations."""
def __init__(self) -> None: