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
@@ -14,7 +14,7 @@ This behavior is the same for all chat client types.
"""
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/02-agents/tools/function_tool_with_approval.py and samples/02-agents/tools/function_tool_with_approval_and_threads.py.
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/02-agents/tools/function_tool_with_approval.py and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
@tool(approval_mode="never_require")
def add(
x: Annotated[int, "First number"],
@@ -14,7 +14,7 @@ The LLM decides whether to retry the call or to respond with something else, bas
"""
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/02-agents/tools/function_tool_with_approval.py and samples/02-agents/tools/function_tool_with_approval_and_threads.py.
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/02-agents/tools/function_tool_with_approval.py and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
@tool(approval_mode="never_require")
def greet(name: Annotated[str, "Name to greet"]) -> str:
"""Greet someone."""
@@ -44,29 +44,28 @@ async def main():
instructions="Use the provided tools.",
tools=[greet, safe_divide],
)
thread = agent.get_new_thread()
session = agent.create_session()
print("=" * 60)
print("Step 1: Call divide(10, 0) - tool raises exception")
response = await agent.run("Divide 10 by 0", thread=thread)
response = await agent.run("Divide 10 by 0", session=session)
print(f"Response: {response.text}")
print("=" * 60)
print("Step 2: Call greet('Bob') - conversation can keep going.")
response = await agent.run("Greet Bob", thread=thread)
response = await agent.run("Greet Bob", session=session)
print(f"Response: {response.text}")
print("=" * 60)
print("Replay the conversation:")
assert thread.message_store
assert thread.message_store.list_messages
for idx, msg in enumerate(await thread.message_store.list_messages()):
if msg.text:
print(f"{idx + 1} {msg.author_name or msg.role}: {msg.text} ")
for content in msg.contents:
if content.type == "function_call":
print(
f"{idx + 1} {msg.author_name}: calling function: {content.name} with arguments: {content.arguments}"
)
if content.type == "function_result":
print(f"{idx + 1} {msg.role}: {content.result if content.result else content.exception}")
# TODO: Use history providers to replay the conversation
# print("Replay the conversation:")
# for idx, msg in enumerate(messages):
# if msg.text:
# print(f"{idx + 1} {msg.author_name or msg.role}: {msg.text} ")
# for content in msg.contents:
# if content.type == "function_call":
# print(
# f"{idx + 1} {msg.author_name}: calling function: {content.name} with arguments: {content.arguments}"
# )
# if content.type == "function_result":
# print(f"{idx + 1} {msg.role}: {content.result if content.result else content.exception}")
"""
@@ -20,7 +20,7 @@ It shows how to handle function call approvals without using threads.
conditions = ["sunny", "cloudy", "raining", "snowing", "clear"]
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/02-agents/tools/function_tool_with_approval.py and samples/02-agents/tools/function_tool_with_approval_and_threads.py.
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/02-agents/tools/function_tool_with_approval.py and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
@tool(approval_mode="never_require")
def get_weather(location: Annotated[str, "The city and state, e.g. San Francisco, CA"]) -> str:
"""Get the current weather for a given location."""
@@ -7,11 +7,11 @@ from agent_framework import Agent, Message, tool
from agent_framework.azure import AzureOpenAIChatClient
"""
Tool Approvals with Threads
Tool Approvals with Sessions
This sample demonstrates using tool approvals with threads.
With threads, you don't need to manually pass previous messages -
the thread stores and retrieves them automatically.
This sample demonstrates using tool approvals with sessions.
With sessions, you don't need to manually pass previous messages -
the session stores and retrieves them automatically.
"""
@@ -25,8 +25,8 @@ def add_to_calendar(
async def approval_example() -> None:
"""Example showing approval with threads."""
print("=== Tool Approval with Thread ===\n")
"""Example showing approval with sessions."""
print("=== Tool Approval with Session ===\n")
agent = Agent(
client=AzureOpenAIChatClient(),
@@ -35,12 +35,12 @@ async def approval_example() -> None:
tools=[add_to_calendar],
)
thread = agent.get_new_thread()
session = agent.create_session()
# Step 1: Agent requests to call the tool
query = "Add a dentist appointment on March 15th"
print(f"User: {query}")
result = await agent.run(query, thread=thread)
result = await agent.run(query, session=session)
# Check for approval requests
if result.user_input_requests:
@@ -55,14 +55,14 @@ async def approval_example() -> None:
# Step 2: Send approval response
approval_response = request.to_function_approval_response(approved=approved)
result = await agent.run(Message("user", [approval_response]), thread=thread)
result = await agent.run(Message("user", [approval_response]), session=session)
print(f"Agent: {result}\n")
async def rejection_example() -> None:
"""Example showing rejection with threads."""
print("=== Tool Rejection with Thread ===\n")
"""Example showing rejection with sessions."""
print("=== Tool Rejection with Session ===\n")
agent = Agent(
client=AzureOpenAIChatClient(),
@@ -71,11 +71,11 @@ async def rejection_example() -> None:
tools=[add_to_calendar],
)
thread = agent.get_new_thread()
session = agent.create_session()
query = "Add a team meeting on December 20th"
print(f"User: {query}")
result = await agent.run(query, thread=thread)
result = await agent.run(query, session=session)
if result.user_input_requests:
for request in result.user_input_requests:
@@ -88,7 +88,7 @@ async def rejection_example() -> None:
# Send rejection response
rejection_response = request.to_function_approval_response(approved=False)
result = await agent.run(Message("user", [rejection_response]), thread=thread)
result = await agent.run(Message("user", [rejection_response]), session=session)
print(f"Agent: {result}\n")
@@ -20,7 +20,7 @@ or provide.
# Define the function tool with **kwargs to accept injected arguments
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/02-agents/tools/function_tool_with_approval.py and samples/02-agents/tools/function_tool_with_approval_and_threads.py.
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/02-agents/tools/function_tool_with_approval.py and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
@tool(approval_mode="never_require")
def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
@@ -36,31 +36,30 @@ async def main():
instructions="Use the provided tools.",
tools=[safe_divide],
)
thread = agent.get_new_thread()
session = agent.create_session()
print("=" * 60)
print("Step 1: Call divide(10, 0) - tool raises exception")
response = await agent.run("Divide 10 by 0", thread=thread)
response = await agent.run("Divide 10 by 0", session=session)
print(f"Response: {response.text}")
print("=" * 60)
print("Step 2: Call divide(100, 0) - will refuse to execute due to max_invocation_exceptions")
response = await agent.run("Divide 100 by 0", thread=thread)
response = await agent.run("Divide 100 by 0", session=session)
print(f"Response: {response.text}")
print("=" * 60)
print(f"Number of tool calls attempted: {safe_divide.invocation_count}")
print(f"Number of tool calls failed: {safe_divide.invocation_exception_count}")
print("Replay the conversation:")
assert thread.message_store
assert thread.message_store.list_messages
for idx, msg in enumerate(await thread.message_store.list_messages()):
if msg.text:
print(f"{idx + 1} {msg.author_name or msg.role}: {msg.text} ")
for content in msg.contents:
if content.type == "function_call":
print(
f"{idx + 1} {msg.author_name}: calling function: {content.name} with arguments: {content.arguments}"
)
if content.type == "function_result":
print(f"{idx + 1} {msg.role}: {content.result if content.result else content.exception}")
# TODO: Use history providers to replay the conversation
# print("Replay the conversation:")
# for idx, msg in enumerate(messages):
# if msg.text:
# print(f"{idx + 1} {msg.author_name or msg.role}: {msg.text} ")
# for content in msg.contents:
# if content.type == "function_call":
# print(
# f"{idx + 1} {msg.author_name}: calling function: {content.name} with arguments: {content.arguments}"
# )
# if content.type == "function_result":
# print(f"{idx + 1} {msg.role}: {content.result if content.result else content.exception}")
"""
@@ -25,31 +25,30 @@ async def main():
instructions="Use the provided tools.",
tools=[unicorn_function],
)
thread = agent.get_new_thread()
session = agent.create_session()
print("=" * 60)
print("Step 1: Call unicorn_function")
response = await agent.run("Call 5 unicorns!", thread=thread)
response = await agent.run("Call 5 unicorns!", session=session)
print(f"Response: {response.text}")
print("=" * 60)
print("Step 2: Call unicorn_function again - will refuse to execute due to max_invocations")
response = await agent.run("Call 10 unicorns and use the function to do it.", thread=thread)
response = await agent.run("Call 10 unicorns and use the function to do it.", session=session)
print(f"Response: {response.text}")
print("=" * 60)
print(f"Number of tool calls attempted: {unicorn_function.invocation_count}")
print(f"Number of tool calls failed: {unicorn_function.invocation_exception_count}")
print("Replay the conversation:")
assert thread.message_store
assert thread.message_store.list_messages
for idx, msg in enumerate(await thread.message_store.list_messages()):
if msg.text:
print(f"{idx + 1} {msg.author_name or msg.role}: {msg.text} ")
for content in msg.contents:
if content.type == "function_call":
print(
f"{idx + 1} {msg.author_name}: calling function: {content.name} with arguments: {content.arguments}"
)
if content.type == "function_result":
print(f"{idx + 1} {msg.role}: {content.result if content.result else content.exception}")
# TODO: Use history providers to replay the conversation
# print("Replay the conversation:")
# for idx, msg in enumerate(messages):
# if msg.text:
# print(f"{idx + 1} {msg.author_name or msg.role}: {msg.text} ")
# for content in msg.contents:
# if content.type == "function_call":
# print(
# f"{idx + 1} {msg.author_name}: calling function: {content.name} with arguments: {content.arguments}"
# )
# if content.type == "function_result":
# print(f"{idx + 1} {msg.role}: {content.result if content.result else content.exception}")
"""
@@ -0,0 +1,52 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from typing import Annotated, Any
from agent_framework import AgentSession, tool
from agent_framework.openai import OpenAIResponsesClient
from pydantic import Field
"""
AI Function with Session Injection Example
This example demonstrates the behavior when passing 'session' to agent.run()
and accessing that session in AI function.
"""
# Define the function tool with **kwargs
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/02-agents/tools/function_tool_with_approval.py and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
@tool(approval_mode="never_require")
async def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
**kwargs: Any,
) -> str:
"""Get the weather for a given location."""
# Get session object from kwargs
session = kwargs.get("session")
if session and isinstance(session, AgentSession) and session.service_session_id:
print(f"Session ID: {session.service_session_id}.")
return f"The weather in {location} is cloudy."
async def main() -> None:
agent = OpenAIResponsesClient().as_agent(
name="WeatherAgent",
instructions="You are a helpful weather assistant.",
tools=[get_weather],
options={"store": True},
)
# Create a session
session = agent.create_session()
# Run the agent with the session
print(f"Agent: {await agent.run('What is the weather in London?', session=session)}")
print(f"Agent: {await agent.run('What is the weather in Amsterdam?', session=session)}")
print(f"Agent: {await agent.run('What cities did I ask about?', session=session)}")
if __name__ == "__main__":
asyncio.run(main())
@@ -1,53 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from typing import Annotated, Any
from agent_framework import AgentThread, tool
from agent_framework.openai import OpenAIChatClient
from pydantic import Field
"""
AI Function with Thread Injection Example
This example demonstrates the behavior when passing 'thread' to agent.run()
and accessing that thread in AI function.
"""
# Define the function tool with **kwargs
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/02-agents/tools/function_tool_with_approval.py and samples/02-agents/tools/function_tool_with_approval_and_threads.py.
@tool(approval_mode="never_require")
async def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
**kwargs: Any,
) -> str:
"""Get the weather for a given location."""
# Get thread object from kwargs
thread = kwargs.get("thread")
if thread and isinstance(thread, AgentThread):
if thread.message_store:
messages = await thread.message_store.list_messages()
print(f"Thread contains {len(messages)} messages.")
elif thread.service_thread_id:
print(f"Thread ID: {thread.service_thread_id}.")
return f"The weather in {location} is cloudy."
async def main() -> None:
agent = OpenAIChatClient().as_agent(
name="WeatherAgent", instructions="You are a helpful weather assistant.", tools=[get_weather]
)
# Create a thread
thread = agent.get_new_thread()
# Run the agent with the thread
print(f"Agent: {await agent.run('What is the weather in London?', thread=thread)}")
print(f"Agent: {await agent.run('What is the weather in Amsterdam?', thread=thread)}")
print(f"Agent: {await agent.run('What cities did I ask about?', thread=thread)}")
if __name__ == "__main__":
asyncio.run(main())