Python: [BREAKING] PR2 — Wire context provider pipeline, remove old types, update all consumers (#3850)

* PR2: Wire context provider pipeline and update all internal consumers

- Replace AgentThread with AgentSession across all packages
- Replace ContextProvider with BaseContextProvider across all packages
- Replace context_provider param with context_providers (Sequence)
- Replace thread= with session= in run() signatures
- Replace get_new_thread() with create_session()
- Add get_session(service_session_id) to agent interface
- DurableAgentThread -> DurableAgentSession
- Remove _notify_thread_of_new_messages from WorkflowAgent
- Wire before_run/after_run context provider pipeline in RawAgent
- Auto-inject InMemoryHistoryProvider when no providers configured

* fix: update all tests for context provider pipeline, fix lazy-loaders, remove old test files

* refactor: update all sample files for context provider pipeline (AgentThread→AgentSession, ContextProvider→BaseContextProvider)

* fix: update remaining ag-ui references (client docstring, getting_started sample)

* fix: make get_session service_session_id keyword-only to avoid confusion with session_id

* refactor: rename _RunContext.thread_messages to session_messages

* refactor: remove _threads.py, _memory.py, and old provider files; migrate devui to use plain message lists

* rename: remove _new_ prefix from test files

* refactor: rewrite SlidingWindowChatMessageStore as SlidingWindowHistoryProvider(InMemoryHistoryProvider)

* fix: read full history from session state directly instead of reaching into provider internals

* fix: update stale .pyi stubs, sample imports, and README references for new provider types

* fix: remove stale message_store, _notify_thread_of_new_messages, and session_id.key references in samples

* refactor: merge context_providers and sessions sample folders into sessions, remove aggregate_context_provider

* refactor: UserInfoMemory stores state in session.state instead of instance attributes

* feat: add Pydantic BaseModel support to session state serialization

Pydantic models stored in session.state are now automatically serialized
via model_dump() and restored via model_validate() during to_dict()/from_dict()
round-trips. Models are auto-registered on first serialization; use
register_state_type() for cold-start deserialization.

Also export register_state_type as a public API.

* fix mem0

* Update sample README links and descriptions for session terminology

- Replace 'thread' with 'session' in sample descriptions across all READMEs
- Update file links for renamed samples (mem0_sessions, redis_sessions, etc.)
- Fix Threads section → Sessions section in main samples/README.md
- Update tools, middleware, workflows, durabletask, azure_functions READMEs
- Update architecture diagrams in concepts/tools/README.md
- Update migration guides (autogen, semantic-kernel)

* Fix broken Redis README link to renamed sample

* Fix Mem0 OSS client search: pass scoping params as direct kwargs

AsyncMemory (OSS) expects user_id/agent_id/run_id as direct kwargs,
while AsyncMemoryClient (Platform) expects them in a filters dict.
Adds tests for both client types.

Port of fix from #3844 to new Mem0ContextProvider.

* Fix rebase issues: restore missing _conversation_state.py and checkpoint decode logic

- Add back _conversation_state.py (encode/decode_chat_messages) lost in rebase
- Fix on_checkpoint_restore to decode cache/conversation with decode_chat_messages
- Fix on_checkpoint_restore to use decode_checkpoint_value for pending requests
- Add tests/workflow/__init__.py for relative import support
- Fix test_agent_executor checkpoint selection (checkpoints[1] not superstep)

* Add STORES_BY_DEFAULT ClassVar to skip redundant InMemoryHistoryProvider injection

Chat clients that store history server-side by default (OpenAI Responses API,
Azure AI Agent) now declare STORES_BY_DEFAULT = True. The agent checks this
during auto-injection and skips InMemoryHistoryProvider unless the user
explicitly sets store=False.

* Fix broken markdown links in azure_ai and redis READMEs

* Fix getting-started samples to use session API instead of removed thread/ContextProvider API

* updates to workflow as agent

* fix group chat import

* Rename Thread→Session throughout, fix service_session_id propagation, remove stale AGUIThread

- Fix: Propagate conversation_id from ChatResponse back to session.service_session_id
  in both streaming and non-streaming paths in _agents.py
- Rename AgentThreadException → AgentSessionException
- Remove stale AGUIThread from ag_ui lazy-loader
- Rename use_service_thread → use_service_session in ag-ui package
- Rename test functions from *_thread_* to *_session_*
- Rename sample files from *_thread* to *_session*
- Update docstrings and comments: thread → session
- Update _mcp.py kwargs filter: add 'session' alongside 'thread'
- Fix ContinuationToken docstring example: thread=thread → session=session
- Fix _clients.py docstring: 'Agent threads' → 'Agent sessions'

* Fix broken markdown links after thread→session file renames

* fix azure ai test
This commit is contained in:
Eduard van Valkenburg
2026-02-12 22:00:32 +01:00
committed by GitHub
Unverified
parent 0c67dbbce5
commit 1e350ea22f
312 changed files with 6669 additions and 11423 deletions
@@ -45,7 +45,7 @@ from ._durable_agent_state import (
)
from ._entities import AgentEntity, AgentEntityStateProviderMixin
from ._executors import DurableAgentExecutor
from ._models import AgentSessionId, DurableAgentThread, RunRequest
from ._models import AgentSessionId, DurableAgentSession, RunRequest
from ._orchestration_context import DurableAIAgentOrchestrationContext
from ._response_utils import ensure_response_format, load_agent_response
from ._shim import DurableAIAgent
@@ -79,6 +79,7 @@ __all__ = [
"DurableAIAgentOrchestrationContext",
"DurableAIAgentWorker",
"DurableAgentExecutor",
"DurableAgentSession",
"DurableAgentState",
"DurableAgentStateContent",
"DurableAgentStateData",
@@ -99,7 +100,6 @@ __all__ = [
"DurableAgentStateUriContent",
"DurableAgentStateUsage",
"DurableAgentStateUsageContent",
"DurableAgentThread",
"DurableStateFields",
"RunRequest",
"__version__",
@@ -16,7 +16,7 @@ from abc import ABC, abstractmethod
from datetime import datetime, timezone
from typing import Any, Generic, TypeVar
from agent_framework import AgentResponse, AgentThread, Content, Message, get_logger
from agent_framework import AgentResponse, AgentSession, Content, Message, get_logger
from durabletask.client import TaskHubGrpcClient
from durabletask.entities import EntityInstanceId
from durabletask.task import CompletableTask, CompositeTask, OrchestrationContext, Task
@@ -24,7 +24,7 @@ from pydantic import BaseModel
from ._constants import DEFAULT_MAX_POLL_RETRIES, DEFAULT_POLL_INTERVAL_SECONDS
from ._durable_agent_state import DurableAgentState
from ._models import AgentSessionId, DurableAgentThread, RunRequest
from ._models import AgentSessionId, DurableAgentSession, RunRequest
from ._response_utils import ensure_response_format, load_agent_response
logger = get_logger("agent_framework.durabletask.executors")
@@ -114,7 +114,7 @@ class DurableAgentExecutor(ABC, Generic[TaskT]):
self,
agent_name: str,
run_request: RunRequest,
thread: AgentThread | None = None,
session: AgentSession | None = None,
) -> TaskT:
"""Execute the durable agent.
@@ -123,20 +123,20 @@ class DurableAgentExecutor(ABC, Generic[TaskT]):
"""
raise NotImplementedError
def get_new_thread(self, agent_name: str, **kwargs: Any) -> DurableAgentThread:
"""Create a new DurableAgentThread with random session ID."""
def get_new_session(self, agent_name: str, **kwargs: Any) -> DurableAgentSession:
"""Create a new DurableAgentSession with random session ID."""
session_id = self._create_session_id(agent_name)
return DurableAgentThread.from_session_id(session_id, **kwargs)
return DurableAgentSession.from_session_id(session_id, **kwargs)
def _create_session_id(
self,
agent_name: str,
thread: AgentThread | None = None,
session: AgentSession | None = None,
) -> AgentSessionId:
"""Create the AgentSessionId for the execution."""
if isinstance(thread, DurableAgentThread) and thread.session_id is not None:
return thread.session_id
# Create new session ID - either no thread provided or it's a regular AgentThread
if isinstance(session, DurableAgentSession) and session.durable_session_id is not None:
return session.durable_session_id
# Create new session ID - either no session provided or it's a regular AgentSession
key = self.generate_unique_id()
return AgentSessionId(name=agent_name, key=key)
@@ -217,7 +217,7 @@ class ClientAgentExecutor(DurableAgentExecutor[AgentResponse]):
self,
agent_name: str,
run_request: RunRequest,
thread: AgentThread | None = None,
session: AgentSession | None = None,
) -> AgentResponse:
"""Execute the agent via the durabletask client.
@@ -231,14 +231,14 @@ class ClientAgentExecutor(DurableAgentExecutor[AgentResponse]):
Args:
agent_name: Name of the agent to execute
run_request: The run request containing message and optional response format
thread: Optional conversation thread (creates new if not provided)
session: Optional conversation session (creates new if not provided)
Returns:
AgentResponse: The agent's response after execution completes, or an immediate
acknowledgement if wait_for_response is False
"""
# Signal the entity with the request
entity_id = self._signal_agent_entity(agent_name, run_request, thread)
entity_id = self._signal_agent_entity(agent_name, run_request, session)
# If fire-and-forget mode, return immediately without polling
if not run_request.wait_for_response:
@@ -258,20 +258,20 @@ class ClientAgentExecutor(DurableAgentExecutor[AgentResponse]):
self,
agent_name: str,
run_request: RunRequest,
thread: AgentThread | None,
session: AgentSession | None,
) -> EntityInstanceId:
"""Signal the agent entity with a run request.
Args:
agent_name: Name of the agent to execute
run_request: The run request containing message and optional response format
thread: Optional conversation thread
session: Optional conversation session
Returns:
entity_id
"""
# Get or create session ID
session_id = self._create_session_id(agent_name, thread)
session_id = self._create_session_id(agent_name, session)
# Create the entity ID
entity_id = EntityInstanceId(
@@ -460,7 +460,7 @@ class OrchestrationAgentExecutor(DurableAgentExecutor[DurableAgentTask]):
self,
agent_name: str,
run_request: RunRequest,
thread: AgentThread | None = None,
session: AgentSession | None = None,
) -> DurableAgentTask:
"""Execute the agent via orchestration context.
@@ -470,13 +470,13 @@ class OrchestrationAgentExecutor(DurableAgentExecutor[DurableAgentTask]):
Args:
agent_name: Name of the agent to execute
run_request: The run request containing message and optional response format
thread: Optional conversation thread (creates new if not provided)
session: Optional conversation session (creates new if not provided)
Returns:
DurableAgentTask: A task wrapping the entity call that yields AgentResponse
"""
# Resolve session
session_id = self._create_session_id(agent_name, thread)
session_id = self._create_session_id(agent_name, session)
# Create the entity ID
entity_id = EntityInstanceId(
@@ -10,13 +10,12 @@ from __future__ import annotations
import inspect
import json
import uuid
from collections.abc import MutableMapping
from dataclasses import dataclass, field
from datetime import datetime, timezone
from importlib import import_module
from typing import TYPE_CHECKING, Any, cast
from agent_framework import AgentThread
from agent_framework import AgentSession
from ._constants import REQUEST_RESPONSE_FORMAT_TEXT
@@ -274,65 +273,57 @@ class AgentSessionId:
raise ValueError(f"Invalid agent session ID format: {session_id_string}")
class DurableAgentThread(AgentThread):
"""Durable agent thread that tracks the owning :class:`AgentSessionId`."""
class DurableAgentSession(AgentSession):
"""Durable agent session that tracks the owning :class:`AgentSessionId`."""
_SERIALIZED_SESSION_ID_KEY = "durable_session_id"
def __init__(
self,
*,
session_id: AgentSessionId | None = None,
durable_session_id: AgentSessionId | None = None,
session_id: str | None = None,
service_session_id: str | None = None,
**kwargs: Any,
) -> None:
super().__init__(**kwargs)
self._session_id: AgentSessionId | None = session_id
super().__init__(session_id=session_id, service_session_id=service_session_id, **kwargs)
self._session_id_value: AgentSessionId | None = durable_session_id
@property
def session_id(self) -> AgentSessionId | None:
return self._session_id
def durable_session_id(self) -> AgentSessionId | None:
return self._session_id_value
@session_id.setter
def session_id(self, value: AgentSessionId | None) -> None:
self._session_id = value
@durable_session_id.setter
def durable_session_id(self, value: AgentSessionId | None) -> None:
self._session_id_value = value
@classmethod
def from_session_id(
cls,
session_id: AgentSessionId,
**kwargs: Any,
) -> DurableAgentThread:
return cls(session_id=session_id, **kwargs)
) -> DurableAgentSession:
return cls(durable_session_id=session_id, **kwargs)
async def serialize(self, **kwargs: Any) -> dict[str, Any]:
state = await super().serialize(**kwargs)
if self._session_id is not None:
state[self._SERIALIZED_SESSION_ID_KEY] = str(self._session_id)
def to_dict(self) -> dict[str, Any]:
state = super().to_dict()
if self._session_id_value is not None:
state[self._SERIALIZED_SESSION_ID_KEY] = str(self._session_id_value)
return state
@classmethod
async def deserialize(
cls,
serialized_thread_state: MutableMapping[str, Any],
*,
message_store: Any = None,
**kwargs: Any,
) -> DurableAgentThread:
state_payload = dict(serialized_thread_state)
def from_dict(cls, data: dict[str, Any]) -> DurableAgentSession:
state_payload = dict(data)
session_id_value = state_payload.pop(cls._SERIALIZED_SESSION_ID_KEY, None)
thread = await super().deserialize(
state_payload,
message_store=message_store,
**kwargs,
session = super().from_dict(state_payload)
# We need to create a DurableAgentSession from the base AgentSession
durable_session = cls(
session_id=session.session_id,
service_session_id=session.service_session_id,
)
if not isinstance(thread, DurableAgentThread):
raise TypeError("Deserialized thread is not a DurableAgentThread instance")
if session_id_value is None:
return thread
if not isinstance(session_id_value, str):
raise ValueError("durable_session_id must be a string when present in serialized state")
thread.session_id = AgentSessionId.parse(session_id_value)
return thread
durable_session.state.update(session.state)
if session_id_value is not None:
if not isinstance(session_id_value, str):
raise ValueError("durable_session_id must be a string when present in serialized state")
durable_session._session_id_value = AgentSessionId.parse(session_id_value)
return durable_session
@@ -12,10 +12,10 @@ from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Any, Generic, Literal, TypeVar
from agent_framework import AgentThread, Message, SupportsAgentRun
from agent_framework import AgentSession, Message, SupportsAgentRun
from ._executors import DurableAgentExecutor
from ._models import DurableAgentThread
from ._models import DurableAgentSession
# TypeVar for the task type returned by executors
# Covariant because TaskT only appears in return positions (output)
@@ -89,7 +89,7 @@ class DurableAIAgent(SupportsAgentRun, Generic[TaskT]):
messages: str | Message | list[str] | list[Message] | None = None,
*,
stream: Literal[False] = False,
thread: AgentThread | None = None,
session: AgentSession | None = None,
options: dict[str, Any] | None = None,
) -> TaskT:
"""Execute the agent via the injected provider.
@@ -98,7 +98,7 @@ class DurableAIAgent(SupportsAgentRun, Generic[TaskT]):
messages: The message(s) to send to the agent
stream: Whether to use streaming for the response (must be False)
DurableAgents do not support streaming mode.
thread: Optional agent thread for conversation context
session: Optional agent session for conversation context
options: Optional options dictionary. Supported keys include
``response_format``, ``enable_tool_calls``, and ``wait_for_response``.
Additional keys are forwarded to the agent execution.
@@ -129,12 +129,19 @@ class DurableAIAgent(SupportsAgentRun, Generic[TaskT]):
return self._executor.run_durable_agent(
agent_name=self.name,
run_request=run_request,
thread=thread,
session=session,
)
def get_new_thread(self, **kwargs: Any) -> DurableAgentThread:
"""Create a new agent thread via the provider."""
return self._executor.get_new_thread(self.name, **kwargs)
def create_session(self, **kwargs: Any) -> DurableAgentSession:
"""Create a new agent session via the provider."""
return self._executor.get_new_session(self.name, **kwargs)
def get_session(self, **kwargs: Any) -> AgentSession:
"""Retrieve an existing session via the provider.
For durable agents, sessions do not use `service_session_id` so this is not used.
"""
return self._executor.get_new_session(self.name, **kwargs)
def _normalize_messages(self, messages: str | Message | list[str] | list[Message] | None) -> str:
"""Convert supported message inputs to a single string.