Python: [BREAKING] Renamed AgentProtocol to SupportsAgentRun (#3717)

* Renamed AgentProtocol to AgentLike

* Resolved comments

* Renamed AgentLike to SupportsAgentRun

* Resolved comments
This commit is contained in:
Dmytro Struk
2026-02-06 09:53:21 -08:00
committed by GitHub
Unverified
parent ac17adb595
commit 15256bb616
55 changed files with 354 additions and 354 deletions
+1 -1
View File
@@ -25,7 +25,7 @@ agent_framework/
### Agents (`_agents.py`)
- **`AgentProtocol`** - Protocol defining the agent interface
- **`SupportsAgentRun`** - Protocol defining the agent interface
- **`BaseAgent`** - Abstract base class for agents
- **`ChatAgent`** - Main agent class wrapping a chat client with tools, instructions, and middleware
+10 -10
View File
@@ -163,14 +163,14 @@ class _RunContext(TypedDict):
finalize_kwargs: dict[str, Any]
__all__ = ["AgentProtocol", "BareAgent", "BaseAgent", "ChatAgent", "RawChatAgent"]
__all__ = ["BareAgent", "BaseAgent", "ChatAgent", "RawChatAgent", "SupportsAgentRun"]
# region Agent Protocol
@runtime_checkable
class AgentProtocol(Protocol):
class SupportsAgentRun(Protocol):
"""A protocol for an agent that can be invoked.
This protocol defines the interface that all agents must implement,
@@ -185,11 +185,11 @@ class AgentProtocol(Protocol):
Examples:
.. code-block:: python
from agent_framework import AgentProtocol
from agent_framework import SupportsAgentRun
# Any class implementing the required methods is compatible
# No need to inherit from AgentProtocol or use any framework classes
# No need to inherit from SupportsAgentRun or use any framework classes
class CustomAgent:
def __init__(self):
self.id = "custom-agent-001"
@@ -218,7 +218,7 @@ class AgentProtocol(Protocol):
# Verify the instance satisfies the protocol
instance = CustomAgent()
assert isinstance(instance, AgentProtocol)
assert isinstance(instance, SupportsAgentRun)
"""
id: str
@@ -297,7 +297,7 @@ class BaseAgent(SerializationMixin):
Note:
BaseAgent cannot be instantiated directly as it doesn't implement the
``run()`` and other methods required by AgentProtocol.
``run()`` and other methods required by SupportsAgentRun.
Use a concrete implementation like ChatAgent or create a subclass.
Examples:
@@ -451,7 +451,7 @@ class BaseAgent(SerializationMixin):
A FunctionTool that can be used as a tool by other agents.
Raises:
TypeError: If the agent does not implement AgentProtocol.
TypeError: If the agent does not implement SupportsAgentRun.
ValueError: If the agent tool name cannot be determined.
Examples:
@@ -468,9 +468,9 @@ class BaseAgent(SerializationMixin):
# Use the tool with another agent
coordinator = ChatAgent(chat_client=client, name="coordinator", tools=research_tool)
"""
# Verify that self implements AgentProtocol
if not isinstance(self, AgentProtocol):
raise TypeError(f"Agent {self.__class__.__name__} must implement AgentProtocol to be used as a tool")
# Verify that self implements SupportsAgentRun
if not isinstance(self, SupportsAgentRun):
raise TypeError(f"Agent {self.__class__.__name__} must implement SupportsAgentRun to be used as a tool")
tool_name = name or _sanitize_agent_name(self.name)
if tool_name is None:
@@ -34,7 +34,7 @@ else:
if TYPE_CHECKING:
from pydantic import BaseModel
from ._agents import AgentProtocol
from ._agents import SupportsAgentRun
from ._clients import ChatClientProtocol
from ._threads import AgentThread
from ._tools import FunctionTool
@@ -64,7 +64,7 @@ __all__ = [
"function_middleware",
]
TAgent = TypeVar("TAgent", bound="AgentProtocol")
AgentT = TypeVar("AgentT", bound="SupportsAgentRun")
TContext = TypeVar("TContext")
TUpdate = TypeVar("TUpdate")
@@ -154,7 +154,7 @@ class AgentContext:
def __init__(
self,
*,
agent: AgentProtocol,
agent: SupportsAgentRun,
messages: list[ChatMessage],
thread: AgentThread | None = None,
options: Mapping[str, Any] | None = None,
@@ -9,7 +9,7 @@ from typing_extensions import Never
from agent_framework import Content
from .._agents import AgentProtocol
from .._agents import SupportsAgentRun
from .._threads import AgentThread
from .._types import AgentResponse, AgentResponseUpdate, ChatMessage
from ._agent_utils import resolve_agent_id
@@ -80,7 +80,7 @@ class AgentExecutor(Executor):
def __init__(
self,
agent: AgentProtocol,
agent: SupportsAgentRun,
*,
agent_thread: AgentThread | None = None,
id: str | None = None,
@@ -1,9 +1,9 @@
# Copyright (c) Microsoft. All rights reserved.
from .._agents import AgentProtocol
from .._agents import SupportsAgentRun
def resolve_agent_id(agent: AgentProtocol) -> str:
def resolve_agent_id(agent: SupportsAgentRun) -> str:
"""Resolve the unique identifier for an agent.
Prefers the `.name` attribute if set; otherwise falls back to `.id`.
@@ -6,7 +6,7 @@ from collections.abc import Callable, Sequence
from dataclasses import dataclass
from typing import Any
from .._agents import AgentProtocol
from .._agents import SupportsAgentRun
from .._threads import AgentThread
from ..observability import OtelAttr, capture_exception, create_workflow_span
from ._agent_executor import AgentExecutor
@@ -171,7 +171,7 @@ class WorkflowBuilder:
self._max_iterations: int = max_iterations
self._name: str | None = name
self._description: str | None = description
# Maps underlying AgentProtocol object id -> wrapped Executor so we reuse the same wrapper
# Maps underlying SupportsAgentRun object id -> wrapped Executor so we reuse the same wrapper
# across set_start_executor / add_edge calls. This avoids multiple AgentExecutor instances
# being created for the same agent.
self._agent_wrappers: dict[str, Executor] = {}
@@ -187,7 +187,7 @@ class WorkflowBuilder:
self._executor_registry: dict[str, Callable[[], Executor]] = {}
# Output executors filter; if set, only outputs from these executors are yielded
self._output_executors: list[Executor | AgentProtocol | str] = []
self._output_executors: list[Executor | SupportsAgentRun | str] = []
# Agents auto-wrapped by builder now always stream incremental updates.
@@ -208,8 +208,8 @@ class WorkflowBuilder:
return executor.id
def _maybe_wrap_agent(self, candidate: Executor | AgentProtocol) -> Executor:
"""If the provided object implements AgentProtocol, wrap it in an AgentExecutor.
def _maybe_wrap_agent(self, candidate: Executor | SupportsAgentRun) -> Executor:
"""If the provided object implements SupportsAgentRun, wrap it in an AgentExecutor.
This allows fluent builder APIs to directly accept agents instead of
requiring callers to manually instantiate AgentExecutor.
@@ -221,13 +221,13 @@ class WorkflowBuilder:
An Executor instance, wrapping the agent if necessary.
"""
try: # Local import to avoid hard dependency at import time
from agent_framework import AgentProtocol # type: ignore
from agent_framework import SupportsAgentRun # type: ignore
except Exception: # pragma: no cover - defensive
AgentProtocol = object # type: ignore
SupportsAgentRun = object # type: ignore
if isinstance(candidate, Executor): # Already an executor
return candidate
if isinstance(candidate, AgentProtocol): # type: ignore[arg-type]
if isinstance(candidate, SupportsAgentRun): # type: ignore[arg-type]
# Reuse existing wrapper for the same agent instance if present
agent_instance_id = str(id(candidate))
existing = self._agent_wrappers.get(agent_instance_id)
@@ -244,7 +244,7 @@ class WorkflowBuilder:
return wrapper
raise TypeError(
f"WorkflowBuilder expected an Executor or AgentProtocol instance; got {type(candidate).__name__}."
f"WorkflowBuilder expected an Executor or SupportsAgentRun instance; got {type(candidate).__name__}."
)
def register_executor(self, factory_func: Callable[[], Executor], name: str | list[str]) -> Self:
@@ -321,7 +321,7 @@ class WorkflowBuilder:
def register_agent(
self,
factory_func: Callable[[], AgentProtocol],
factory_func: Callable[[], SupportsAgentRun],
name: str,
agent_thread: AgentThread | None = None,
) -> Self:
@@ -332,7 +332,7 @@ class WorkflowBuilder:
enabling deferred initialization and potentially reducing startup time.
Args:
factory_func: A callable that returns an AgentProtocol instance when called.
factory_func: A callable that returns an SupportsAgentRun instance when called.
name: The name of the registered agent factory. This doesn't have to match
the agent's internal name. But it must be unique within the workflow.
agent_thread: The thread to use for running the agent. If None, a new thread will be created when
@@ -375,8 +375,8 @@ class WorkflowBuilder:
def add_edge(
self,
source: Executor | AgentProtocol | str,
target: Executor | AgentProtocol | str,
source: Executor | SupportsAgentRun | str,
target: Executor | SupportsAgentRun | str,
condition: EdgeCondition | None = None,
) -> Self:
"""Add a directed edge between two executors.
@@ -441,8 +441,8 @@ class WorkflowBuilder:
not isinstance(source, str) and isinstance(target, str)
):
raise ValueError(
"Both source and target must be either registered factory names (str) "
"or Executor/AgentProtocol instances."
"Both source and target must be either registered factory names (str) or "
"Executor/SupportsAgentRun instances."
)
if isinstance(source, str) and isinstance(target, str):
@@ -450,7 +450,7 @@ class WorkflowBuilder:
self._edge_registry.append(_EdgeRegistration(source=source, target=target, condition=condition))
return self
# Both are Executor/AgentProtocol instances; wrap and add now
# Both are Executor/SupportsAgentRun instances; wrap and add now
source_exec = self._maybe_wrap_agent(source) # type: ignore[arg-type]
target_exec = self._maybe_wrap_agent(target) # type: ignore[arg-type]
source_id = self._add_executor(source_exec)
@@ -460,8 +460,8 @@ class WorkflowBuilder:
def add_fan_out_edges(
self,
source: Executor | AgentProtocol | str,
targets: Sequence[Executor | AgentProtocol | str],
source: Executor | SupportsAgentRun | str,
targets: Sequence[Executor | SupportsAgentRun | str],
) -> Self:
"""Add multiple edges to the workflow where messages from the source will be sent to all targets.
@@ -520,8 +520,8 @@ class WorkflowBuilder:
not isinstance(source, str) and any(isinstance(t, str) for t in targets)
):
raise ValueError(
"Both source and targets must be either registered factory names (str) "
"or Executor/AgentProtocol instances."
"Both source and targets must be either registered factory names (str) or "
"Executor/SupportsAgentRun instances."
)
if isinstance(source, str) and all(isinstance(t, str) for t in targets):
@@ -529,7 +529,7 @@ class WorkflowBuilder:
self._edge_registry.append(_FanOutEdgeRegistration(source=source, targets=list(targets))) # type: ignore
return self
# Both are Executor/AgentProtocol instances; wrap and add now
# Both are Executor/SupportsAgentRun instances; wrap and add now
source_exec = self._maybe_wrap_agent(source) # type: ignore[arg-type]
target_execs = [self._maybe_wrap_agent(t) for t in targets] # type: ignore[arg-type]
source_id = self._add_executor(source_exec)
@@ -540,7 +540,7 @@ class WorkflowBuilder:
def add_switch_case_edge_group(
self,
source: Executor | AgentProtocol | str,
source: Executor | SupportsAgentRun | str,
cases: Sequence[Case | Default],
) -> Self:
"""Add an edge group that represents a switch-case statement.
@@ -620,7 +620,7 @@ class WorkflowBuilder:
):
raise ValueError(
"Both source and case targets must be either registered factory names (str) "
"or Executor/AgentProtocol instances."
"or Executor/SupportsAgentRun instances."
)
if isinstance(source, str) and all(isinstance(case.target, str) for case in cases):
@@ -628,7 +628,7 @@ class WorkflowBuilder:
self._edge_registry.append(_SwitchCaseEdgeGroupRegistration(source=source, cases=list(cases))) # type: ignore
return self
# Source is an Executor/AgentProtocol instance; wrap and add now
# Source is an Executor/SupportsAgentRun instance; wrap and add now
source_exec = self._maybe_wrap_agent(source) # type: ignore[arg-type]
source_id = self._add_executor(source_exec)
# Convert case data types to internal types that only uses target_id.
@@ -647,8 +647,8 @@ class WorkflowBuilder:
def add_multi_selection_edge_group(
self,
source: Executor | AgentProtocol | str,
targets: Sequence[Executor | AgentProtocol | str],
source: Executor | SupportsAgentRun | str,
targets: Sequence[Executor | SupportsAgentRun | str],
selection_func: Callable[[Any, list[str]], list[str]],
) -> Self:
"""Add an edge group that represents a multi-selection execution model.
@@ -731,8 +731,8 @@ class WorkflowBuilder:
not isinstance(source, str) and any(isinstance(t, str) for t in targets)
):
raise ValueError(
"Both source and targets must be either registered factory names (str) "
"or Executor/AgentProtocol instances."
"Both source and targets must be either registered factory names (str) or "
"Executor/SupportsAgentRun instances."
)
if isinstance(source, str) and all(isinstance(t, str) for t in targets):
@@ -746,7 +746,7 @@ class WorkflowBuilder:
)
return self
# Both are Executor/AgentProtocol instances; wrap and add now
# Both are Executor/SupportsAgentRun instances; wrap and add now
source_exec = self._maybe_wrap_agent(source) # type: ignore
target_execs = [self._maybe_wrap_agent(t) for t in targets] # type: ignore
source_id = self._add_executor(source_exec)
@@ -757,8 +757,8 @@ class WorkflowBuilder:
def add_fan_in_edges(
self,
sources: Sequence[Executor | AgentProtocol | str],
target: Executor | AgentProtocol | str,
sources: Sequence[Executor | SupportsAgentRun | str],
target: Executor | SupportsAgentRun | str,
) -> Self:
"""Add multiple edges from sources to a single target executor.
@@ -816,8 +816,8 @@ class WorkflowBuilder:
not all(isinstance(s, str) for s in sources) and isinstance(target, str)
):
raise ValueError(
"Both sources and target must be either registered factory names (str) "
"or Executor/AgentProtocol instances."
"Both sources and target must be either registered factory names (str) or "
"Executor/SupportsAgentRun instances."
)
if all(isinstance(s, str) for s in sources) and isinstance(target, str):
@@ -825,7 +825,7 @@ class WorkflowBuilder:
self._edge_registry.append(_FanInEdgeRegistration(sources=list(sources), target=target)) # type: ignore
return self
# Both are Executor/AgentProtocol instances; wrap and add now
# Both are Executor/SupportsAgentRun instances; wrap and add now
source_execs = [self._maybe_wrap_agent(s) for s in sources] # type: ignore
target_exec = self._maybe_wrap_agent(target) # type: ignore
source_ids = [self._add_executor(s) for s in source_execs]
@@ -834,7 +834,7 @@ class WorkflowBuilder:
return self
def add_chain(self, executors: Sequence[Executor | AgentProtocol | str]) -> Self:
def add_chain(self, executors: Sequence[Executor | SupportsAgentRun | str]) -> Self:
"""Add a chain of executors to the workflow.
The output of each executor in the chain will be sent to the next executor in the chain.
@@ -895,7 +895,7 @@ class WorkflowBuilder:
if not all(isinstance(e, str) for e in executors) and any(isinstance(e, str) for e in executors):
raise ValueError(
"All executors in the chain must be either registered factory names (str) "
"or Executor/AgentProtocol instances."
"or Executor/SupportsAgentRun instances."
)
if all(isinstance(e, str) for e in executors):
@@ -904,21 +904,21 @@ class WorkflowBuilder:
self.add_edge(executors[i], executors[i + 1])
return self
# All are Executor/AgentProtocol instances; wrap and add now
# All are Executor/SupportsAgentRun instances; wrap and add now
# Wrap each candidate first to ensure stable IDs before adding edges
wrapped: list[Executor] = [self._maybe_wrap_agent(e) for e in executors] # type: ignore[arg-type]
for i in range(len(wrapped) - 1):
self.add_edge(wrapped[i], wrapped[i + 1])
return self
def set_start_executor(self, executor: Executor | AgentProtocol | str) -> Self:
def set_start_executor(self, executor: Executor | SupportsAgentRun | str) -> Self:
"""Set the starting executor for the workflow.
The start executor is the entry point for the workflow. When the workflow is executed,
the initial message will be sent to this executor.
Args:
executor: The starting executor, which can be an Executor instance, AgentProtocol instance,
executor: The starting executor, which can be an Executor instance, SupportsAgentRun instance,
or the name of a registered executor factory.
Returns:
@@ -1067,7 +1067,7 @@ class WorkflowBuilder:
self._checkpoint_storage = checkpoint_storage
return self
def with_output_from(self, executors: list[Executor | AgentProtocol | str]) -> Self:
def with_output_from(self, executors: list[Executor | SupportsAgentRun | str]) -> Self:
"""Specify which executors' outputs should be collected as workflow outputs.
By default, outputs from all executors are collected. This method allows
@@ -1231,7 +1231,11 @@ class WorkflowBuilder:
if isinstance(factory_name, str)
]
+ [ex.id for ex in self._output_executors if isinstance(ex, Executor)]
+ [resolve_agent_id(agent) for agent in self._output_executors if isinstance(agent, AgentProtocol)]
+ [
resolve_agent_id(agent)
for agent in self._output_executors
if isinstance(agent, SupportsAgentRun)
]
)
# Perform validation before creating the workflow
@@ -38,7 +38,7 @@ if TYPE_CHECKING: # pragma: no cover
from opentelemetry.util._decorator import _AgnosticContextManager # type: ignore[reportPrivateUsage]
from pydantic import BaseModel
from ._agents import AgentProtocol
from ._agents import SupportsAgentRun
from ._clients import ChatClientProtocol
from ._threads import AgentThread
from ._tools import FunctionTool
@@ -70,7 +70,7 @@ __all__ = [
]
TAgent = TypeVar("TAgent", bound="AgentProtocol")
AgentT = TypeVar("AgentT", bound="SupportsAgentRun")
TChatClient = TypeVar("TChatClient", bound="ChatClientProtocol[Any]")
+3 -3
View File
@@ -12,7 +12,6 @@ from pydantic import BaseModel
from pytest import fixture
from agent_framework import (
AgentProtocol,
AgentResponse,
AgentResponseUpdate,
AgentThread,
@@ -24,6 +23,7 @@ from agent_framework import (
Content,
FunctionInvocationLayer,
ResponseStream,
SupportsAgentRun,
ToolProtocol,
tool,
)
@@ -273,7 +273,7 @@ class MockAgentThread(AgentThread):
# Mock Agent implementation for testing
class MockAgent(AgentProtocol):
class MockAgent(SupportsAgentRun):
@property
def id(self) -> str:
return str(uuid4())
@@ -329,5 +329,5 @@ def agent_thread() -> AgentThread:
@fixture
def agent() -> AgentProtocol:
def agent() -> SupportsAgentRun:
return MockAgent()
@@ -10,7 +10,6 @@ import pytest
from pytest import raises
from agent_framework import (
AgentProtocol,
AgentResponse,
AgentResponseUpdate,
AgentThread,
@@ -24,6 +23,7 @@ from agent_framework import (
Context,
ContextProvider,
HostedCodeInterpreterTool,
SupportsAgentRun,
ToolProtocol,
tool,
)
@@ -36,17 +36,17 @@ def test_agent_thread_type(agent_thread: AgentThread) -> None:
assert isinstance(agent_thread, AgentThread)
def test_agent_type(agent: AgentProtocol) -> None:
assert isinstance(agent, AgentProtocol)
def test_agent_type(agent: SupportsAgentRun) -> None:
assert isinstance(agent, SupportsAgentRun)
async def test_agent_run(agent: AgentProtocol) -> None:
async def test_agent_run(agent: SupportsAgentRun) -> None:
response = await agent.run("test")
assert response.messages[0].role == "assistant"
assert response.messages[0].text == "Response"
async def test_agent_run_streaming(agent: AgentProtocol) -> None:
async def test_agent_run_streaming(agent: SupportsAgentRun) -> None:
async def collect_updates(updates: AsyncIterable[AgentResponseUpdate]) -> list[AgentResponseUpdate]:
return [u async for u in updates]
@@ -57,7 +57,7 @@ async def test_agent_run_streaming(agent: AgentProtocol) -> None:
def test_chat_client_agent_type(chat_client: ChatClientProtocol) -> None:
chat_client_agent = ChatAgent(chat_client=chat_client)
assert isinstance(chat_client_agent, AgentProtocol)
assert isinstance(chat_client_agent, SupportsAgentRun)
async def test_chat_client_agent_init(chat_client: ChatClientProtocol) -> None:
@@ -804,7 +804,7 @@ def test_sanitize_agent_name_replaces_invalid_chars():
# endregion
# region Test AgentProtocol.get_new_thread and deserialize_thread
# region Test SupportsAgentRun.get_new_thread and deserialize_thread
@pytest.mark.asyncio
@@ -8,7 +8,6 @@ import pytest
from pydantic import BaseModel, Field
from agent_framework import (
AgentProtocol,
AgentResponse,
AgentResponseUpdate,
ChatMessage,
@@ -16,6 +15,7 @@ from agent_framework import (
ChatResponseUpdate,
Content,
ResponseStream,
SupportsAgentRun,
)
from agent_framework._middleware import (
AgentContext,
@@ -35,7 +35,7 @@ from agent_framework._tools import FunctionTool
class TestAgentContext:
"""Test cases for AgentContext."""
def test_init_with_defaults(self, mock_agent: AgentProtocol) -> None:
def test_init_with_defaults(self, mock_agent: SupportsAgentRun) -> None:
"""Test AgentContext initialization with default values."""
messages = [ChatMessage(role="user", text="test")]
context = AgentContext(agent=mock_agent, messages=messages)
@@ -45,7 +45,7 @@ class TestAgentContext:
assert context.stream is False
assert context.metadata == {}
def test_init_with_custom_values(self, mock_agent: AgentProtocol) -> None:
def test_init_with_custom_values(self, mock_agent: SupportsAgentRun) -> None:
"""Test AgentContext initialization with custom values."""
messages = [ChatMessage(role="user", text="test")]
metadata = {"key": "value"}
@@ -56,7 +56,7 @@ class TestAgentContext:
assert context.stream is True
assert context.metadata == metadata
def test_init_with_thread(self, mock_agent: AgentProtocol) -> None:
def test_init_with_thread(self, mock_agent: SupportsAgentRun) -> None:
"""Test AgentContext initialization with thread parameter."""
from agent_framework import AgentThread
@@ -163,7 +163,7 @@ class TestAgentMiddlewarePipeline:
pipeline = AgentMiddlewarePipeline(test_middleware)
assert pipeline.has_middlewares
async def test_execute_no_middleware(self, mock_agent: AgentProtocol) -> None:
async def test_execute_no_middleware(self, mock_agent: SupportsAgentRun) -> None:
"""Test pipeline execution with no middleware."""
pipeline = AgentMiddlewarePipeline()
messages = [ChatMessage(role="user", text="test")]
@@ -177,7 +177,7 @@ class TestAgentMiddlewarePipeline:
result = await pipeline.execute(context, final_handler)
assert result == expected_response
async def test_execute_with_middleware(self, mock_agent: AgentProtocol) -> None:
async def test_execute_with_middleware(self, mock_agent: SupportsAgentRun) -> None:
"""Test pipeline execution with middleware."""
execution_order: list[str] = []
@@ -205,7 +205,7 @@ class TestAgentMiddlewarePipeline:
assert result == expected_response
assert execution_order == ["test_before", "handler", "test_after"]
async def test_execute_stream_no_middleware(self, mock_agent: AgentProtocol) -> None:
async def test_execute_stream_no_middleware(self, mock_agent: SupportsAgentRun) -> None:
"""Test pipeline streaming execution with no middleware."""
pipeline = AgentMiddlewarePipeline()
messages = [ChatMessage(role="user", text="test")]
@@ -228,7 +228,7 @@ class TestAgentMiddlewarePipeline:
assert updates[0].text == "chunk1"
assert updates[1].text == "chunk2"
async def test_execute_stream_with_middleware(self, mock_agent: AgentProtocol) -> None:
async def test_execute_stream_with_middleware(self, mock_agent: SupportsAgentRun) -> None:
"""Test pipeline streaming execution with middleware."""
execution_order: list[str] = []
@@ -265,7 +265,7 @@ class TestAgentMiddlewarePipeline:
assert updates[1].text == "chunk2"
assert execution_order == ["test_before", "test_after", "handler_start", "handler_end"]
async def test_execute_with_pre_next_termination(self, mock_agent: AgentProtocol) -> None:
async def test_execute_with_pre_next_termination(self, mock_agent: SupportsAgentRun) -> None:
"""Test pipeline execution with termination before next()."""
middleware = self.PreNextTerminateMiddleware()
pipeline = AgentMiddlewarePipeline(middleware)
@@ -283,7 +283,7 @@ class TestAgentMiddlewarePipeline:
# Handler should not be called when terminated before next()
assert execution_order == []
async def test_execute_with_post_next_termination(self, mock_agent: AgentProtocol) -> None:
async def test_execute_with_post_next_termination(self, mock_agent: SupportsAgentRun) -> None:
"""Test pipeline execution with termination after next()."""
middleware = self.PostNextTerminateMiddleware()
pipeline = AgentMiddlewarePipeline(middleware)
@@ -301,7 +301,7 @@ class TestAgentMiddlewarePipeline:
assert response.messages[0].text == "response"
assert execution_order == ["handler"]
async def test_execute_stream_with_pre_next_termination(self, mock_agent: AgentProtocol) -> None:
async def test_execute_stream_with_pre_next_termination(self, mock_agent: SupportsAgentRun) -> None:
"""Test pipeline streaming execution with termination before next()."""
middleware = self.PreNextTerminateMiddleware()
pipeline = AgentMiddlewarePipeline(middleware)
@@ -329,7 +329,7 @@ class TestAgentMiddlewarePipeline:
assert execution_order == []
assert not updates
async def test_execute_stream_with_post_next_termination(self, mock_agent: AgentProtocol) -> None:
async def test_execute_stream_with_post_next_termination(self, mock_agent: SupportsAgentRun) -> None:
"""Test pipeline streaming execution with termination after next()."""
middleware = self.PostNextTerminateMiddleware()
pipeline = AgentMiddlewarePipeline(middleware)
@@ -356,7 +356,7 @@ class TestAgentMiddlewarePipeline:
assert updates[1].text == "chunk2"
assert execution_order == ["handler_start", "handler_end"]
async def test_execute_with_thread_in_context(self, mock_agent: AgentProtocol) -> None:
async def test_execute_with_thread_in_context(self, mock_agent: SupportsAgentRun) -> None:
"""Test pipeline execution properly passes thread to middleware."""
from agent_framework import AgentThread
@@ -383,7 +383,7 @@ class TestAgentMiddlewarePipeline:
assert result == expected_response
assert captured_thread is thread
async def test_execute_with_no_thread_in_context(self, mock_agent: AgentProtocol) -> None:
async def test_execute_with_no_thread_in_context(self, mock_agent: SupportsAgentRun) -> None:
"""Test pipeline execution when no thread is provided."""
captured_thread = "not_none" # Use string to distinguish from None
@@ -761,7 +761,7 @@ class TestChatMiddlewarePipeline:
class TestClassBasedMiddleware:
"""Test cases for class-based middleware implementations."""
async def test_agent_middleware_execution(self, mock_agent: AgentProtocol) -> None:
async def test_agent_middleware_execution(self, mock_agent: SupportsAgentRun) -> None:
"""Test class-based agent middleware execution."""
metadata_updates: list[str] = []
@@ -825,7 +825,7 @@ class TestClassBasedMiddleware:
class TestFunctionBasedMiddleware:
"""Test cases for function-based middleware implementations."""
async def test_agent_function_middleware(self, mock_agent: AgentProtocol) -> None:
async def test_agent_function_middleware(self, mock_agent: SupportsAgentRun) -> None:
"""Test function-based agent middleware."""
execution_order: list[str] = []
@@ -879,7 +879,7 @@ class TestFunctionBasedMiddleware:
class TestMixedMiddleware:
"""Test cases for mixed class and function-based middleware."""
async def test_mixed_agent_middleware(self, mock_agent: AgentProtocol) -> None:
async def test_mixed_agent_middleware(self, mock_agent: SupportsAgentRun) -> None:
"""Test mixed class and function-based agent middleware."""
execution_order: list[str] = []
@@ -976,7 +976,7 @@ class TestMixedMiddleware:
class TestMultipleMiddlewareOrdering:
"""Test cases for multiple middleware execution order."""
async def test_agent_middleware_execution_order(self, mock_agent: AgentProtocol) -> None:
async def test_agent_middleware_execution_order(self, mock_agent: SupportsAgentRun) -> None:
"""Test that multiple agent middleware execute in registration order."""
execution_order: list[str] = []
@@ -1110,7 +1110,7 @@ class TestMultipleMiddlewareOrdering:
class TestContextContentValidation:
"""Test cases for validating middleware context content."""
async def test_agent_context_validation(self, mock_agent: AgentProtocol) -> None:
async def test_agent_context_validation(self, mock_agent: SupportsAgentRun) -> None:
"""Test that agent context contains expected data."""
class ContextValidationMiddleware(AgentMiddleware):
@@ -1231,7 +1231,7 @@ class TestContextContentValidation:
class TestStreamingScenarios:
"""Test cases for streaming and non-streaming scenarios."""
async def test_streaming_flag_validation(self, mock_agent: AgentProtocol) -> None:
async def test_streaming_flag_validation(self, mock_agent: SupportsAgentRun) -> None:
"""Test that stream flag is correctly set for streaming calls."""
streaming_flags: list[bool] = []
@@ -1271,7 +1271,7 @@ class TestStreamingScenarios:
# Verify flags: [non-streaming middleware, non-streaming handler, streaming middleware, streaming handler]
assert streaming_flags == [False, False, True, True]
async def test_streaming_middleware_behavior(self, mock_agent: AgentProtocol) -> None:
async def test_streaming_middleware_behavior(self, mock_agent: SupportsAgentRun) -> None:
"""Test middleware behavior with streaming responses."""
chunks_processed: list[str] = []
@@ -1437,7 +1437,7 @@ class MockFunctionArgs(BaseModel):
class TestMiddlewareExecutionControl:
"""Test cases for middleware execution control (when next() is called vs not called)."""
async def test_agent_middleware_no_next_no_execution(self, mock_agent: AgentProtocol) -> None:
async def test_agent_middleware_no_next_no_execution(self, mock_agent: SupportsAgentRun) -> None:
"""Test that when agent middleware doesn't call next(), no execution happens."""
class NoNextMiddleware(AgentMiddleware):
@@ -1464,7 +1464,7 @@ class TestMiddlewareExecutionControl:
assert not handler_called
assert context.result is None
async def test_agent_middleware_no_next_no_streaming_execution(self, mock_agent: AgentProtocol) -> None:
async def test_agent_middleware_no_next_no_streaming_execution(self, mock_agent: SupportsAgentRun) -> None:
"""Test that when agent middleware doesn't call next(), no streaming execution happens."""
class NoNextStreamingMiddleware(AgentMiddleware):
@@ -1529,7 +1529,7 @@ class TestMiddlewareExecutionControl:
assert not handler_called
assert context.result is None
async def test_multiple_middlewares_early_stop(self, mock_agent: AgentProtocol) -> None:
async def test_multiple_middlewares_early_stop(self, mock_agent: SupportsAgentRun) -> None:
"""Test that when first middleware doesn't call next(), subsequent middleware are not called."""
execution_order: list[str] = []
@@ -1664,9 +1664,9 @@ class TestMiddlewareExecutionControl:
@pytest.fixture
def mock_agent() -> AgentProtocol:
def mock_agent() -> SupportsAgentRun:
"""Mock agent for testing."""
agent = MagicMock(spec=AgentProtocol)
agent = MagicMock(spec=SupportsAgentRun)
agent.name = "test_agent"
return agent
@@ -8,13 +8,13 @@ import pytest
from pydantic import BaseModel, Field
from agent_framework import (
AgentProtocol,
AgentResponse,
AgentResponseUpdate,
ChatAgent,
ChatMessage,
Content,
ResponseStream,
SupportsAgentRun,
)
from agent_framework._middleware import (
AgentContext,
@@ -38,7 +38,7 @@ class FunctionTestArgs(BaseModel):
class TestResultOverrideMiddleware:
"""Test cases for middleware result override functionality."""
async def test_agent_middleware_response_override_non_streaming(self, mock_agent: AgentProtocol) -> None:
async def test_agent_middleware_response_override_non_streaming(self, mock_agent: SupportsAgentRun) -> None:
"""Test that agent middleware can override response for non-streaming execution."""
override_response = AgentResponse(messages=[ChatMessage(role="assistant", text="overridden response")])
@@ -69,7 +69,7 @@ class TestResultOverrideMiddleware:
# Verify original handler was called since middleware called next()
assert handler_called
async def test_agent_middleware_response_override_streaming(self, mock_agent: AgentProtocol) -> None:
async def test_agent_middleware_response_override_streaming(self, mock_agent: SupportsAgentRun) -> None:
"""Test that agent middleware can override response for streaming execution."""
async def override_stream() -> AsyncIterable[AgentResponseUpdate]:
@@ -211,7 +211,7 @@ class TestResultOverrideMiddleware:
assert normal_updates[0].text == "test streaming response "
assert normal_updates[1].text == "another update"
async def test_agent_middleware_conditional_no_next(self, mock_agent: AgentProtocol) -> None:
async def test_agent_middleware_conditional_no_next(self, mock_agent: SupportsAgentRun) -> None:
"""Test that when agent middleware conditionally doesn't call next(), no execution happens."""
class ConditionalNoNextMiddleware(AgentMiddleware):
@@ -303,7 +303,7 @@ class TestResultOverrideMiddleware:
class TestResultObservability:
"""Test cases for middleware result observability functionality."""
async def test_agent_middleware_response_observability(self, mock_agent: AgentProtocol) -> None:
async def test_agent_middleware_response_observability(self, mock_agent: SupportsAgentRun) -> None:
"""Test that middleware can observe response after execution."""
observed_responses: list[AgentResponse] = []
@@ -370,7 +370,7 @@ class TestResultObservability:
assert observed_results[0] == "executed function result"
assert result == observed_results[0]
async def test_agent_middleware_post_execution_override(self, mock_agent: AgentProtocol) -> None:
async def test_agent_middleware_post_execution_override(self, mock_agent: SupportsAgentRun) -> None:
"""Test that middleware can override response after observing execution."""
class PostExecutionOverrideMiddleware(AgentMiddleware):
@@ -436,9 +436,9 @@ class TestResultObservability:
@pytest.fixture
def mock_agent() -> AgentProtocol:
def mock_agent() -> SupportsAgentRun:
"""Mock agent for testing."""
agent = MagicMock(spec=AgentProtocol)
agent = MagicMock(spec=SupportsAgentRun)
agent.name = "test_agent"
return agent
@@ -1853,13 +1853,13 @@ class TestChatAgentChatMiddleware:
# class TestMiddlewareWithProtocolOnlyAgent:
# """Test use_agent_middleware with agents implementing only AgentProtocol."""
# """Test use_agent_middleware with agents implementing only SupportsAgentRun."""
# async def test_middleware_with_protocol_only_agent(self) -> None:
# """Verify middleware works without BaseAgent inheritance for both run."""
# from collections.abc import AsyncIterable
# from agent_framework import AgentProtocol, AgentResponse, AgentResponseUpdate
# from agent_framework import SupportsAgentRun, AgentResponse, AgentResponseUpdate
# execution_order: list[str] = []
@@ -1873,7 +1873,7 @@ class TestChatAgentChatMiddleware:
# @use_agent_middleware
# class ProtocolOnlyAgent:
# """Minimal agent implementing only AgentProtocol, not inheriting from BaseAgent."""
# """Minimal agent implementing only SupportsAgentRun, not inheriting from BaseAgent."""
# def __init__(self):
# self.id = "protocol-only-agent"
@@ -1896,7 +1896,7 @@ class TestChatAgentChatMiddleware:
# return None
# agent = ProtocolOnlyAgent()
# assert isinstance(agent, AgentProtocol)
# assert isinstance(agent, SupportsAgentRun)
# # Test run (non-streaming)
# response = await agent.run("test message")
@@ -12,7 +12,6 @@ from opentelemetry.trace import StatusCode
from agent_framework import (
AGENT_FRAMEWORK_USER_AGENT,
AgentProtocol,
AgentResponse,
BaseChatClient,
ChatMessage,
@@ -20,6 +19,7 @@ from agent_framework import (
ChatResponseUpdate,
Content,
ResponseStream,
SupportsAgentRun,
UsageDetails,
prepend_agent_framework_to_user_agent,
tool,
@@ -473,7 +473,7 @@ def mock_chat_agent():
@pytest.mark.parametrize("enable_sensitive_data", [True, False], indirect=True)
async def test_agent_instrumentation_enabled(
mock_chat_agent: AgentProtocol, span_exporter: InMemorySpanExporter, enable_sensitive_data
mock_chat_agent: SupportsAgentRun, span_exporter: InMemorySpanExporter, enable_sensitive_data
):
"""Test that when agent diagnostics are enabled, telemetry is applied."""
@@ -499,7 +499,7 @@ async def test_agent_instrumentation_enabled(
@pytest.mark.parametrize("enable_sensitive_data", [True, False], indirect=True)
async def test_agent_streaming_response_with_diagnostics_enabled(
mock_chat_agent: AgentProtocol, span_exporter: InMemorySpanExporter, enable_sensitive_data
mock_chat_agent: SupportsAgentRun, span_exporter: InMemorySpanExporter, enable_sensitive_data
):
"""Test agent streaming telemetry through the agent telemetry mixin."""
agent = mock_chat_agent()
@@ -9,7 +9,6 @@ from typing_extensions import Never
from agent_framework import (
AgentExecutorRequest,
AgentProtocol,
AgentResponse,
AgentResponseUpdate,
AgentThread,
@@ -18,6 +17,7 @@ from agent_framework import (
Content,
Executor,
ResponseStream,
SupportsAgentRun,
UsageDetails,
WorkflowAgent,
WorkflowBuilder,
@@ -615,7 +615,7 @@ class TestWorkflowAgent:
async def test_agent_executor_output_response_false_filters_streaming_events(self):
"""Test that AgentExecutor with output_response=False does not surface streaming events."""
class MockAgent(AgentProtocol):
class MockAgent(SupportsAgentRun):
"""Mock agent for testing."""
def __init__(self, name: str, response_text: str) -> None:
@@ -705,7 +705,7 @@ class TestWorkflowAgent:
async def test_agent_executor_output_response_no_duplicate_from_workflow_output_event(self):
"""Test that AgentExecutor with output_response=True does not duplicate content."""
class MockAgent(AgentProtocol):
class MockAgent(SupportsAgentRun):
"""Mock agent for testing."""
def __init__(self, name: str, response_text: str) -> None:
@@ -422,7 +422,7 @@ def test_mixing_eager_and_lazy_initialization_error():
ValueError,
match=(
r"Both source and target must be either registered factory names \(str\) "
r"or Executor/AgentProtocol instances\."
r"or Executor/SupportsAgentRun instances\."
),
):
builder.add_edge(eager_executor, "Lazy")