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
@@ -6,7 +6,7 @@ import logging
from collections.abc import Callable, Sequence
from typing import Any
from agent_framework import AgentProtocol, ChatMessage
from agent_framework import ChatMessage, SupportsAgentRun
from agent_framework._workflows._agent_executor import AgentExecutor, AgentExecutorRequest, AgentExecutorResponse
from agent_framework._workflows._agent_utils import resolve_agent_id
from agent_framework._workflows._checkpoint import CheckpointStorage
@@ -29,8 +29,8 @@ parallel workflow with:
- a default aggregator that combines all agent conversations and completes the workflow
Notes:
- Participants can be provided as AgentProtocol or Executor instances via `.participants()`,
or as factories returning AgentProtocol or Executor via `.register_participants()`.
- Participants can be provided as SupportsAgentRun or Executor instances via `.participants()`,
or as factories returning SupportsAgentRun or Executor via `.register_participants()`.
- A custom aggregator can be provided as:
- an Executor instance (it should handle list[AgentExecutorResponse],
yield output), or
@@ -186,8 +186,8 @@ class _CallbackAggregator(Executor):
class ConcurrentBuilder:
r"""High-level builder for concurrent agent workflows.
- `participants([...])` accepts a list of AgentProtocol (recommended) or Executor.
- `register_participants([...])` accepts a list of factories for AgentProtocol (recommended)
- `participants([...])` accepts a list of SupportsAgentRun (recommended) or Executor.
- `register_participants([...])` accepts a list of factories for SupportsAgentRun (recommended)
or Executor factories
- `build()` wires: dispatcher -> fan-out -> participants -> fan-in -> aggregator.
- `with_aggregator(...)` overrides the default aggregator with an Executor or callback.
@@ -238,8 +238,8 @@ class ConcurrentBuilder:
"""
def __init__(self) -> None:
self._participants: list[AgentProtocol | Executor] = []
self._participant_factories: list[Callable[[], AgentProtocol | Executor]] = []
self._participants: list[SupportsAgentRun | Executor] = []
self._participant_factories: list[Callable[[], SupportsAgentRun | Executor]] = []
self._aggregator: Executor | None = None
self._aggregator_factory: Callable[[], Executor] | None = None
self._checkpoint_storage: CheckpointStorage | None = None
@@ -249,16 +249,16 @@ class ConcurrentBuilder:
def register_participants(
self,
participant_factories: Sequence[Callable[[], AgentProtocol | Executor]],
participant_factories: Sequence[Callable[[], SupportsAgentRun | Executor]],
) -> "ConcurrentBuilder":
r"""Define the parallel participants for this concurrent workflow.
Accepts factories (callables) that return AgentProtocol instances (e.g., created
Accepts factories (callables) that return SupportsAgentRun instances (e.g., created
by a chat client) or Executor instances. Each participant created by a factory
is wired as a parallel branch using fan-out edges from an internal dispatcher.
Args:
participant_factories: Sequence of callables returning AgentProtocol or Executor instances
participant_factories: Sequence of callables returning SupportsAgentRun or Executor instances
Raises:
ValueError: if `participant_factories` is empty or `.participants()`
@@ -300,20 +300,20 @@ class ConcurrentBuilder:
self._participant_factories = list(participant_factories)
return self
def participants(self, participants: Sequence[AgentProtocol | Executor]) -> "ConcurrentBuilder":
def participants(self, participants: Sequence[SupportsAgentRun | Executor]) -> "ConcurrentBuilder":
r"""Define the parallel participants for this concurrent workflow.
Accepts AgentProtocol instances (e.g., created by a chat client) or Executor
Accepts SupportsAgentRun instances (e.g., created by a chat client) or Executor
instances. Each participant is wired as a parallel branch using fan-out edges
from an internal dispatcher.
Args:
participants: Sequence of AgentProtocol or Executor instances
participants: Sequence of SupportsAgentRun or Executor instances
Raises:
ValueError: if `participants` is empty, contains duplicates, or `.register_participants()`
or `.participants()` were already called
TypeError: if any entry is not AgentProtocol or Executor
TypeError: if any entry is not SupportsAgentRun or Executor
Example:
@@ -341,13 +341,13 @@ class ConcurrentBuilder:
if p.id in seen_executor_ids:
raise ValueError(f"Duplicate executor participant detected: id '{p.id}'")
seen_executor_ids.add(p.id)
elif isinstance(p, AgentProtocol):
elif isinstance(p, SupportsAgentRun):
pid = id(p)
if pid in seen_agent_ids:
raise ValueError("Duplicate agent participant detected (same agent instance provided twice)")
seen_agent_ids.add(pid)
else:
raise TypeError(f"participants must be AgentProtocol or Executor instances; got {type(p).__name__}")
raise TypeError(f"participants must be SupportsAgentRun or Executor instances; got {type(p).__name__}")
self._participants = list(participants)
return self
@@ -459,7 +459,7 @@ class ConcurrentBuilder:
def with_request_info(
self,
*,
agents: Sequence[str | AgentProtocol] | None = None,
agents: Sequence[str | SupportsAgentRun] | None = None,
) -> "ConcurrentBuilder":
"""Enable request info after agent participant responses.
@@ -508,7 +508,7 @@ class ConcurrentBuilder:
raise ValueError("No participants provided. Call .participants() or .register_participants() first.")
# We don't need to check if both are set since that is handled in the respective methods
participants: list[Executor | AgentProtocol] = []
participants: list[Executor | SupportsAgentRun] = []
if self._participant_factories:
# Resolve the participant factories now. This doesn't break the factory pattern
# since the Sequential builder still creates new instances per workflow build.
@@ -522,7 +522,7 @@ class ConcurrentBuilder:
for p in participants:
if isinstance(p, Executor):
executors.append(p)
elif isinstance(p, AgentProtocol):
elif isinstance(p, SupportsAgentRun):
if self._request_info_enabled and (
not self._request_info_filter or resolve_agent_id(p) in self._request_info_filter
):
@@ -531,7 +531,7 @@ class ConcurrentBuilder:
else:
executors.append(AgentExecutor(p))
else:
raise TypeError(f"Participants must be AgentProtocol or Executor instances. Got {type(p).__name__}.")
raise TypeError(f"Participants must be SupportsAgentRun or Executor instances. Got {type(p).__name__}.")
return executors
@@ -26,7 +26,7 @@ from collections.abc import Awaitable, Callable, Sequence
from dataclasses import dataclass
from typing import Any, ClassVar, cast, overload
from agent_framework import AgentProtocol, ChatAgent
from agent_framework import ChatAgent, SupportsAgentRun
from agent_framework._threads import AgentThread
from agent_framework._types import ChatMessage
from agent_framework._workflows._agent_executor import AgentExecutor, AgentExecutorRequest, AgentExecutorResponse
@@ -523,8 +523,8 @@ class GroupChatBuilder:
def __init__(self) -> None:
"""Initialize the GroupChatBuilder."""
self._participants: dict[str, AgentProtocol | Executor] = {}
self._participant_factories: list[Callable[[], AgentProtocol | Executor]] = []
self._participants: dict[str, SupportsAgentRun | Executor] = {}
self._participant_factories: list[Callable[[], SupportsAgentRun | Executor]] = []
# Orchestrator related members
self._orchestrator: BaseGroupChatOrchestrator | None = None
@@ -683,13 +683,13 @@ class GroupChatBuilder:
def register_participants(
self,
participant_factories: Sequence[Callable[[], AgentProtocol | Executor]],
participant_factories: Sequence[Callable[[], SupportsAgentRun | Executor]],
) -> "GroupChatBuilder":
"""Register participant factories for this group chat workflow.
Args:
participant_factories: Sequence of callables that produce participant definitions
when invoked. Each callable should return either an AgentProtocol instance
when invoked. Each callable should return either an SupportsAgentRun instance
(auto-wrapped as AgentExecutor) or an Executor instance.
Returns:
@@ -711,10 +711,10 @@ class GroupChatBuilder:
self._participant_factories = list(participant_factories)
return self
def participants(self, participants: Sequence[AgentProtocol | Executor]) -> "GroupChatBuilder":
def participants(self, participants: Sequence[SupportsAgentRun | Executor]) -> "GroupChatBuilder":
"""Define participants for this group chat workflow.
Accepts AgentProtocol instances (auto-wrapped as AgentExecutor) or Executor instances.
Accepts SupportsAgentRun instances (auto-wrapped as AgentExecutor) or Executor instances.
Args:
participants: Sequence of participant definitions
@@ -725,7 +725,7 @@ class GroupChatBuilder:
Raises:
ValueError: If participants are empty, names are duplicated, or participants
or participant factories are already set
TypeError: If any participant is not AgentProtocol or Executor instance
TypeError: If any participant is not SupportsAgentRun or Executor instance
Example:
@@ -750,17 +750,17 @@ class GroupChatBuilder:
raise ValueError("participants cannot be empty.")
# Name of the executor mapped to participant instance
named: dict[str, AgentProtocol | Executor] = {}
named: dict[str, SupportsAgentRun | Executor] = {}
for participant in participants:
if isinstance(participant, Executor):
identifier = participant.id
elif isinstance(participant, AgentProtocol):
elif isinstance(participant, SupportsAgentRun):
if not participant.name:
raise ValueError("AgentProtocol participants must have a non-empty name.")
raise ValueError("SupportsAgentRun participants must have a non-empty name.")
identifier = participant.name
else:
raise TypeError(
f"Participants must be AgentProtocol or Executor instances. Got {type(participant).__name__}."
f"Participants must be SupportsAgentRun or Executor instances. Got {type(participant).__name__}."
)
if identifier in named:
@@ -861,7 +861,7 @@ class GroupChatBuilder:
self._checkpoint_storage = checkpoint_storage
return self
def with_request_info(self, *, agents: Sequence[str | AgentProtocol] | None = None) -> "GroupChatBuilder":
def with_request_info(self, *, agents: Sequence[str | SupportsAgentRun] | None = None) -> "GroupChatBuilder":
"""Enable request info after agent participant responses.
This enables human-in-the-loop (HIL) scenarios for the group chat orchestration.
@@ -962,7 +962,7 @@ class GroupChatBuilder:
raise ValueError("No participants provided. Call .participants() or .register_participants() first.")
# We don't need to check if both are set since that is handled in the respective methods
participants: list[Executor | AgentProtocol] = []
participants: list[Executor | SupportsAgentRun] = []
if self._participant_factories:
for factory in self._participant_factories:
participant = factory()
@@ -974,7 +974,7 @@ class GroupChatBuilder:
for participant in participants:
if isinstance(participant, Executor):
executors.append(participant)
elif isinstance(participant, AgentProtocol):
elif isinstance(participant, SupportsAgentRun):
if self._request_info_enabled and (
not self._request_info_filter or resolve_agent_id(participant) in self._request_info_filter
):
@@ -984,7 +984,7 @@ class GroupChatBuilder:
executors.append(AgentExecutor(participant))
else:
raise TypeError(
f"Participants must be AgentProtocol or Executor instances. Got {type(participant).__name__}."
f"Participants must be SupportsAgentRun or Executor instances. Got {type(participant).__name__}."
)
return executors
@@ -36,7 +36,7 @@ from collections.abc import Awaitable, Callable, Mapping, Sequence
from dataclasses import dataclass
from typing import Any, cast
from agent_framework import AgentProtocol, ChatAgent
from agent_framework import ChatAgent, SupportsAgentRun
from agent_framework._middleware import FunctionInvocationContext, FunctionMiddleware
from agent_framework._threads import AgentThread
from agent_framework._tools import FunctionTool, tool
@@ -89,14 +89,14 @@ class HandoffConfiguration:
target_id: str
description: str | None = None
def __init__(self, *, target: str | AgentProtocol, description: str | None = None) -> None:
def __init__(self, *, target: str | SupportsAgentRun, description: str | None = None) -> None:
"""Initialize HandoffConfiguration.
Args:
target: Target agent identifier or AgentProtocol instance
target: Target agent identifier or SupportsAgentRun instance
description: Optional human-readable description of the handoff
"""
self.target_id = resolve_agent_id(target) if isinstance(target, AgentProtocol) else target
self.target_id = resolve_agent_id(target) if isinstance(target, SupportsAgentRun) else target
self.description = description
def __eq__(self, other: Any) -> bool:
@@ -193,7 +193,7 @@ class HandoffAgentExecutor(AgentExecutor):
def __init__(
self,
agent: AgentProtocol,
agent: SupportsAgentRun,
handoffs: Sequence[HandoffConfiguration],
*,
agent_thread: AgentThread | None = None,
@@ -236,9 +236,9 @@ class HandoffAgentExecutor(AgentExecutor):
def _prepare_agent_with_handoffs(
self,
agent: AgentProtocol,
agent: SupportsAgentRun,
handoffs: Sequence[HandoffConfiguration],
) -> AgentProtocol:
) -> SupportsAgentRun:
"""Prepare an agent by adding handoff tools for the specified target agents.
Args:
@@ -574,8 +574,8 @@ class HandoffBuilder:
self,
*,
name: str | None = None,
participants: Sequence[AgentProtocol] | None = None,
participant_factories: Mapping[str, Callable[[], AgentProtocol]] | None = None,
participants: Sequence[SupportsAgentRun] | None = None,
participant_factories: Mapping[str, Callable[[], SupportsAgentRun]] | None = None,
description: str | None = None,
) -> None:
r"""Initialize a HandoffBuilder for creating conversational handoff workflows.
@@ -604,8 +604,8 @@ class HandoffBuilder:
self._description = description
# Participant related members
self._participants: dict[str, AgentProtocol] = {}
self._participant_factories: dict[str, Callable[[], AgentProtocol]] = {}
self._participants: dict[str, SupportsAgentRun] = {}
self._participant_factories: dict[str, Callable[[], SupportsAgentRun]] = {}
self._start_id: str | None = None
if participant_factories:
self.register_participants(participant_factories)
@@ -629,16 +629,16 @@ class HandoffBuilder:
self._termination_condition: Callable[[list[ChatMessage]], bool | Awaitable[bool]] | None = None
def register_participants(
self, participant_factories: Mapping[str, Callable[[], AgentProtocol]]
self, participant_factories: Mapping[str, Callable[[], SupportsAgentRun]]
) -> "HandoffBuilder":
"""Register factories that produce agents for the handoff workflow.
Each factory is a callable that returns an AgentProtocol instance.
Each factory is a callable that returns an SupportsAgentRun instance.
Factories are invoked when building the workflow, allowing for lazy instantiation
and state isolation per workflow instance.
Args:
participant_factories: Mapping of factory names to callables that return AgentProtocol
participant_factories: Mapping of factory names to callables that return SupportsAgentRun
instances. Each produced participant must have a unique identifier
(`.name` is preferred if set, otherwise `.id` is used).
@@ -690,11 +690,11 @@ class HandoffBuilder:
self._participant_factories = dict(participant_factories)
return self
def participants(self, participants: Sequence[AgentProtocol]) -> "HandoffBuilder":
def participants(self, participants: Sequence[SupportsAgentRun]) -> "HandoffBuilder":
"""Register the agents that will participate in the handoff workflow.
Args:
participants: Sequence of AgentProtocol instances. Each must have a unique identifier.
participants: Sequence of SupportsAgentRun instances. Each must have a unique identifier.
(`.name` is preferred if set, otherwise `.id` is used).
Returns:
@@ -703,7 +703,7 @@ class HandoffBuilder:
Raises:
ValueError: If participants is empty, contains duplicates, or `.participants()` or
`.register_participants()` has already been called.
TypeError: If participants are not AgentProtocol instances.
TypeError: If participants are not SupportsAgentRun instances.
Example:
@@ -729,13 +729,13 @@ class HandoffBuilder:
if not participants:
raise ValueError("participants cannot be empty")
named: dict[str, AgentProtocol] = {}
named: dict[str, SupportsAgentRun] = {}
for participant in participants:
if isinstance(participant, AgentProtocol):
if isinstance(participant, SupportsAgentRun):
resolved_id = self._resolve_to_id(participant)
else:
raise TypeError(
f"Participants must be AgentProtocol or Executor instances. Got {type(participant).__name__}."
f"Participants must be SupportsAgentRun or Executor instances. Got {type(participant).__name__}."
)
if resolved_id in named:
@@ -748,8 +748,8 @@ class HandoffBuilder:
def add_handoff(
self,
source: str | AgentProtocol,
targets: Sequence[str] | Sequence[AgentProtocol],
source: str | SupportsAgentRun,
targets: Sequence[str] | Sequence[SupportsAgentRun],
*,
description: str | None = None,
) -> "HandoffBuilder":
@@ -763,11 +763,11 @@ class HandoffBuilder:
Args:
source: The agent that can initiate the handoff. Can be:
- Factory name (str): If using participant factories
- AgentProtocol instance: The actual agent object
- SupportsAgentRun instance: The actual agent object
- Cannot mix factory names and instances across source and targets
targets: One or more target agents that the source can hand off to. Can be:
- Factory name (str): If using participant factories
- AgentProtocol instance: The actual agent object
- SupportsAgentRun instance: The actual agent object
- Single target: ["billing_agent"] or [agent_instance]
- Multiple targets: ["billing_agent", "support_agent"] or [agent1, agent2]
- Cannot mix factory names and instances across source and targets
@@ -786,7 +786,7 @@ class HandoffBuilder:
participants(...) hasn't been called yet.
2) If source or targets are factory names (str) but participant_factories(...)
hasn't been called yet, or if they are not in the participant_factories list.
TypeError: If mixing factory names (str) and AgentProtocol/Executor instances
TypeError: If mixing factory names (str) and SupportsAgentRun/Executor instances
Examples:
Single target (using factory name):
@@ -848,7 +848,7 @@ class HandoffBuilder:
self._handoff_config[source].add(HandoffConfiguration(target=t, description=description))
return self
if isinstance(source, (AgentProtocol)) and all(isinstance(t, AgentProtocol) for t in targets):
if isinstance(source, (SupportsAgentRun)) and all(isinstance(t, SupportsAgentRun) for t in targets):
# Both source and targets are instances
if not self._participants:
raise ValueError("Call participants(...) before add_handoff(...)")
@@ -881,10 +881,10 @@ class HandoffBuilder:
return self
raise TypeError(
"Cannot mix factory names (str) and AgentProtocol instances across source and targets in add_handoff()"
"Cannot mix factory names (str) and SupportsAgentRun instances across source and targets in add_handoff()"
)
def with_start_agent(self, agent: str | AgentProtocol) -> "HandoffBuilder":
def with_start_agent(self, agent: str | SupportsAgentRun) -> "HandoffBuilder":
"""Set the agent that will initiate the handoff workflow.
If not specified, the first registered participant will be used as the starting agent.
@@ -892,7 +892,7 @@ class HandoffBuilder:
Args:
agent: The agent that will start the workflow. Can be:
- Factory name (str): If using participant factories
- AgentProtocol instance: The actual agent object
- SupportsAgentRun instance: The actual agent object
Returns:
Self for method chaining.
"""
@@ -903,7 +903,7 @@ class HandoffBuilder:
else:
raise ValueError("Call register_participants(...) before with_start_agent(...)")
self._start_id = agent
elif isinstance(agent, AgentProtocol):
elif isinstance(agent, SupportsAgentRun):
resolved_id = self._resolve_to_id(agent)
if self._participants:
if resolved_id not in self._participants:
@@ -912,14 +912,14 @@ class HandoffBuilder:
raise ValueError("Call participants(...) before with_start_agent(...)")
self._start_id = resolved_id
else:
raise TypeError("Start agent must be a factory name (str) or an AgentProtocol instance")
raise TypeError("Start agent must be a factory name (str) or an SupportsAgentRun instance")
return self
def with_autonomous_mode(
self,
*,
agents: Sequence[AgentProtocol] | Sequence[str] | None = None,
agents: Sequence[SupportsAgentRun] | Sequence[str] | None = None,
prompts: dict[str, str] | None = None,
turn_limits: dict[str, int] | None = None,
) -> "HandoffBuilder":
@@ -933,7 +933,7 @@ class HandoffBuilder:
Args:
agents: Optional list of agents to enable autonomous mode for. Can be:
- Factory names (str): If using participant factories
- AgentProtocol instances: The actual agent objects
- SupportsAgentRun instances: The actual agent objects
- If not provided, all agents will operate in autonomous mode.
prompts: Optional mapping of agent identifiers/factory names to custom prompts to use when continuing
in autonomous mode. If not provided, a default prompt will be used.
@@ -1084,7 +1084,7 @@ class HandoffBuilder:
# region Internal Helper Methods
def _resolve_agents(self) -> dict[str, AgentProtocol]:
def _resolve_agents(self) -> dict[str, SupportsAgentRun]:
"""Resolve participant factories into agent instances.
If agent instances were provided directly via participants(...), those are
@@ -1092,7 +1092,7 @@ class HandoffBuilder:
those are invoked to create the agent instances.
Returns:
Map of executor IDs or factory names to `AgentProtocol` instances
Map of executor IDs or factory names to `SupportsAgentRun` instances
"""
if not self._participants and not self._participant_factories:
raise ValueError("No participants provided. Call .participants() or .register_participants() first.")
@@ -1103,13 +1103,13 @@ class HandoffBuilder:
if self._participant_factories:
# Invoke each factory to create participant instances
factory_names_to_agents: dict[str, AgentProtocol] = {}
factory_names_to_agents: dict[str, SupportsAgentRun] = {}
for factory_name, factory in self._participant_factories.items():
instance = factory()
if isinstance(instance, AgentProtocol):
if isinstance(instance, SupportsAgentRun):
resolved_id = self._resolve_to_id(instance)
else:
raise TypeError(f"Participants must be AgentProtocol instances. Got {type(instance).__name__}.")
raise TypeError(f"Participants must be SupportsAgentRun instances. Got {type(instance).__name__}.")
if resolved_id in factory_names_to_agents:
raise ValueError(f"Duplicate participant name '{resolved_id}' detected")
@@ -1122,11 +1122,11 @@ class HandoffBuilder:
raise ValueError("No executors or participant_factories have been configured")
def _resolve_handoffs(self, agents: Mapping[str, AgentProtocol]) -> dict[str, list[HandoffConfiguration]]:
def _resolve_handoffs(self, agents: Mapping[str, SupportsAgentRun]) -> dict[str, list[HandoffConfiguration]]:
"""Handoffs may be specified using factory names or instances; resolve to executor IDs.
Args:
agents: Map of agent IDs or factory names to `AgentProtocol` instances
agents: Map of agent IDs or factory names to `SupportsAgentRun` instances
Returns:
Map of executor IDs to list of HandoffConfiguration instances
@@ -1173,13 +1173,13 @@ class HandoffBuilder:
def _resolve_executors(
self,
agents: dict[str, AgentProtocol],
agents: dict[str, SupportsAgentRun],
handoffs: dict[str, list[HandoffConfiguration]],
) -> dict[str, HandoffAgentExecutor]:
"""Resolve agents into HandoffAgentExecutors.
Args:
agents: Map of agent IDs or factory names to `AgentProtocol` instances
agents: Map of agent IDs or factory names to `SupportsAgentRun` instances
handoffs: Map of executor IDs to list of HandoffConfiguration instances
Returns:
@@ -1213,9 +1213,9 @@ class HandoffBuilder:
return executors
def _resolve_to_id(self, candidate: str | AgentProtocol) -> str:
def _resolve_to_id(self, candidate: str | SupportsAgentRun) -> str:
"""Resolve a participant reference into a concrete executor identifier."""
if isinstance(candidate, AgentProtocol):
if isinstance(candidate, SupportsAgentRun):
return resolve_agent_id(candidate)
if isinstance(candidate, str):
return candidate
@@ -13,9 +13,9 @@ from enum import Enum
from typing import Any, ClassVar, TypeVar, cast, overload
from agent_framework import (
AgentProtocol,
AgentResponse,
ChatMessage,
SupportsAgentRun,
)
from agent_framework._workflows._agent_executor import AgentExecutor, AgentExecutorRequest, AgentExecutorResponse
from agent_framework._workflows._checkpoint import CheckpointStorage
@@ -521,7 +521,7 @@ class StandardMagenticManager(MagenticManagerBase):
def __init__(
self,
agent: AgentProtocol,
agent: SupportsAgentRun,
task_ledger: _MagenticTaskLedger | None = None,
*,
task_ledger_facts_prompt: str | None = None,
@@ -562,7 +562,7 @@ class StandardMagenticManager(MagenticManagerBase):
max_round_count=max_round_count,
)
self._agent: AgentProtocol = agent
self._agent: SupportsAgentRun = agent
self.task_ledger: _MagenticTaskLedger | None = task_ledger
# Prompts may be overridden if needed
@@ -1311,10 +1311,10 @@ class MagenticOrchestrator(BaseGroupChatOrchestrator):
class MagenticAgentExecutor(AgentExecutor):
"""Specialized AgentExecutor for Magentic agent participants."""
def __init__(self, agent: AgentProtocol) -> None:
def __init__(self, agent: SupportsAgentRun) -> None:
"""Initialize a Magentic Agent Executor.
This executor wraps an AgentProtocol instance to be used as a participant
This executor wraps an SupportsAgentRun instance to be used as a participant
in a Magentic One workflow.
Args:
@@ -1377,13 +1377,13 @@ class MagenticBuilder:
def __init__(self) -> None:
"""Initialize the Magentic workflow builder."""
self._participants: dict[str, AgentProtocol | Executor] = {}
self._participant_factories: list[Callable[[], AgentProtocol | Executor]] = []
self._participants: dict[str, SupportsAgentRun | Executor] = {}
self._participant_factories: list[Callable[[], SupportsAgentRun | Executor]] = []
# Manager related members
self._manager: MagenticManagerBase | None = None
self._manager_factory: Callable[[], MagenticManagerBase] | None = None
self._manager_agent_factory: Callable[[], AgentProtocol] | None = None
self._manager_agent_factory: Callable[[], SupportsAgentRun] | None = None
self._standard_manager_options: dict[str, Any] = {}
self._enable_plan_review: bool = False
@@ -1394,12 +1394,12 @@ class MagenticBuilder:
def register_participants(
self,
participant_factories: Sequence[Callable[[], AgentProtocol | Executor]],
participant_factories: Sequence[Callable[[], SupportsAgentRun | Executor]],
) -> "MagenticBuilder":
"""Register participant factories for this Magentic workflow.
Args:
participant_factories: Sequence of callables that return AgentProtocol or Executor instances.
participant_factories: Sequence of callables that return SupportsAgentRun or Executor instances.
Returns:
Self for method chaining
@@ -1420,10 +1420,10 @@ class MagenticBuilder:
self._participant_factories = list(participant_factories)
return self
def participants(self, participants: Sequence[AgentProtocol | Executor]) -> Self:
def participants(self, participants: Sequence[SupportsAgentRun | Executor]) -> Self:
"""Define participants for this Magentic workflow.
Accepts AgentProtocol instances (auto-wrapped as AgentExecutor) or Executor instances.
Accepts SupportsAgentRun instances (auto-wrapped as AgentExecutor) or Executor instances.
Args:
participants: Sequence of participant definitions
@@ -1434,7 +1434,7 @@ class MagenticBuilder:
Raises:
ValueError: If participants are empty, names are duplicated, or participants
or participant factories are already set
TypeError: If any participant is not AgentProtocol or Executor instance
TypeError: If any participant is not SupportsAgentRun or Executor instance
Example:
@@ -1462,17 +1462,17 @@ class MagenticBuilder:
raise ValueError("participants cannot be empty.")
# Name of the executor mapped to participant instance
named: dict[str, AgentProtocol | Executor] = {}
named: dict[str, SupportsAgentRun | Executor] = {}
for participant in participants:
if isinstance(participant, Executor):
identifier = participant.id
elif isinstance(participant, AgentProtocol):
elif isinstance(participant, SupportsAgentRun):
if not participant.name:
raise ValueError("AgentProtocol participants must have a non-empty name.")
raise ValueError("SupportsAgentRun participants must have a non-empty name.")
identifier = participant.name
else:
raise TypeError(
f"Participants must be AgentProtocol or Executor instances. Got {type(participant).__name__}."
f"Participants must be SupportsAgentRun or Executor instances. Got {type(participant).__name__}."
)
if identifier in named:
@@ -1608,7 +1608,7 @@ class MagenticBuilder:
def with_manager(
self,
*,
agent: AgentProtocol,
agent: SupportsAgentRun,
task_ledger: _MagenticTaskLedger | None = None,
# Prompt overrides
task_ledger_facts_prompt: str | None = None,
@@ -1628,7 +1628,7 @@ class MagenticBuilder:
This will create a StandardMagenticManager using the provided agent.
Args:
agent: AgentProtocol instance for the standard magentic manager
agent: SupportsAgentRun instance for the standard magentic manager
(`StandardMagenticManager`)
task_ledger: Optional custom task ledger implementation for specialized
prompting or structured output requirements
@@ -1661,7 +1661,7 @@ class MagenticBuilder:
def with_manager(
self,
*,
agent_factory: Callable[[], AgentProtocol],
agent_factory: Callable[[], SupportsAgentRun],
task_ledger: _MagenticTaskLedger | None = None,
# Prompt overrides
task_ledger_facts_prompt: str | None = None,
@@ -1681,7 +1681,7 @@ class MagenticBuilder:
This will create a StandardMagenticManager using the provided agent factory.
Args:
agent_factory: Callable that returns a new AgentProtocol instance for the standard
agent_factory: Callable that returns a new SupportsAgentRun instance for the standard
magentic manager (`StandardMagenticManager`)
task_ledger: Optional custom task ledger implementation for specialized
prompting or structured output requirements
@@ -1715,9 +1715,9 @@ class MagenticBuilder:
*,
manager: MagenticManagerBase | None = None,
manager_factory: Callable[[], MagenticManagerBase] | None = None,
agent_factory: Callable[[], AgentProtocol] | None = None,
agent_factory: Callable[[], SupportsAgentRun] | None = None,
# Constructor args for StandardMagenticManager when manager is not provided
agent: AgentProtocol | None = None,
agent: SupportsAgentRun | None = None,
task_ledger: _MagenticTaskLedger | None = None,
# Prompt overrides
task_ledger_facts_prompt: str | None = None,
@@ -1956,7 +1956,7 @@ class MagenticBuilder:
raise ValueError("No participants provided. Call .participants() or .register_participants() first.")
# We don't need to check if both are set since that is handled in the respective methods
participants: list[Executor | AgentProtocol] = []
participants: list[Executor | SupportsAgentRun] = []
if self._participant_factories:
for factory in self._participant_factories:
participant = factory()
@@ -1968,11 +1968,11 @@ class MagenticBuilder:
for participant in participants:
if isinstance(participant, Executor):
executors.append(participant)
elif isinstance(participant, AgentProtocol):
elif isinstance(participant, SupportsAgentRun):
executors.append(MagenticAgentExecutor(participant))
else:
raise TypeError(
f"Participants must be AgentProtocol or Executor instances. Got {type(participant).__name__}."
f"Participants must be SupportsAgentRun or Executor instances. Got {type(participant).__name__}."
)
return executors
@@ -2,7 +2,7 @@
from dataclasses import dataclass
from agent_framework._agents import AgentProtocol
from agent_framework._agents import SupportsAgentRun
from agent_framework._types import ChatMessage
from agent_framework._workflows._agent_executor import AgentExecutor, AgentExecutorRequest, AgentExecutorResponse
from agent_framework._workflows._agent_utils import resolve_agent_id
@@ -14,11 +14,11 @@ from agent_framework._workflows._workflow_context import WorkflowContext
from agent_framework._workflows._workflow_executor import WorkflowExecutor
def resolve_request_info_filter(agents: list[str | AgentProtocol] | None) -> set[str]:
def resolve_request_info_filter(agents: list[str | SupportsAgentRun] | None) -> set[str]:
"""Resolve a list of agent/executor references to a set of IDs for filtering.
Args:
agents: List of agent names (str), AgentProtocol instances, or Executor instances.
agents: List of agent names (str), SupportsAgentRun instances, or Executor instances.
If None, returns None (meaning no filtering - pause for all).
Returns:
@@ -31,7 +31,7 @@ def resolve_request_info_filter(agents: list[str | AgentProtocol] | None) -> set
for agent in agents:
if isinstance(agent, str):
result.add(agent)
elif isinstance(agent, AgentProtocol):
elif isinstance(agent, SupportsAgentRun):
result.add(resolve_agent_id(agent))
else:
raise TypeError(f"Unsupported type for request_info filter: {type(agent).__name__}")
@@ -117,7 +117,7 @@ class AgentApprovalExecutor(WorkflowExecutor):
agent's output or send the final response to down stream executors in the orchestration.
"""
def __init__(self, agent: AgentProtocol) -> None:
def __init__(self, agent: SupportsAgentRun) -> None:
"""Initialize the AgentApprovalExecutor.
Args:
@@ -126,7 +126,7 @@ class AgentApprovalExecutor(WorkflowExecutor):
super().__init__(workflow=self._build_workflow(agent), id=resolve_agent_id(agent), propagate_request=True)
self._description = agent.description
def _build_workflow(self, agent: AgentProtocol) -> Workflow:
def _build_workflow(self, agent: SupportsAgentRun) -> Workflow:
"""Build the internal workflow for the AgentApprovalExecutor."""
agent_executor = AgentExecutor(agent)
request_info_executor = AgentRequestInfoExecutor(id="agent_request_info_executor")
@@ -4,8 +4,8 @@
This module provides a high-level, agent-focused API to assemble a sequential
workflow where:
- Participants can be provided as AgentProtocol or Executor instances via `.participants()`,
or as factories returning AgentProtocol or Executor via `.register_participants()`
- Participants can be provided as SupportsAgentRun or Executor instances via `.participants()`,
or as factories returning SupportsAgentRun or Executor via `.register_participants()`
- A shared conversation context (list[ChatMessage]) is passed along the chain
- Agents append their assistant messages to the context
- Custom executors can transform or summarize and return a refined context
@@ -15,7 +15,7 @@ Typical wiring:
input -> _InputToConversation -> participant1 -> (agent? -> _ResponseToConversation) -> ... -> participantN -> _EndWithConversation
Notes:
- Participants can mix AgentProtocol and Executor objects
- Participants can mix SupportsAgentRun and Executor objects
- Agents are auto-wrapped by WorkflowBuilder as AgentExecutor (unless already wrapped)
- AgentExecutor produces AgentExecutorResponse; _ResponseToConversation converts this to list[ChatMessage]
- Non-agent executors must define a handler that consumes `list[ChatMessage]` and sends back
@@ -41,7 +41,7 @@ import logging
from collections.abc import Callable, Sequence
from typing import Any
from agent_framework import AgentProtocol, ChatMessage
from agent_framework import ChatMessage, SupportsAgentRun
from agent_framework._workflows._agent_executor import (
AgentExecutor,
AgentExecutorResponse,
@@ -109,8 +109,8 @@ class _EndWithConversation(Executor):
class SequentialBuilder:
r"""High-level builder for sequential agent/executor workflows with shared context.
- `participants([...])` accepts a list of AgentProtocol (recommended) or Executor instances
- `register_participants([...])` accepts a list of factories for AgentProtocol (recommended)
- `participants([...])` accepts a list of SupportsAgentRun (recommended) or Executor instances
- `register_participants([...])` accepts a list of factories for SupportsAgentRun (recommended)
or Executor factories
- Executors must define a handler that consumes list[ChatMessage] and sends out a list[ChatMessage]
- The workflow wires participants in order, passing a list[ChatMessage] down the chain
@@ -148,8 +148,8 @@ class SequentialBuilder:
"""
def __init__(self) -> None:
self._participants: list[AgentProtocol | Executor] = []
self._participant_factories: list[Callable[[], AgentProtocol | Executor]] = []
self._participants: list[SupportsAgentRun | Executor] = []
self._participant_factories: list[Callable[[], SupportsAgentRun | Executor]] = []
self._checkpoint_storage: CheckpointStorage | None = None
self._request_info_enabled: bool = False
self._request_info_filter: set[str] | None = None
@@ -157,7 +157,7 @@ class SequentialBuilder:
def register_participants(
self,
participant_factories: Sequence[Callable[[], AgentProtocol | Executor]],
participant_factories: Sequence[Callable[[], SupportsAgentRun | Executor]],
) -> "SequentialBuilder":
"""Register participant factories for this sequential workflow."""
if self._participants:
@@ -172,10 +172,10 @@ class SequentialBuilder:
self._participant_factories = list(participant_factories)
return self
def participants(self, participants: Sequence[AgentProtocol | Executor]) -> "SequentialBuilder":
def participants(self, participants: Sequence[SupportsAgentRun | Executor]) -> "SequentialBuilder":
"""Define the ordered participants for this sequential workflow.
Accepts AgentProtocol instances (auto-wrapped as AgentExecutor) or Executor instances.
Accepts SupportsAgentRun instances (auto-wrapped as AgentExecutor) or Executor instances.
Raises if empty or duplicates are provided for clarity.
"""
if self._participant_factories:
@@ -196,7 +196,7 @@ class SequentialBuilder:
raise ValueError(f"Duplicate executor participant detected: id '{p.id}'")
seen_executor_ids.add(p.id)
else:
# Treat non-Executor as agent-like (AgentProtocol). Structural checks can be brittle at runtime.
# Treat non-Executor as agent-like (SupportsAgentRun). Structural checks can be brittle at runtime.
pid = id(p)
if pid in seen_agent_ids:
raise ValueError("Duplicate agent participant detected (same agent instance provided twice)")
@@ -213,7 +213,7 @@ class SequentialBuilder:
def with_request_info(
self,
*,
agents: Sequence[str | AgentProtocol] | None = None,
agents: Sequence[str | SupportsAgentRun] | None = None,
) -> "SequentialBuilder":
"""Enable request info after agent participant responses.
@@ -262,7 +262,7 @@ class SequentialBuilder:
raise ValueError("No participants provided. Call .participants() or .register_participants() first.")
# We don't need to check if both are set since that is handled in the respective methods
participants: list[Executor | AgentProtocol] = []
participants: list[Executor | SupportsAgentRun] = []
if self._participant_factories:
# Resolve the participant factories now. This doesn't break the factory pattern
# since the Sequential builder still creates new instances per workflow build.
@@ -276,7 +276,7 @@ class SequentialBuilder:
for p in participants:
if isinstance(p, Executor):
executors.append(p)
elif isinstance(p, AgentProtocol):
elif isinstance(p, SupportsAgentRun):
if self._request_info_enabled and (
not self._request_info_filter or resolve_agent_id(p) in self._request_info_filter
):
@@ -285,7 +285,7 @@ class SequentialBuilder:
else:
executors.append(AgentExecutor(p))
else:
raise TypeError(f"Participants must be AgentProtocol or Executor instances. Got {type(p).__name__}.")
raise TypeError(f"Participants must be SupportsAgentRun or Executor instances. Got {type(p).__name__}.")
return executors
@@ -312,7 +312,7 @@ class SequentialBuilder:
builder.set_start_executor(input_conv)
# Start of the chain is the input normalizer
prior: Executor | AgentProtocol = input_conv
prior: Executor | SupportsAgentRun = input_conv
for p in participants:
builder.add_edge(prior, p)
prior = p
@@ -320,7 +320,7 @@ class TestGroupChatBuilder:
builder = GroupChatBuilder().with_orchestrator(selection_func=selector)
with pytest.raises(ValueError, match="AgentProtocol participants must have a non-empty name"):
with pytest.raises(ValueError, match="SupportsAgentRun participants must have a non-empty name"):
builder.participants([agent])
def test_empty_participant_name_raises_error(self) -> None:
@@ -332,7 +332,7 @@ class TestGroupChatBuilder:
builder = GroupChatBuilder().with_orchestrator(selection_func=selector)
with pytest.raises(ValueError, match="AgentProtocol participants must have a non-empty name"):
with pytest.raises(ValueError, match="SupportsAgentRun participants must have a non-empty name"):
builder.participants([agent])
@@ -420,7 +420,7 @@ def test_handoff_builder_rejects_mixed_types_in_add_handoff_source():
triage = MockHandoffAgent(name="triage")
specialist = MockHandoffAgent(name="specialist")
with pytest.raises(TypeError, match="Cannot mix factory names \\(str\\) and AgentProtocol.*instances"):
with pytest.raises(TypeError, match="Cannot mix factory names \\(str\\) and SupportsAgentRun.*instances"):
(
HandoffBuilder(participants=[triage, specialist])
.with_start_agent(triage)
@@ -7,7 +7,6 @@ from typing import Any, ClassVar, cast
import pytest
from agent_framework import (
AgentProtocol,
AgentResponse,
AgentResponseUpdate,
AgentThread,
@@ -15,6 +14,7 @@ from agent_framework import (
ChatMessage,
Content,
Executor,
SupportsAgentRun,
Workflow,
WorkflowCheckpoint,
WorkflowCheckpointException,
@@ -576,7 +576,7 @@ class StubAssistantsAgent(BaseAgent):
)
async def _collect_agent_responses_setup(participant: AgentProtocol) -> list[ChatMessage]:
async def _collect_agent_responses_setup(participant: SupportsAgentRun) -> list[ChatMessage]:
captured: list[ChatMessage] = []
wf = (
@@ -1121,10 +1121,10 @@ async def test_magentic_with_agent_factory():
"""Test workflow creation using agent_factory for StandardMagenticManager."""
factory_call_count = 0
def agent_factory() -> AgentProtocol:
def agent_factory() -> SupportsAgentRun:
nonlocal factory_call_count
factory_call_count += 1
return cast(AgentProtocol, StubManagerAgent())
return cast(SupportsAgentRun, StubManagerAgent())
participant = StubAgent("agentA", "reply from agentA")
workflow = (
@@ -1239,10 +1239,10 @@ def test_magentic_agent_factory_with_standard_manager_options():
"""Test that agent_factory properly passes through standard manager options."""
factory_call_count = 0
def agent_factory() -> AgentProtocol:
def agent_factory() -> SupportsAgentRun:
nonlocal factory_call_count
factory_call_count += 1
return cast(AgentProtocol, StubManagerAgent())
return cast(SupportsAgentRun, StubManagerAgent())
# Custom options to verify they are passed through
custom_max_stall_count = 5
@@ -8,11 +8,11 @@ from unittest.mock import AsyncMock, MagicMock
import pytest
from agent_framework import (
AgentProtocol,
AgentResponse,
AgentResponseUpdate,
AgentThread,
ChatMessage,
SupportsAgentRun,
)
from agent_framework._workflows._agent_executor import AgentExecutorRequest, AgentExecutorResponse
from agent_framework._workflows._workflow_context import WorkflowContext
@@ -44,10 +44,10 @@ class TestResolveRequestInfoFilter:
assert result == {"agent1", "agent2"}
def test_resolves_agent_display_names(self):
"""Test resolving AgentProtocol instances by name attribute."""
agent1 = MagicMock(spec=AgentProtocol)
"""Test resolving SupportsAgentRun instances by name attribute."""
agent1 = MagicMock(spec=SupportsAgentRun)
agent1.name = "writer"
agent2 = MagicMock(spec=AgentProtocol)
agent2 = MagicMock(spec=SupportsAgentRun)
agent2.name = "reviewer"
result = resolve_request_info_filter([agent1, agent2])
@@ -55,7 +55,7 @@ class TestResolveRequestInfoFilter:
def test_mixed_types(self):
"""Test resolving a mix of strings and agents."""
agent = MagicMock(spec=AgentProtocol)
agent = MagicMock(spec=SupportsAgentRun)
agent.name = "writer"
result = resolve_request_info_filter(["manual_name", agent])