[BREAKING] Python: Move single-config fluent methods to constructor parameters (#3693)

* Move single-config fluent methods to constructor parameters

* Updates

* Adjust magentic and group chat
This commit is contained in:
Evan Mattson
2026-02-07 15:01:52 +09:00
committed by GitHub
Unverified
parent 5d355ac507
commit 74ac470a56
132 changed files with 1341 additions and 2386 deletions
+8 -12
View File
@@ -52,12 +52,10 @@ Orchestrator-directed multi-agent conversations:
```python
from agent_framework_orchestrations import GroupChatBuilder
workflow = (
GroupChatBuilder()
.with_orchestrator(selection_func=my_selector)
.participants([agent1, agent2])
.build()
)
workflow = GroupChatBuilder(
participants=[agent1, agent2],
selection_func=my_selector,
).build()
```
### MagenticBuilder
@@ -67,12 +65,10 @@ Sophisticated multi-agent orchestration using the Magentic One pattern:
```python
from agent_framework_orchestrations import MagenticBuilder
workflow = (
MagenticBuilder()
.participants([researcher, writer, reviewer])
.with_manager(agent=manager_agent)
.build()
)
workflow = MagenticBuilder(
participants=[researcher, writer, reviewer],
manager_agent=manager_agent,
).build()
```
## Usage with agent_framework
@@ -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 SupportsAgentRun or Executor instances via `.participants()`,
or as factories returning SupportsAgentRun or Executor via `.register_participants()`.
- Participants can be provided as SupportsAgentRun or Executor instances via `participants=[...]`,
or as factories returning SupportsAgentRun or Executor via `participant_factories=[...]`.
- 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 SupportsAgentRun (recommended) or Executor.
- `register_participants([...])` accepts a list of factories for SupportsAgentRun (recommended)
- `participants=[...]` accepts a list of SupportsAgentRun (recommended) or Executor.
- `participant_factories=[...]` 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.
@@ -200,10 +200,10 @@ class ConcurrentBuilder:
from agent_framework_orchestrations import ConcurrentBuilder
# Minimal: use default aggregator (returns list[ChatMessage])
workflow = ConcurrentBuilder().participants([agent1, agent2, agent3]).build()
workflow = ConcurrentBuilder(participants=[agent1, agent2, agent3]).build()
# With agent factories
workflow = ConcurrentBuilder().register_participants([create_agent1, create_agent2, create_agent3]).build()
workflow = ConcurrentBuilder(participant_factories=[create_agent1, create_agent2, create_agent3]).build()
# Custom aggregator via callback (sync or async). The callback receives
@@ -212,7 +212,7 @@ class ConcurrentBuilder:
return " | ".join(r.agent_response.messages[-1].text for r in results)
workflow = ConcurrentBuilder().participants([agent1, agent2, agent3]).with_aggregator(summarize).build()
workflow = ConcurrentBuilder(participants=[agent1, agent2, agent3]).with_aggregator(summarize).build()
# Custom aggregator via a factory
@@ -223,112 +223,76 @@ class ConcurrentBuilder:
workflow = (
ConcurrentBuilder()
.register_participants([create_agent1, create_agent2, create_agent3])
ConcurrentBuilder(participant_factories=[create_agent1, create_agent2, create_agent3])
.register_aggregator(lambda: MyAggregator(id="my_aggregator"))
.build()
)
# Enable checkpoint persistence so runs can resume
workflow = ConcurrentBuilder().participants([agent1, agent2, agent3]).with_checkpointing(storage).build()
workflow = ConcurrentBuilder(participants=[agent1, agent2, agent3], checkpoint_storage=storage).build()
# Enable request info before aggregation
workflow = ConcurrentBuilder().participants([agent1, agent2]).with_request_info().build()
workflow = ConcurrentBuilder(participants=[agent1, agent2]).with_request_info().build()
"""
def __init__(self) -> None:
def __init__(
self,
*,
participants: Sequence[SupportsAgentRun | Executor] | None = None,
participant_factories: Sequence[Callable[[], SupportsAgentRun | Executor]] | None = None,
checkpoint_storage: CheckpointStorage | None = None,
intermediate_outputs: bool = False,
) -> None:
"""Initialize the ConcurrentBuilder.
Args:
participants: Optional sequence of agent or executor instances to run in parallel.
participant_factories: Optional sequence of callables returning agent or executor instances.
checkpoint_storage: Optional checkpoint storage for enabling workflow state persistence.
intermediate_outputs: If True, enables intermediate outputs from agent participants
before aggregation.
"""
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
self._checkpoint_storage: CheckpointStorage | None = checkpoint_storage
self._request_info_enabled: bool = False
self._request_info_filter: set[str] | None = None
self._intermediate_outputs: bool = False
self._intermediate_outputs: bool = intermediate_outputs
def register_participants(
if participants is None and participant_factories is None:
raise ValueError("Either participants or participant_factories must be provided.")
if participant_factories is not None:
self._set_participant_factories(participant_factories)
if participants is not None:
self._set_participants(participants)
def _set_participant_factories(
self,
participant_factories: Sequence[Callable[[], SupportsAgentRun | Executor]],
) -> "ConcurrentBuilder":
r"""Define the parallel participants for this concurrent workflow.
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 SupportsAgentRun or Executor instances
Raises:
ValueError: if `participant_factories` is empty or `.participants()`
or `.register_participants()` were already called
Example:
.. code-block:: python
def create_researcher() -> ChatAgent:
return ...
def create_marketer() -> ChatAgent:
return ...
def create_legal() -> ChatAgent:
return ...
class MyCustomExecutor(Executor): ...
wf = ConcurrentBuilder().register_participants([create_researcher, create_marketer, create_legal]).build()
# Mixing agent(s) and executor(s) is supported
wf2 = ConcurrentBuilder().register_participants([create_researcher, MyCustomExecutor]).build()
"""
) -> None:
"""Set participant factories (internal)."""
if self._participants:
raise ValueError("Cannot mix .participants() and .register_participants() in the same builder instance.")
raise ValueError("Cannot provide both participants and participant_factories.")
if self._participant_factories:
raise ValueError("register_participants() has already been called on this builder instance.")
raise ValueError("participant_factories already set.")
if not participant_factories:
raise ValueError("participant_factories cannot be empty")
self._participant_factories = list(participant_factories)
return self
def participants(self, participants: Sequence[SupportsAgentRun | Executor]) -> "ConcurrentBuilder":
r"""Define the parallel participants for this concurrent workflow.
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 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 SupportsAgentRun or Executor
Example:
.. code-block:: python
wf = ConcurrentBuilder().participants([researcher_agent, marketer_agent, legal_agent]).build()
# Mixing agent(s) and executor(s) is supported
wf2 = ConcurrentBuilder().participants([researcher_agent, my_custom_executor]).build()
"""
def _set_participants(self, participants: Sequence[SupportsAgentRun | Executor]) -> None:
"""Set participants (internal)."""
if self._participant_factories:
raise ValueError("Cannot mix .participants() and .register_participants() in the same builder instance.")
raise ValueError("Cannot provide both participants and participant_factories.")
if self._participants:
raise ValueError("participants() has already been called on this builder instance.")
raise ValueError("participants already set.")
if not participants:
raise ValueError("participants cannot be empty")
@@ -350,7 +314,6 @@ class ConcurrentBuilder:
raise TypeError(f"participants must be SupportsAgentRun or Executor instances; got {type(p).__name__}")
self._participants = list(participants)
return self
def register_aggregator(self, aggregator_factory: Callable[[], Executor]) -> "ConcurrentBuilder":
r"""Define a custom aggregator for this concurrent workflow.
@@ -412,7 +375,7 @@ class ConcurrentBuilder:
await ctx.yield_output(" | ".join(r.agent_response.messages[-1].text for r in results))
wf = ConcurrentBuilder().participants([a1, a2, a3]).with_aggregator(CustomAggregator()).build()
wf = ConcurrentBuilder(participants=[a1, a2, a3]).with_aggregator(CustomAggregator()).build()
# Callback-based aggregator (string result)
@@ -420,7 +383,7 @@ class ConcurrentBuilder:
return " | ".join(r.agent_response.messages[-1].text for r in results)
wf = ConcurrentBuilder().participants([a1, a2, a3]).with_aggregator(summarize).build()
wf = ConcurrentBuilder(participants=[a1, a2, a3]).with_aggregator(summarize).build()
# Callback-based aggregator (yield result)
@@ -428,7 +391,7 @@ class ConcurrentBuilder:
await ctx.yield_output(" | ".join(r.agent_response.messages[-1].text for r in results))
wf = ConcurrentBuilder().participants([a1, a2, a3]).with_aggregator(summarize).build()
wf = ConcurrentBuilder(participants=[a1, a2, a3]).with_aggregator(summarize).build()
"""
if self._aggregator_factory is not None:
raise ValueError(
@@ -447,15 +410,6 @@ class ConcurrentBuilder:
return self
def with_checkpointing(self, checkpoint_storage: CheckpointStorage) -> "ConcurrentBuilder":
"""Enable checkpoint persistence using the provided storage backend.
Args:
checkpoint_storage: CheckpointStorage instance for persisting workflow state
"""
self._checkpoint_storage = checkpoint_storage
return self
def with_request_info(
self,
*,
@@ -489,23 +443,10 @@ class ConcurrentBuilder:
return self
def with_intermediate_outputs(self) -> "ConcurrentBuilder":
"""Enable intermediate outputs from agent participants before aggregation.
When enabled, the workflow returns each agent participant's response or yields
streaming updates as they become available. The output of the aggregator will
always be available as the final output of the workflow.
Returns:
Self for fluent chaining
"""
self._intermediate_outputs = True
return self
def _resolve_participants(self) -> list[Executor]:
"""Resolve participant instances into Executor objects."""
if not self._participants and not self._participant_factories:
raise ValueError("No participants provided. Call .participants() or .register_participants() first.")
raise ValueError("No participants provided. Pass participants or participant_factories to the constructor.")
# We don't need to check if both are set since that is handled in the respective methods
participants: list[Executor | SupportsAgentRun] = []
@@ -557,7 +498,7 @@ class ConcurrentBuilder:
.. code-block:: python
workflow = ConcurrentBuilder().participants([agent1, agent2]).build()
workflow = ConcurrentBuilder(participants=[agent1, agent2]).build()
"""
# Internal nodes
dispatcher = _DispatchToAllParticipants(id="dispatcher")
@@ -574,18 +515,14 @@ class ConcurrentBuilder:
# Resolve participants and participant factories to executors
participants: list[Executor] = self._resolve_participants()
builder = WorkflowBuilder()
builder.set_start_executor(dispatcher)
builder = WorkflowBuilder(
start_executor=dispatcher,
checkpoint_storage=self._checkpoint_storage,
output_executors=[aggregator] if not self._intermediate_outputs else None,
)
# Fan-out for parallel execution
builder.add_fan_out_edges(dispatcher, participants)
# Direct fan-in to aggregator
builder.add_fan_in_edges(participants, aggregator)
if not self._intermediate_outputs:
# Constrain output to aggregator only
builder = builder.with_output_from([aggregator])
if self._checkpoint_storage is not None:
builder = builder.with_checkpointing(self._checkpoint_storage)
return builder.build()
@@ -24,7 +24,7 @@ import sys
from collections import OrderedDict
from collections.abc import Awaitable, Callable, Sequence
from dataclasses import dataclass
from typing import Any, ClassVar, cast, overload
from typing import Any, ClassVar, cast
from agent_framework import ChatAgent, SupportsAgentRun
from agent_framework._threads import AgentThread
@@ -521,8 +521,39 @@ class GroupChatBuilder:
DEFAULT_ORCHESTRATOR_ID: ClassVar[str] = "group_chat_orchestrator"
def __init__(self) -> None:
"""Initialize the GroupChatBuilder."""
def __init__(
self,
*,
participants: Sequence[SupportsAgentRun | Executor] | None = None,
participant_factories: Sequence[Callable[[], SupportsAgentRun | Executor]] | None = None,
# Orchestrator config (exactly one required)
orchestrator_agent: ChatAgent | Callable[[], ChatAgent] | None = None,
orchestrator: BaseGroupChatOrchestrator | Callable[[], BaseGroupChatOrchestrator] | None = None,
selection_func: GroupChatSelectionFunction | None = None,
orchestrator_name: str | None = None,
# Existing params
termination_condition: TerminationCondition | None = None,
max_rounds: int | None = None,
checkpoint_storage: CheckpointStorage | None = None,
intermediate_outputs: bool = False,
) -> None:
"""Initialize the GroupChatBuilder.
Args:
participants: Optional sequence of agent or executor instances for the group chat.
participant_factories: Optional sequence of callables returning agent or executor instances.
orchestrator_agent: An instance of ChatAgent or a callable that produces one to manage the group chat.
orchestrator: An instance of BaseGroupChatOrchestrator or a callable that produces one to manage the
group chat.
selection_func: Callable that receives the current GroupChatState and returns the name of the next
participant to speak.
orchestrator_name: Optional display name for the orchestrator when using a selection function.
termination_condition: Optional callable that receives the conversation history and returns
True to terminate the conversation, False to continue.
max_rounds: Optional maximum number of orchestrator rounds to prevent infinite conversations.
checkpoint_storage: Optional checkpoint storage for enabling workflow state persistence.
intermediate_outputs: If True, enables intermediate outputs from agent participants.
"""
self._participants: dict[str, SupportsAgentRun | Executor] = {}
self._participant_factories: list[Callable[[], SupportsAgentRun | Executor]] = []
@@ -531,96 +562,49 @@ class GroupChatBuilder:
self._orchestrator_factory: Callable[[], ChatAgent | BaseGroupChatOrchestrator] | None = None
self._selection_func: GroupChatSelectionFunction | None = None
self._agent_orchestrator: ChatAgent | None = None
self._termination_condition: TerminationCondition | None = None
self._max_rounds: int | None = None
self._termination_condition: TerminationCondition | None = termination_condition
self._max_rounds: int | None = max_rounds
self._orchestrator_name: str | None = None
# Checkpoint related members
self._checkpoint_storage: CheckpointStorage | None = None
self._checkpoint_storage: CheckpointStorage | None = checkpoint_storage
# Request info related members
self._request_info_enabled: bool = False
self._request_info_filter: set[str] = set()
# Intermediate outputs
self._intermediate_outputs = False
self._intermediate_outputs = intermediate_outputs
@overload
def with_orchestrator(self, *, agent: ChatAgent | Callable[[], ChatAgent]) -> "GroupChatBuilder":
"""Set the orchestrator for this group chat workflow using a ChatAgent.
if participants is None and participant_factories is None:
raise ValueError("Either participants or participant_factories must be provided.")
Args:
agent: An instance of ChatAgent or a callable that produces one to manage the group chat.
if participant_factories is not None:
self._set_participant_factories(participant_factories)
if participants is not None:
self._set_participants(participants)
Returns:
Self for fluent chaining.
"""
...
# Set orchestrator if provided
if any(x is not None for x in [orchestrator_agent, orchestrator, selection_func]):
self._set_orchestrator(
orchestrator_agent=orchestrator_agent,
orchestrator=orchestrator,
selection_func=selection_func,
orchestrator_name=orchestrator_name,
)
@overload
def with_orchestrator(
self, *, orchestrator: BaseGroupChatOrchestrator | Callable[[], BaseGroupChatOrchestrator]
) -> "GroupChatBuilder":
"""Set the orchestrator for this group chat workflow using a custom orchestrator.
Args:
orchestrator: An instance of BaseGroupChatOrchestrator or a callable that produces one to
manage the group chat.
Returns:
Self for fluent chaining.
Note:
When using a custom orchestrator that implements `BaseGroupChatOrchestrator`, setting
`termination_condition` and `max_rounds` on the builder will have no effect since the
orchestrator is already fully defined.
"""
...
@overload
def with_orchestrator(
def _set_orchestrator(
self,
*,
selection_func: GroupChatSelectionFunction,
orchestrator_name: str | None = None,
) -> "GroupChatBuilder":
"""Set the orchestrator for this group chat workflow using a selection function.
Args:
selection_func: Callable that receives the current GroupChatState and returns
the name of the next participant to speak, or None to finish.
orchestrator_name: Optional display name for the orchestrator in the workflow.
If not provided, defaults to `GroupChatBuilder.DEFAULT_ORCHESTRATOR_ID`.
Returns:
Self for fluent chaining.
"""
...
def with_orchestrator(
self,
*,
agent: ChatAgent | Callable[[], ChatAgent] | None = None,
orchestrator_agent: ChatAgent | Callable[[], ChatAgent] | None = None,
orchestrator: BaseGroupChatOrchestrator | Callable[[], BaseGroupChatOrchestrator] | None = None,
selection_func: GroupChatSelectionFunction | None = None,
orchestrator_name: str | None = None,
) -> "GroupChatBuilder":
"""Set the orchestrator for this group chat workflow.
An group chat orchestrator is responsible for managing the flow of conversation, making
sure all participants are synced and picking the next speaker according to the defined logic
until the termination conditions are met.
There are a few ways to configure the orchestrator:
1. Provide a ChatAgent instance or a factory function that produces one to use an agent-based orchestrator
2. Provide a BaseGroupChatOrchestrator instance or a factory function that produces one to use a custom
orchestrator
3. Provide a selection function to use that picks the next speaker based on the function logic
You can only use one of the above methods to configure the orchestrator.
) -> None:
"""Set the orchestrator for this group chat workflow (internal).
Args:
agent: An instance of ChatAgent or a callable that produces one to manage the group chat.
orchestrator_agent: An instance of ChatAgent or a callable that produces one to manage the group chat.
orchestrator: An instance of BaseGroupChatOrchestrator or a callable that produces one to manage the group
chat.
selection_func: Callable that receives the current GroupChatState and returns
@@ -630,121 +614,58 @@ class GroupChatBuilder:
`GroupChatBuilder.DEFAULT_ORCHESTRATOR_ID`. This parameter is
ignored if using an agent or custom orchestrator.
Returns:
Self for fluent chaining.
Raises:
ValueError: If an orchestrator has already been set or if none or multiple
of the parameters are provided.
Note:
When using a custom orchestrator that implements `BaseGroupChatOrchestrator`, either
via the `orchestrator` or `orchestrator_factory` parameters, setting `termination_condition`
and `max_rounds` on the builder will have no effect since the orchestrator is already
fully defined.
Example:
.. code-block:: python
from agent_framework_orchestrations import GroupChatBuilder
orchestrator = CustomGroupChatOrchestrator(...)
workflow = GroupChatBuilder().with_orchestrator(orchestrator).participants([agent1, agent2]).build()
"""
if self._agent_orchestrator is not None:
raise ValueError(
"An agent orchestrator has already been configured. Call with_orchestrator(...) once only."
)
raise ValueError("An agent orchestrator has already been configured. Set orchestrator config once only.")
if self._orchestrator is not None:
raise ValueError("An orchestrator has already been configured. Call with_orchestrator(...) once only.")
raise ValueError("An orchestrator has already been configured. Set orchestrator config once only.")
if self._orchestrator_factory is not None:
raise ValueError("A factory has already been configured. Call with_orchestrator(...) once only.")
raise ValueError("A factory has already been configured. Set orchestrator config once only.")
if self._selection_func is not None:
raise ValueError("A selection function has already been configured. Call with_orchestrator(...) once only.")
raise ValueError("A selection function has already been configured. Set orchestrator config once only.")
if sum(x is not None for x in [agent, orchestrator, selection_func]) != 1:
raise ValueError("Exactly one of agent, orchestrator, or selection_func must be provided.")
if sum(x is not None for x in [orchestrator_agent, orchestrator, selection_func]) != 1:
raise ValueError("Exactly one of orchestrator_agent, orchestrator, or selection_func must be provided.")
if agent is not None and isinstance(agent, ChatAgent):
self._agent_orchestrator = agent
if orchestrator_agent is not None and isinstance(orchestrator_agent, ChatAgent):
self._agent_orchestrator = orchestrator_agent
elif orchestrator is not None and isinstance(orchestrator, BaseGroupChatOrchestrator):
self._orchestrator = orchestrator
elif selection_func is not None:
self._selection_func = selection_func
self._orchestrator_name = orchestrator_name
else:
self._orchestrator_factory = agent or orchestrator
self._orchestrator_factory = orchestrator_agent or orchestrator
return self
def register_participants(
def _set_participant_factories(
self,
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 SupportsAgentRun instance
(auto-wrapped as AgentExecutor) or an Executor instance.
Returns:
Self for fluent chaining
Raises:
ValueError: If participant_factories is empty, or participants
or participant factories are already set
"""
) -> None:
"""Set participant factories (internal)."""
if self._participants:
raise ValueError("Cannot mix .participants() and .register_participants() in the same builder instance.")
raise ValueError("Cannot provide both participants and participant_factories.")
if self._participant_factories:
raise ValueError("register_participants() has already been called on this builder instance.")
raise ValueError("participant_factories already set.")
if not participant_factories:
raise ValueError("participant_factories cannot be empty")
self._participant_factories = list(participant_factories)
return self
def participants(self, participants: Sequence[SupportsAgentRun | Executor]) -> "GroupChatBuilder":
"""Define participants for this group chat workflow.
Accepts SupportsAgentRun instances (auto-wrapped as AgentExecutor) or Executor instances.
Args:
participants: Sequence of participant definitions
Returns:
Self for fluent chaining
Raises:
ValueError: If participants are empty, names are duplicated, or participants
or participant factories are already set
TypeError: If any participant is not SupportsAgentRun or Executor instance
Example:
.. code-block:: python
from agent_framework_orchestrations import GroupChatBuilder
workflow = (
GroupChatBuilder()
.with_orchestrator(selection_func=my_selection_function)
.participants([agent1, agent2, custom_executor])
.build()
)
"""
def _set_participants(self, participants: Sequence[SupportsAgentRun | Executor]) -> None:
"""Set participants (internal)."""
if self._participant_factories:
raise ValueError("Cannot mix .participants() and .register_participants() in the same builder instance.")
raise ValueError("Cannot provide both participants and participant_factories.")
if self._participants:
raise ValueError("participants have already been set. Call participants() at most once.")
raise ValueError("participants already set.")
if not participants:
raise ValueError("participants cannot be empty.")
@@ -770,8 +691,6 @@ class GroupChatBuilder:
self._participants = named
return self
def with_termination_condition(self, termination_condition: TerminationCondition) -> "GroupChatBuilder":
"""Set a custom termination condition for the group chat workflow.
@@ -797,9 +716,10 @@ class GroupChatBuilder:
specialist_agent = ...
workflow = (
GroupChatBuilder()
.with_orchestrator(selection_func=my_selection_function)
.participants([agent1, specialist_agent])
GroupChatBuilder(
participants=[agent1, specialist_agent],
selection_func=my_selection_function,
)
.with_termination_condition(stop_after_two_calls)
.build()
)
@@ -851,9 +771,10 @@ class GroupChatBuilder:
storage = MemoryCheckpointStorage()
workflow = (
GroupChatBuilder()
.with_orchestrator(selection_func=my_selection_function)
.participants([agent1, agent2])
GroupChatBuilder(
participants=[agent1, agent2],
selection_func=my_selection_function,
)
.with_checkpointing(storage)
.build()
)
@@ -890,19 +811,6 @@ class GroupChatBuilder:
return self
def with_intermediate_outputs(self) -> "GroupChatBuilder":
"""Enable intermediate outputs from agent participants.
When enabled, the workflow returns each agent participant's response or yields
streaming updates as they become available. The output of the orchestrator will
always be available as the final output of the workflow.
Returns:
Self for fluent chaining
"""
self._intermediate_outputs = True
return self
def _resolve_orchestrator(self, participants: Sequence[Executor]) -> Executor:
"""Determine the orchestrator to use for the workflow.
@@ -913,8 +821,11 @@ class GroupChatBuilder:
x is None
for x in [self._agent_orchestrator, self._selection_func, self._orchestrator, self._orchestrator_factory]
):
raise ValueError("No orchestrator has been configured. Call with_orchestrator() to set one.")
# We don't need to check if multiple are set since that is handled in with_orchestrator()
raise ValueError(
"No orchestrator has been configured. "
"Pass orchestrator_agent, orchestrator, or selection_func to the constructor."
)
# We don't need to check if multiple are set since that is handled in _set_orchestrator()
if self._agent_orchestrator:
return AgentBasedGroupChatOrchestrator(
@@ -954,12 +865,15 @@ class GroupChatBuilder:
)
# This should never be reached due to the checks above
raise RuntimeError("Orchestrator could not be resolved. Please provide one via with_orchestrator()")
raise RuntimeError(
"Orchestrator could not be resolved. "
"Pass orchestrator_agent, orchestrator, or selection_func to the constructor."
)
def _resolve_participants(self) -> list[Executor]:
"""Resolve participant instances into Executor objects."""
if not self._participants and not self._participant_factories:
raise ValueError("No participants provided. Call .participants() or .register_participants() first.")
raise ValueError("No participants provided. Pass participants or participant_factories to the constructor.")
# We don't need to check if both are set since that is handled in the respective methods
participants: list[Executor | SupportsAgentRun] = []
@@ -1004,19 +918,16 @@ class GroupChatBuilder:
orchestrator: Executor = self._resolve_orchestrator(participants)
# Build workflow graph
workflow_builder = WorkflowBuilder().set_start_executor(orchestrator)
workflow_builder = WorkflowBuilder(
start_executor=orchestrator,
checkpoint_storage=self._checkpoint_storage,
output_executors=[orchestrator] if not self._intermediate_outputs else None,
)
for participant in participants:
# Orchestrator and participant bi-directional edges
workflow_builder = workflow_builder.add_edge(orchestrator, participant)
workflow_builder = workflow_builder.add_edge(participant, orchestrator)
if not self._intermediate_outputs:
# Constrain output to orchestrator only
workflow_builder = workflow_builder.with_output_from([orchestrator])
if self._checkpoint_storage is not None:
workflow_builder = workflow_builder.with_checkpointing(self._checkpoint_storage)
return workflow_builder.build()
@@ -577,6 +577,8 @@ class HandoffBuilder:
participants: Sequence[SupportsAgentRun] | None = None,
participant_factories: Mapping[str, Callable[[], SupportsAgentRun]] | None = None,
description: str | None = None,
checkpoint_storage: CheckpointStorage | None = None,
termination_condition: TerminationCondition | None = None,
) -> None:
r"""Initialize a HandoffBuilder for creating conversational handoff workflows.
@@ -599,6 +601,9 @@ class HandoffBuilder:
created by this builder.
description: Optional human-readable description explaining the workflow's
purpose. Useful for documentation and observability.
checkpoint_storage: Optional checkpoint storage for enabling workflow state persistence.
termination_condition: Optional callable that receives the full conversation and returns True
(or awaitable True) if the workflow should terminate.
"""
self._name = name
self._description = description
@@ -617,7 +622,7 @@ class HandoffBuilder:
self._handoff_config: dict[str, set[HandoffConfiguration]] = {}
# Checkpoint related members
self._checkpoint_storage: CheckpointStorage | None = None
self._checkpoint_storage: CheckpointStorage | None = checkpoint_storage
# Autonomous mode related
self._autonomous_mode: bool = False
@@ -626,7 +631,9 @@ class HandoffBuilder:
self._autonomous_mode_enabled_agents: list[str] = []
# Termination related members
self._termination_condition: Callable[[list[ChatMessage]], bool | Awaitable[bool]] | None = None
self._termination_condition: Callable[[list[ChatMessage]], bool | Awaitable[bool]] | None = (
termination_condition
)
def register_participants(
self, participant_factories: Mapping[str, Callable[[], SupportsAgentRun]]
@@ -1060,7 +1067,9 @@ class HandoffBuilder:
builder = WorkflowBuilder(
name=self._name,
description=self._description,
).set_start_executor(start_executor)
start_executor=start_executor,
checkpoint_storage=self._checkpoint_storage,
)
# Add the appropriate edges
# In handoff workflows, all executors are connected, making a fully connected graph.
@@ -1076,10 +1085,6 @@ class HandoffBuilder:
elif len(targets) == 1:
builder = builder.add_edge(executor, targets[0])
# Configure checkpointing if enabled
if self._checkpoint_storage:
builder.with_checkpointing(self._checkpoint_storage)
return builder.build()
# region Internal Helper Methods
@@ -10,7 +10,7 @@ from abc import ABC, abstractmethod
from collections.abc import Callable, Sequence
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, ClassVar, TypeVar, cast, overload
from typing import Any, ClassVar, TypeVar, cast
from agent_framework import (
AgentResponse,
@@ -41,10 +41,6 @@ if sys.version_info >= (3, 12):
from typing import override # type: ignore # pragma: no cover
else:
from typing_extensions import override # type: ignore # pragma: no cover
if sys.version_info >= (3, 11):
from typing import Self # type: ignore # pragma: no cover
else:
from typing_extensions import Self # type: ignore # pragma: no cover
logger = logging.getLogger(__name__)
@@ -1366,7 +1362,7 @@ class MagenticBuilder:
Human-in-the-loop Support:
Magentic provides specialized HITL mechanisms via:
- `.with_plan_review()` - Review and approve/revise plans before execution
- `enable_plan_review=True` - Review and approve/revise plans before execution
- `.with_human_input_on_stall()` - Intervene when workflow stalls
- Tool approval via `function_approval_request` - Approve individual tool calls
@@ -1375,8 +1371,57 @@ class MagenticBuilder:
for Magentic's planning-based orchestration.
"""
def __init__(self) -> None:
"""Initialize the Magentic workflow builder."""
def __init__(
self,
*,
participants: Sequence[SupportsAgentRun | Executor] | None = None,
participant_factories: Sequence[Callable[[], SupportsAgentRun | Executor]] | None = None,
# Manager config (exactly one required)
manager: MagenticManagerBase | None = None,
manager_factory: Callable[[], MagenticManagerBase] | None = None,
manager_agent: SupportsAgentRun | None = None,
manager_agent_factory: Callable[[], SupportsAgentRun] | None = None,
# StandardMagenticManager options (used with manager_agent/manager_agent_factory)
task_ledger: _MagenticTaskLedger | None = None,
task_ledger_facts_prompt: str | None = None,
task_ledger_plan_prompt: str | None = None,
task_ledger_full_prompt: str | None = None,
task_ledger_facts_update_prompt: str | None = None,
task_ledger_plan_update_prompt: str | None = None,
progress_ledger_prompt: str | None = None,
final_answer_prompt: str | None = None,
max_stall_count: int = 3,
max_reset_count: int | None = None,
max_round_count: int | None = None,
# Existing params
enable_plan_review: bool = False,
checkpoint_storage: CheckpointStorage | None = None,
intermediate_outputs: bool = False,
) -> None:
"""Initialize the Magentic workflow builder.
Args:
participants: Optional sequence of agent or executor instances for the workflow.
participant_factories: Optional sequence of callables returning agent or executor instances.
manager: Pre-configured manager instance (subclass of MagenticManagerBase).
manager_factory: Callable that returns a new MagenticManagerBase instance.
manager_agent: Agent instance for creating a StandardMagenticManager.
manager_agent_factory: Callable that returns a new agent instance for creating a StandardMagenticManager.
task_ledger: Optional custom task ledger (used with manager_agent/manager_agent_factory).
task_ledger_facts_prompt: Custom prompt for extracting facts.
task_ledger_plan_prompt: Custom prompt for generating initial plan.
task_ledger_full_prompt: Custom prompt for complete task ledger.
task_ledger_facts_update_prompt: Custom prompt for updating facts.
task_ledger_plan_update_prompt: Custom prompt for replanning.
progress_ledger_prompt: Custom prompt for assessing progress.
final_answer_prompt: Custom prompt for synthesizing final response.
max_stall_count: Max consecutive rounds without progress before replan (default 3).
max_reset_count: Max number of resets allowed. None means unlimited.
max_round_count: Max total coordination rounds. None means unlimited.
enable_plan_review: If True, requires human approval of the initial plan before proceeding.
checkpoint_storage: Optional checkpoint storage for enabling workflow state persistence.
intermediate_outputs: If True, enables intermediate outputs from agent participants.
"""
self._participants: dict[str, SupportsAgentRun | Executor] = {}
self._participant_factories: list[Callable[[], SupportsAgentRun | Executor]] = []
@@ -1385,78 +1430,64 @@ class MagenticBuilder:
self._manager_factory: Callable[[], MagenticManagerBase] | None = None
self._manager_agent_factory: Callable[[], SupportsAgentRun] | None = None
self._standard_manager_options: dict[str, Any] = {}
self._enable_plan_review: bool = False
self._enable_plan_review: bool = enable_plan_review
self._checkpoint_storage: CheckpointStorage | None = None
self._checkpoint_storage: CheckpointStorage | None = checkpoint_storage
# Intermediate outputs
self._intermediate_outputs = False
self._intermediate_outputs = intermediate_outputs
def register_participants(
if participants is None and participant_factories is None:
raise ValueError("Either participants or participant_factories must be provided.")
if participant_factories is not None:
self._set_participant_factories(participant_factories)
if participants is not None:
self._set_participants(participants)
# Set manager if provided
if any(x is not None for x in [manager, manager_factory, manager_agent, manager_agent_factory]):
self._set_manager(
manager=manager,
manager_factory=manager_factory,
manager_agent=manager_agent,
manager_agent_factory=manager_agent_factory,
task_ledger=task_ledger,
task_ledger_facts_prompt=task_ledger_facts_prompt,
task_ledger_plan_prompt=task_ledger_plan_prompt,
task_ledger_full_prompt=task_ledger_full_prompt,
task_ledger_facts_update_prompt=task_ledger_facts_update_prompt,
task_ledger_plan_update_prompt=task_ledger_plan_update_prompt,
progress_ledger_prompt=progress_ledger_prompt,
final_answer_prompt=final_answer_prompt,
max_stall_count=max_stall_count,
max_reset_count=max_reset_count,
max_round_count=max_round_count,
)
def _set_participant_factories(
self,
participant_factories: Sequence[Callable[[], SupportsAgentRun | Executor]],
) -> "MagenticBuilder":
"""Register participant factories for this Magentic workflow.
Args:
participant_factories: Sequence of callables that return SupportsAgentRun or Executor instances.
Returns:
Self for method chaining
Raises:
ValueError: If participant_factories is empty, or participants
or participant factories are already set
"""
) -> None:
"""Set participant factories (internal)."""
if self._participants:
raise ValueError("Cannot mix .participants() and .register_participants() in the same builder instance.")
raise ValueError("Cannot provide both participants and participant_factories.")
if self._participant_factories:
raise ValueError("register_participants() has already been called on this builder instance.")
raise ValueError("participant_factories already set.")
if not participant_factories:
raise ValueError("participant_factories cannot be empty")
self._participant_factories = list(participant_factories)
return self
def participants(self, participants: Sequence[SupportsAgentRun | Executor]) -> Self:
"""Define participants for this Magentic workflow.
Accepts SupportsAgentRun instances (auto-wrapped as AgentExecutor) or Executor instances.
Args:
participants: Sequence of participant definitions
Returns:
Self for method chaining
Raises:
ValueError: If participants are empty, names are duplicated, or participants
or participant factories are already set
TypeError: If any participant is not SupportsAgentRun or Executor instance
Example:
.. code-block:: python
workflow = (
MagenticBuilder()
.participants([research_agent, writing_agent, coding_agent, review_agent])
.with_manager(agent=manager_agent)
.build()
)
Notes:
- Participant names become part of the manager's context for selection
- Agent descriptions (if available) are extracted and provided to the manager
- Can be called multiple times to add participants incrementally
"""
def _set_participants(self, participants: Sequence[SupportsAgentRun | Executor]) -> None:
"""Set participants (internal)."""
if self._participant_factories:
raise ValueError("Cannot mix .participants() and .register_participants() in the same builder instance.")
raise ValueError("Cannot provide both participants and participant_factories.")
if self._participants:
raise ValueError("participants have already been set. Call participants(...) at most once.")
raise ValueError("participants already set.")
if not participants:
raise ValueError("participants cannot be empty.")
@@ -1482,8 +1513,6 @@ class MagenticBuilder:
self._participants = named
return self
def with_plan_review(self, enable: bool = True) -> "MagenticBuilder":
"""Enable or disable human-in-the-loop plan review before task execution.
@@ -1509,9 +1538,7 @@ class MagenticBuilder:
.. code-block:: python
workflow = (
MagenticBuilder()
.participants(agent1=agent1)
.with_manager(agent=manager_agent)
MagenticBuilder(participants=[agent1], manager_agent=manager_agent)
.with_plan_review(enable=True)
.build()
)
@@ -1556,11 +1583,7 @@ class MagenticBuilder:
storage = InMemoryCheckpointStorage()
workflow = (
MagenticBuilder()
.participants([agent1])
.with_manager(agent=manager_agent)
.with_checkpointing(storage)
.build()
MagenticBuilder(participants=[agent1], manager_agent=manager_agent).with_checkpointing(storage).build()
)
# First run
@@ -1580,144 +1603,14 @@ class MagenticBuilder:
self._checkpoint_storage = checkpoint_storage
return self
@overload
def with_manager(self, *, manager: MagenticManagerBase) -> Self:
"""Configure the workflow with a pre-defined Magentic manager instance.
Args:
manager: A custom manager instance (subclass of MagenticManagerBase)
Returns:
Self for method chaining
"""
...
@overload
def with_manager(self, *, manager_factory: Callable[[], MagenticManagerBase]) -> Self:
"""Configure the workflow with a factory for creating custom Magentic manager instances.
Args:
manager_factory: Callable that returns a new MagenticManagerBase instance
Returns:
Self for method chaining
"""
...
@overload
def with_manager(
self,
*,
agent: SupportsAgentRun,
task_ledger: _MagenticTaskLedger | None = None,
# Prompt overrides
task_ledger_facts_prompt: str | None = None,
task_ledger_plan_prompt: str | None = None,
task_ledger_full_prompt: str | None = None,
task_ledger_facts_update_prompt: str | None = None,
task_ledger_plan_update_prompt: str | None = None,
progress_ledger_prompt: str | None = None,
final_answer_prompt: str | None = None,
# Limits
max_stall_count: int = 3,
max_reset_count: int | None = None,
max_round_count: int | None = None,
) -> Self:
"""Configure the workflow with an agent for creating a standard manager.
This will create a StandardMagenticManager using the provided agent.
Args:
agent: SupportsAgentRun instance for the standard magentic manager
(`StandardMagenticManager`)
task_ledger: Optional custom task ledger implementation for specialized
prompting or structured output requirements
task_ledger_facts_prompt: Custom prompt template for extracting facts from
task description
task_ledger_plan_prompt: Custom prompt template for generating initial plan
task_ledger_full_prompt: Custom prompt template for complete task ledger
(facts + plan combined)
task_ledger_facts_update_prompt: Custom prompt template for updating facts
based on agent progress
task_ledger_plan_update_prompt: Custom prompt template for replanning when
needed
progress_ledger_prompt: Custom prompt template for assessing progress and
determining next actions
final_answer_prompt: Custom prompt template for synthesizing final response
when task is complete
max_stall_count: Maximum consecutive rounds without progress before triggering
replan (default 3). Set to 0 to disable stall detection.
max_reset_count: Maximum number of complete resets allowed before failing.
None means unlimited resets.
max_round_count: Maximum total coordination rounds before stopping with
partial result. None means unlimited rounds.
Returns:
Self for method chaining
"""
...
@overload
def with_manager(
self,
*,
agent_factory: Callable[[], SupportsAgentRun],
task_ledger: _MagenticTaskLedger | None = None,
# Prompt overrides
task_ledger_facts_prompt: str | None = None,
task_ledger_plan_prompt: str | None = None,
task_ledger_full_prompt: str | None = None,
task_ledger_facts_update_prompt: str | None = None,
task_ledger_plan_update_prompt: str | None = None,
progress_ledger_prompt: str | None = None,
final_answer_prompt: str | None = None,
# Limits
max_stall_count: int = 3,
max_reset_count: int | None = None,
max_round_count: int | None = None,
) -> Self:
"""Configure the workflow with a factory for creating the manager agent.
This will create a StandardMagenticManager using the provided agent factory.
Args:
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
task_ledger_facts_prompt: Custom prompt template for extracting facts from
task description
task_ledger_plan_prompt: Custom prompt template for generating initial plan
task_ledger_full_prompt: Custom prompt template for complete task ledger
(facts + plan combined)
task_ledger_facts_update_prompt: Custom prompt template for updating facts
based on agent progress
task_ledger_plan_update_prompt: Custom prompt template for replanning when
needed
progress_ledger_prompt: Custom prompt template for assessing progress and
determining next actions
final_answer_prompt: Custom prompt template for synthesizing final response
when task is complete
max_stall_count: Maximum consecutive rounds without progress before triggering
replan (default 3). Set to 0 to disable stall detection.
max_reset_count: Maximum number of complete resets allowed before failing.
None means unlimited resets.
max_round_count: Maximum total coordination rounds before stopping with
partial result. None means unlimited rounds.
Returns:
Self for method chaining
"""
...
def with_manager(
def _set_manager(
self,
*,
manager: MagenticManagerBase | None = None,
manager_factory: Callable[[], MagenticManagerBase] | None = None,
agent_factory: Callable[[], SupportsAgentRun] | None = None,
manager_agent: SupportsAgentRun | None = None,
manager_agent_factory: Callable[[], SupportsAgentRun] | None = None,
# Constructor args for StandardMagenticManager when manager is not provided
agent: SupportsAgentRun | None = None,
task_ledger: _MagenticTaskLedger | None = None,
# Prompt overrides
task_ledger_facts_prompt: str | None = None,
@@ -1731,123 +1624,37 @@ class MagenticBuilder:
max_stall_count: int = 3,
max_reset_count: int | None = None,
max_round_count: int | None = None,
) -> Self:
"""Configure the workflow manager for task planning and agent coordination.
The manager is responsible for creating plans, selecting agents, tracking progress,
and deciding when to replan or complete. This method supports four usage patterns:
1. **Provide existing manager**: Pass a pre-configured manager instance (custom
or standard) for full control over behavior
2. **Factory for custom manager**: Pass a callable that returns a new manager
instance for more advanced scenarios so that the builder can be reused
3. **Factory for agent**: Pass a callable that returns a new agent instance to
automatically create a `StandardMagenticManager`
4. **Auto-create with agent**: Pass an agent to automatically create a `StandardMagenticManager`
) -> None:
"""Configure the workflow manager for task planning and agent coordination (internal).
Args:
manager: Pre-configured manager instance (`StandardMagenticManager` or custom
`MagenticManagerBase` subclass). If provided, all other arguments are ignored.
manager: Pre-configured manager instance.
manager_factory: Callable that returns a new manager instance.
agent_factory: Callable that returns a new agent instance.
agent: Agent instance for generating plans and decisions. The agent's
configured instructions and options (temperature, seed, etc.) will be
applied.
task_ledger: Optional custom task ledger implementation for specialized
prompting or structured output requirements
task_ledger_facts_prompt: Custom prompt template for extracting facts from
task description
task_ledger_plan_prompt: Custom prompt template for generating initial plan
task_ledger_full_prompt: Custom prompt template for complete task ledger
(facts + plan combined)
task_ledger_facts_update_prompt: Custom prompt template for updating facts
based on agent progress
task_ledger_plan_update_prompt: Custom prompt template for replanning when
needed
progress_ledger_prompt: Custom prompt template for assessing progress and
determining next actions
final_answer_prompt: Custom prompt template for synthesizing final response
when task is complete
max_stall_count: Maximum consecutive rounds without progress before triggering
replan (default 3). Set to 0 to disable stall detection.
max_reset_count: Maximum number of complete resets allowed before failing.
None means unlimited resets.
max_round_count: Maximum total coordination rounds before stopping with
partial result. None means unlimited rounds.
Returns:
Self for method chaining
manager_agent: Agent instance for creating a StandardMagenticManager.
manager_agent_factory: Callable that returns a new agent instance for creating a StandardMagenticManager.
task_ledger: Optional custom task ledger implementation.
task_ledger_facts_prompt: Custom prompt for extracting facts.
task_ledger_plan_prompt: Custom prompt for generating initial plan.
task_ledger_full_prompt: Custom prompt for complete task ledger.
task_ledger_facts_update_prompt: Custom prompt for updating facts.
task_ledger_plan_update_prompt: Custom prompt for replanning.
progress_ledger_prompt: Custom prompt for assessing progress.
final_answer_prompt: Custom prompt for synthesizing final response.
max_stall_count: Max consecutive rounds without progress before replan (default 3).
max_reset_count: Max number of resets allowed. None means unlimited.
max_round_count: Max total coordination rounds. None means unlimited.
Raises:
ValueError: If manager is None and agent is not provided.
Usage with agent (recommended):
.. code-block:: python
from agent_framework import ChatAgent, ChatOptions
from agent_framework.openai import OpenAIChatClient
# Configure manager agent with specific options and instructions
manager_agent = ChatAgent(
name="Coordinator",
chat_client=OpenAIChatClient(model_id="gpt-4o"),
options=ChatOptions(temperature=0.3, seed=42),
instructions="Be concise and focus on accuracy",
)
workflow = (
MagenticBuilder()
.participants(agent1=agent1, agent2=agent2)
.with_manager(
agent=manager_agent,
max_round_count=20,
max_stall_count=3,
)
.build()
)
Usage with custom manager:
.. code-block:: python
class MyManager(MagenticManagerBase):
async def plan(self, context: MagenticContext) -> ChatMessage:
# Custom planning logic
return ChatMessage(role="assistant", text="...")
manager = MyManager()
workflow = MagenticBuilder().participants(agent1=agent1).with_manager(manager).build()
Usage with prompt customization:
.. code-block:: python
workflow = (
MagenticBuilder()
.participants(coder=coder_agent, reviewer=reviewer_agent)
.with_manager(
agent=manager_agent,
task_ledger_plan_prompt="Create a detailed step-by-step plan...",
progress_ledger_prompt="Assess progress and decide next action...",
max_stall_count=2,
)
.build()
)
Notes:
- StandardMagenticManager uses structured LLM calls for all decisions
- Custom managers can implement alternative selection strategies
- Prompt templates support Jinja2-style variable substitution
- Stall detection helps prevent infinite loops in stuck scenarios
- The agent's instructions are used as system instructions for all manager prompts
ValueError: If a manager has already been set or if none or multiple
of the primary parameters are provided.
"""
if any([self._manager, self._manager_factory, self._manager_agent_factory]):
raise ValueError("with_manager() has already been called on this builder instance.")
raise ValueError("Manager has already been configured. Set manager config once only.")
if sum(x is not None for x in [manager, agent, manager_factory, agent_factory]) != 1:
raise ValueError("Exactly one of manager, agent, manager_factory, or agent_factory must be provided.")
if sum(x is not None for x in [manager, manager_agent, manager_factory, manager_agent_factory]) != 1:
raise ValueError(
"Exactly one of manager, manager_agent, manager_factory, or manager_agent_factory must be provided."
)
def _log_warning_if_constructor_args_provided() -> None:
if any(
@@ -1866,14 +1673,14 @@ class MagenticBuilder:
max_round_count,
]
):
logger.warning("Customer manager provided; all other with_manager() arguments will be ignored.")
logger.warning("Custom manager provided; all other manager arguments will be ignored.")
if manager is not None:
self._manager = manager
_log_warning_if_constructor_args_provided()
elif agent is not None:
elif manager_agent is not None:
self._manager = StandardMagenticManager(
agent=agent,
agent=manager_agent,
task_ledger=task_ledger,
task_ledger_facts_prompt=task_ledger_facts_prompt,
task_ledger_plan_prompt=task_ledger_plan_prompt,
@@ -1889,8 +1696,8 @@ class MagenticBuilder:
elif manager_factory is not None:
self._manager_factory = manager_factory
_log_warning_if_constructor_args_provided()
elif agent_factory is not None:
self._manager_agent_factory = agent_factory
elif manager_agent_factory is not None:
self._manager_agent_factory = manager_agent_factory
self._standard_manager_options = {
"task_ledger": task_ledger,
"task_ledger_facts_prompt": task_ledger_facts_prompt,
@@ -1905,21 +1712,6 @@ class MagenticBuilder:
"max_round_count": max_round_count,
}
return self
def with_intermediate_outputs(self) -> Self:
"""Enable intermediate outputs from agent participants before aggregation.
When enabled, the workflow returns each agent participant's response or yields
streaming updates as they become available. The output of the orchestrator will
always be available as the final output of the workflow.
Returns:
Self for fluent chaining
"""
self._intermediate_outputs = True
return self
def _resolve_orchestrator(self, participants: Sequence[Executor]) -> Executor:
"""Determine the orchestrator to use for the workflow.
@@ -1927,8 +1719,11 @@ class MagenticBuilder:
participants: List of resolved participant executors
"""
if all(x is None for x in [self._manager, self._manager_factory, self._manager_agent_factory]):
raise ValueError("No manager configured. Call with_manager(...) before building the orchestrator.")
# We don't need to check if multiple are set since that is handled in with_orchestrator()
raise ValueError(
"No manager configured. "
"Pass manager, manager_factory, manager_agent, or manager_agent_factory to the constructor."
)
# We don't need to check if multiple are set since that is handled in _set_manager()
if self._manager:
manager = self._manager
@@ -1942,7 +1737,10 @@ class MagenticBuilder:
)
else:
# This should never be reached due to the checks above
raise RuntimeError("Manager could not be resolved. Please set the manager properly with with_manager().")
raise RuntimeError(
"Manager could not be resolved. "
"Pass manager, manager_factory, manager_agent, or manager_agent_factory to the constructor."
)
return MagenticOrchestrator(
manager=manager,
@@ -1953,7 +1751,7 @@ class MagenticBuilder:
def _resolve_participants(self) -> list[Executor]:
"""Resolve participant instances into Executor objects."""
if not self._participants and not self._participant_factories:
raise ValueError("No participants provided. Call .participants() or .register_participants() first.")
raise ValueError("No participants provided. Pass participants or participant_factories to the constructor.")
# We don't need to check if both are set since that is handled in the respective methods
participants: list[Executor | SupportsAgentRun] = []
@@ -1985,17 +1783,15 @@ class MagenticBuilder:
orchestrator: Executor = self._resolve_orchestrator(participants)
# Build workflow graph
workflow_builder = WorkflowBuilder().set_start_executor(orchestrator)
workflow_builder = WorkflowBuilder(
start_executor=orchestrator,
checkpoint_storage=self._checkpoint_storage,
output_executors=[orchestrator] if not self._intermediate_outputs else None,
)
for participant in participants:
# Orchestrator and participant bi-directional edges
workflow_builder = workflow_builder.add_edge(orchestrator, participant)
workflow_builder = workflow_builder.add_edge(participant, orchestrator)
if self._checkpoint_storage is not None:
workflow_builder = workflow_builder.with_checkpointing(self._checkpoint_storage)
if not self._intermediate_outputs:
# Constrain output to orchestrator only
workflow_builder = workflow_builder.with_output_from([orchestrator])
return workflow_builder.build()
@@ -132,11 +132,10 @@ class AgentApprovalExecutor(WorkflowExecutor):
request_info_executor = AgentRequestInfoExecutor(id="agent_request_info_executor")
return (
WorkflowBuilder()
WorkflowBuilder(start_executor=agent_executor)
# Create a loop between agent executor and request info executor
.add_edge(agent_executor, request_info_executor)
.add_edge(request_info_executor, agent_executor)
.set_start_executor(agent_executor)
.build()
)
@@ -4,8 +4,8 @@
This module provides a high-level, agent-focused API to assemble a sequential
workflow where:
- Participants can be provided as SupportsAgentRun or Executor instances via `.participants()`,
or as factories returning SupportsAgentRun or Executor via `.register_participants()`
- Participants can be provided as SupportsAgentRun or Executor instances via `participants=[...]`,
or as factories returning SupportsAgentRun or Executor via `participant_factories=[...]`
- 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
@@ -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 SupportsAgentRun (recommended) or Executor instances
- `register_participants([...])` accepts a list of factories for SupportsAgentRun (recommended)
- `participants=[...]` accepts a list of SupportsAgentRun (recommended) or Executor instances
- `participant_factories=[...]` 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
@@ -125,64 +125,81 @@ class SequentialBuilder:
from agent_framework_orchestrations import SequentialBuilder
# With agent instances
workflow = SequentialBuilder().participants([agent1, agent2, summarizer_exec]).build()
workflow = SequentialBuilder(participants=[agent1, agent2, summarizer_exec]).build()
# With agent factories
workflow = (
SequentialBuilder().register_participants([create_agent1, create_agent2, create_summarizer_exec]).build()
)
workflow = SequentialBuilder(
participant_factories=[create_agent1, create_agent2, create_summarizer_exec]
).build()
# Enable checkpoint persistence
workflow = SequentialBuilder().participants([agent1, agent2]).with_checkpointing(storage).build()
workflow = SequentialBuilder(participants=[agent1, agent2], checkpoint_storage=storage).build()
# Enable request info for mid-workflow feedback (pauses before each agent)
workflow = SequentialBuilder().participants([agent1, agent2]).with_request_info().build()
workflow = SequentialBuilder(participants=[agent1, agent2]).with_request_info().build()
# Enable request info only for specific agents
workflow = (
SequentialBuilder()
.participants([agent1, agent2, agent3])
SequentialBuilder(participants=[agent1, agent2, agent3])
.with_request_info(agents=[agent2]) # Only pause before agent2
.build()
)
"""
def __init__(self) -> None:
def __init__(
self,
*,
participants: Sequence[SupportsAgentRun | Executor] | None = None,
participant_factories: Sequence[Callable[[], SupportsAgentRun | Executor]] | None = None,
checkpoint_storage: CheckpointStorage | None = None,
intermediate_outputs: bool = False,
) -> None:
"""Initialize the SequentialBuilder.
Args:
participants: Optional sequence of agent or executor instances to run sequentially.
participant_factories: Optional sequence of callables returning agent or executor instances.
checkpoint_storage: Optional checkpoint storage for enabling workflow state persistence.
intermediate_outputs: If True, enables intermediate outputs from agent participants.
"""
self._participants: list[SupportsAgentRun | Executor] = []
self._participant_factories: list[Callable[[], SupportsAgentRun | Executor]] = []
self._checkpoint_storage: CheckpointStorage | None = None
self._checkpoint_storage: CheckpointStorage | None = checkpoint_storage
self._request_info_enabled: bool = False
self._request_info_filter: set[str] | None = None
self._intermediate_outputs: bool = False
self._intermediate_outputs: bool = intermediate_outputs
def register_participants(
if participants is None and participant_factories is None:
raise ValueError("Either participants or participant_factories must be provided.")
if participant_factories is not None:
self._set_participant_factories(participant_factories)
if participants is not None:
self._set_participants(participants)
def _set_participant_factories(
self,
participant_factories: Sequence[Callable[[], SupportsAgentRun | Executor]],
) -> "SequentialBuilder":
"""Register participant factories for this sequential workflow."""
) -> None:
"""Set participant factories (internal)."""
if self._participants:
raise ValueError("Cannot mix .participants() and .register_participants() in the same builder instance.")
raise ValueError("Cannot provide both participants and participant_factories.")
if self._participant_factories:
raise ValueError("register_participants() has already been called on this builder instance.")
raise ValueError("participant_factories already set.")
if not participant_factories:
raise ValueError("participant_factories cannot be empty")
self._participant_factories = list(participant_factories)
return self
def participants(self, participants: Sequence[SupportsAgentRun | Executor]) -> "SequentialBuilder":
"""Define the ordered participants for this sequential workflow.
Accepts SupportsAgentRun instances (auto-wrapped as AgentExecutor) or Executor instances.
Raises if empty or duplicates are provided for clarity.
"""
def _set_participants(self, participants: Sequence[SupportsAgentRun | Executor]) -> None:
"""Set participants (internal)."""
if self._participant_factories:
raise ValueError("Cannot mix .participants() and .register_participants() in the same builder instance.")
raise ValueError("Cannot provide both participants and participant_factories.")
if self._participants:
raise ValueError("participants() has already been called on this builder instance.")
raise ValueError("participants already set.")
if not participants:
raise ValueError("participants cannot be empty")
@@ -203,12 +220,6 @@ class SequentialBuilder:
seen_agent_ids.add(pid)
self._participants = list(participants)
return self
def with_checkpointing(self, checkpoint_storage: CheckpointStorage) -> "SequentialBuilder":
"""Enable checkpointing for the built workflow using the provided storage."""
self._checkpoint_storage = checkpoint_storage
return self
def with_request_info(
self,
@@ -243,23 +254,10 @@ class SequentialBuilder:
return self
def with_intermediate_outputs(self) -> "SequentialBuilder":
"""Enable intermediate outputs from agent participants.
When enabled, the workflow returns each agent participant's response or yields
streaming updates as they become available. The output of the last participant
will always be available as the final output of the workflow.
Returns:
Self for fluent chaining
"""
self._intermediate_outputs = True
return self
def _resolve_participants(self) -> list[Executor]:
"""Resolve participant instances into Executor objects."""
if not self._participants and not self._participant_factories:
raise ValueError("No participants provided. Call .participants() or .register_participants() first.")
raise ValueError("No participants provided. Pass participants or participant_factories to the constructor.")
# We don't need to check if both are set since that is handled in the respective methods
participants: list[Executor | SupportsAgentRun] = []
@@ -308,8 +306,11 @@ class SequentialBuilder:
# Resolve participants and participant factories to executors
participants: list[Executor] = self._resolve_participants()
builder = WorkflowBuilder()
builder.set_start_executor(input_conv)
builder = WorkflowBuilder(
start_executor=input_conv,
checkpoint_storage=self._checkpoint_storage,
output_executors=[end] if not self._intermediate_outputs else None,
)
# Start of the chain is the input normalizer
prior: Executor | SupportsAgentRun = input_conv
@@ -319,11 +320,4 @@ class SequentialBuilder:
# Terminate with the final conversation
builder.add_edge(prior, end)
if not self._intermediate_outputs:
# Constrain output to end only
builder = builder.with_output_from([end])
if self._checkpoint_storage is not None:
builder = builder.with_checkpointing(self._checkpoint_storage)
return builder.build()
@@ -39,14 +39,14 @@ class _FakeAgentExec(Executor):
def test_concurrent_builder_rejects_empty_participants() -> None:
with pytest.raises(ValueError):
ConcurrentBuilder().participants([])
ConcurrentBuilder(participants=[])
def test_concurrent_builder_rejects_duplicate_executors() -> None:
a = _FakeAgentExec("dup", "A")
b = _FakeAgentExec("dup", "B") # same executor id
with pytest.raises(ValueError):
ConcurrentBuilder().participants([a, b])
ConcurrentBuilder(participants=[a, b])
def test_concurrent_builder_rejects_duplicate_executors_from_factories() -> None:
@@ -58,43 +58,35 @@ def test_concurrent_builder_rejects_duplicate_executors_from_factories() -> None
def create_dup2() -> Executor:
return _FakeAgentExec("dup", "B") # same executor id
builder = ConcurrentBuilder().register_participants([create_dup1, create_dup2])
builder = ConcurrentBuilder(participant_factories=[create_dup1, create_dup2])
with pytest.raises(ValueError, match="Duplicate executor ID 'dup' detected in workflow."):
builder.build()
def test_concurrent_builder_rejects_mixed_participants_and_factories() -> None:
"""Test that mixing .participants() and .register_participants() raises an error."""
# Case 1: participants first, then register_participants
with pytest.raises(ValueError, match="Cannot mix .participants"):
(
ConcurrentBuilder()
.participants([_FakeAgentExec("a", "A")])
.register_participants([lambda: _FakeAgentExec("b", "B")])
)
# Case 2: register_participants first, then participants
with pytest.raises(ValueError, match="Cannot mix .participants"):
(
ConcurrentBuilder()
.register_participants([lambda: _FakeAgentExec("a", "A")])
.participants([_FakeAgentExec("b", "B")])
"""Test that passing both participants and participant_factories to the constructor raises an error."""
with pytest.raises(ValueError, match="Cannot provide both participants and participant_factories"):
ConcurrentBuilder(
participants=[_FakeAgentExec("a", "A")],
participant_factories=[lambda: _FakeAgentExec("b", "B")],
)
def test_concurrent_builder_rejects_multiple_calls_to_participants() -> None:
"""Test that multiple calls to .participants() raises an error."""
with pytest.raises(ValueError, match=r"participants\(\) has already been called"):
(ConcurrentBuilder().participants([_FakeAgentExec("a", "A")]).participants([_FakeAgentExec("b", "B")]))
def test_concurrent_builder_rejects_both_participants_and_factories() -> None:
"""Test that passing both participants and participant_factories raises an error."""
with pytest.raises(ValueError, match="Cannot provide both participants and participant_factories"):
ConcurrentBuilder(
participants=[_FakeAgentExec("a", "A")],
participant_factories=[lambda: _FakeAgentExec("b", "B")],
)
def test_concurrent_builder_rejects_multiple_calls_to_register_participants() -> None:
"""Test that multiple calls to .register_participants() raises an error."""
with pytest.raises(ValueError, match=r"register_participants\(\) has already been called"):
(
ConcurrentBuilder()
.register_participants([lambda: _FakeAgentExec("a", "A")])
.register_participants([lambda: _FakeAgentExec("b", "B")])
def test_concurrent_builder_rejects_both_factories_and_participants() -> None:
"""Test that passing both participant_factories and participants raises an error."""
with pytest.raises(ValueError, match="Cannot provide both participants and participant_factories"):
ConcurrentBuilder(
participant_factories=[lambda: _FakeAgentExec("a", "A")],
participants=[_FakeAgentExec("b", "B")],
)
@@ -104,7 +96,7 @@ async def test_concurrent_default_aggregator_emits_single_user_and_assistants()
e2 = _FakeAgentExec("agentB", "Beta")
e3 = _FakeAgentExec("agentC", "Gamma")
wf = ConcurrentBuilder().participants([e1, e2, e3]).build()
wf = ConcurrentBuilder(participants=[e1, e2, e3]).build()
completed = False
output: list[ChatMessage] | None = None
@@ -142,7 +134,7 @@ async def test_concurrent_custom_aggregator_callback_is_used() -> None:
texts.append(msgs[-1].text if msgs else "")
return " | ".join(sorted(texts))
wf = ConcurrentBuilder().participants([e1, e2]).with_aggregator(summarize).build()
wf = ConcurrentBuilder(participants=[e1, e2]).with_aggregator(summarize).build()
completed = False
output: str | None = None
@@ -173,7 +165,7 @@ async def test_concurrent_custom_aggregator_sync_callback_is_used() -> None:
texts.append(msgs[-1].text if msgs else "")
return " | ".join(sorted(texts))
wf = ConcurrentBuilder().participants([e1, e2]).with_aggregator(summarize_sync).build()
wf = ConcurrentBuilder(participants=[e1, e2]).with_aggregator(summarize_sync).build()
completed = False
output: str | None = None
@@ -198,7 +190,7 @@ def test_concurrent_custom_aggregator_uses_callback_name_for_id() -> None:
def summarize(results: list[AgentExecutorResponse]) -> str: # type: ignore[override]
return str(len(results))
wf = ConcurrentBuilder().participants([e1, e2]).with_aggregator(summarize).build()
wf = ConcurrentBuilder(participants=[e1, e2]).with_aggregator(summarize).build()
assert "summarize" in wf.executors
aggregator = wf.executors["summarize"]
@@ -221,7 +213,7 @@ async def test_concurrent_with_aggregator_executor_instance() -> None:
e2 = _FakeAgentExec("agentB", "Two")
aggregator_instance = CustomAggregator(id="instance_aggregator")
wf = ConcurrentBuilder().participants([e1, e2]).with_aggregator(aggregator_instance).build()
wf = ConcurrentBuilder(participants=[e1, e2]).with_aggregator(aggregator_instance).build()
completed = False
output: str | None = None
@@ -255,8 +247,7 @@ async def test_concurrent_with_aggregator_executor_factory() -> None:
e2 = _FakeAgentExec("agentB", "Two")
wf = (
ConcurrentBuilder()
.participants([e1, e2])
ConcurrentBuilder(participants=[e1, e2])
.register_aggregator(lambda: CustomAggregator(id="custom_aggregator"))
.build()
)
@@ -295,7 +286,7 @@ async def test_concurrent_with_aggregator_executor_factory_with_default_id() ->
e1 = _FakeAgentExec("agentA", "One")
e2 = _FakeAgentExec("agentB", "Two")
wf = ConcurrentBuilder().participants([e1, e2]).register_aggregator(CustomAggregator).build()
wf = ConcurrentBuilder(participants=[e1, e2]).register_aggregator(CustomAggregator).build()
completed = False
output: str | None = None
@@ -320,7 +311,11 @@ def test_concurrent_builder_rejects_multiple_calls_to_with_aggregator() -> None:
return str(len(results))
with pytest.raises(ValueError, match=r"with_aggregator\(\) has already been called"):
(ConcurrentBuilder().with_aggregator(summarize).with_aggregator(summarize))
(
ConcurrentBuilder(participants=[_FakeAgentExec("a", "A")])
.with_aggregator(summarize)
.with_aggregator(summarize)
)
def test_concurrent_builder_rejects_multiple_calls_to_register_aggregator() -> None:
@@ -331,7 +326,7 @@ def test_concurrent_builder_rejects_multiple_calls_to_register_aggregator() -> N
with pytest.raises(ValueError, match=r"register_aggregator\(\) has already been called"):
(
ConcurrentBuilder()
ConcurrentBuilder(participants=[_FakeAgentExec("a", "A")])
.register_aggregator(lambda: CustomAggregator(id="agg1"))
.register_aggregator(lambda: CustomAggregator(id="agg2"))
)
@@ -346,7 +341,7 @@ async def test_concurrent_checkpoint_resume_round_trip() -> None:
_FakeAgentExec("agentC", "Gamma"),
)
wf = ConcurrentBuilder().participants(list(participants)).with_checkpointing(storage).build()
wf = ConcurrentBuilder(participants=list(participants), checkpoint_storage=storage).build()
baseline_output: list[ChatMessage] | None = None
async for ev in wf.run("checkpoint concurrent", stream=True):
@@ -370,7 +365,7 @@ async def test_concurrent_checkpoint_resume_round_trip() -> None:
_FakeAgentExec("agentB", "Beta"),
_FakeAgentExec("agentC", "Gamma"),
)
wf_resume = ConcurrentBuilder().participants(list(resumed_participants)).with_checkpointing(storage).build()
wf_resume = ConcurrentBuilder(participants=list(resumed_participants), checkpoint_storage=storage).build()
resumed_output: list[ChatMessage] | None = None
async for ev in wf_resume.run(checkpoint_id=resume_checkpoint.checkpoint_id, stream=True):
@@ -392,7 +387,7 @@ async def test_concurrent_checkpoint_runtime_only() -> None:
storage = InMemoryCheckpointStorage()
agents = [_FakeAgentExec(id="agent1", reply_text="A1"), _FakeAgentExec(id="agent2", reply_text="A2")]
wf = ConcurrentBuilder().participants(agents).build()
wf = ConcurrentBuilder(participants=agents).build()
baseline_output: list[ChatMessage] | None = None
async for ev in wf.run("runtime checkpoint test", checkpoint_storage=storage, stream=True):
@@ -413,7 +408,7 @@ async def test_concurrent_checkpoint_runtime_only() -> None:
)
resumed_agents = [_FakeAgentExec(id="agent1", reply_text="A1"), _FakeAgentExec(id="agent2", reply_text="A2")]
wf_resume = ConcurrentBuilder().participants(resumed_agents).build()
wf_resume = ConcurrentBuilder(participants=resumed_agents).build()
resumed_output: list[ChatMessage] | None = None
async for ev in wf_resume.run(
@@ -442,7 +437,7 @@ async def test_concurrent_checkpoint_runtime_overrides_buildtime() -> None:
runtime_storage = FileCheckpointStorage(temp_dir2)
agents = [_FakeAgentExec(id="agent1", reply_text="A1"), _FakeAgentExec(id="agent2", reply_text="A2")]
wf = ConcurrentBuilder().participants(agents).with_checkpointing(buildtime_storage).build()
wf = ConcurrentBuilder(participants=agents, checkpoint_storage=buildtime_storage).build()
baseline_output: list[ChatMessage] | None = None
async for ev in wf.run("override test", checkpoint_storage=runtime_storage, stream=True):
@@ -462,7 +457,7 @@ async def test_concurrent_checkpoint_runtime_overrides_buildtime() -> None:
def test_concurrent_builder_rejects_empty_participant_factories() -> None:
with pytest.raises(ValueError):
ConcurrentBuilder().register_participants([])
ConcurrentBuilder(participant_factories=[])
async def test_concurrent_builder_reusable_after_build_with_participants() -> None:
@@ -470,7 +465,7 @@ async def test_concurrent_builder_reusable_after_build_with_participants() -> No
e1 = _FakeAgentExec("agentA", "One")
e2 = _FakeAgentExec("agentB", "Two")
builder = ConcurrentBuilder().participants([e1, e2])
builder = ConcurrentBuilder(participants=[e1, e2])
builder.build()
@@ -493,7 +488,7 @@ async def test_concurrent_builder_reusable_after_build_with_factories() -> None:
call_count += 1
return _FakeAgentExec("agentB", "Two")
builder = ConcurrentBuilder().register_participants([create_agent_executor_a, create_agent_executor_b])
builder = ConcurrentBuilder(participant_factories=[create_agent_executor_a, create_agent_executor_b])
# Build the first workflow
wf1 = builder.build()
@@ -523,7 +518,7 @@ async def test_concurrent_with_register_participants() -> None:
def create_agent3() -> Executor:
return _FakeAgentExec("agentC", "Gamma")
wf = ConcurrentBuilder().register_participants([create_agent1, create_agent2, create_agent3]).build()
wf = ConcurrentBuilder(participant_factories=[create_agent1, create_agent2, create_agent3]).build()
completed = False
output: list[ChatMessage] | None = None
@@ -178,13 +178,12 @@ async def test_group_chat_builder_basic_flow() -> None:
alpha = StubAgent("alpha", "ack from alpha")
beta = StubAgent("beta", "ack from beta")
workflow = (
GroupChatBuilder()
.with_orchestrator(selection_func=selector, orchestrator_name="manager")
.participants([alpha, beta])
.with_max_rounds(2) # Limit rounds to prevent infinite loop
.build()
)
workflow = GroupChatBuilder(
participants=[alpha, beta],
max_rounds=2, # Limit rounds to prevent infinite loop
selection_func=selector,
orchestrator_name="manager",
).build()
outputs: list[list[ChatMessage]] = []
async for event in workflow.run("coordinate task", stream=True):
@@ -205,13 +204,12 @@ async def test_group_chat_as_agent_accepts_conversation() -> None:
alpha = StubAgent("alpha", "ack from alpha")
beta = StubAgent("beta", "ack from beta")
workflow = (
GroupChatBuilder()
.with_orchestrator(selection_func=selector, orchestrator_name="manager")
.participants([alpha, beta])
.with_max_rounds(2) # Limit rounds to prevent infinite loop
.build()
)
workflow = GroupChatBuilder(
participants=[alpha, beta],
max_rounds=2, # Limit rounds to prevent infinite loop
selection_func=selector,
orchestrator_name="manager",
).build()
agent = workflow.as_agent(name="group-chat-agent")
conversation = [
@@ -233,64 +231,47 @@ class TestGroupChatBuilder:
"""Test that building without a manager raises ValueError."""
agent = StubAgent("test", "response")
builder = GroupChatBuilder().participants([agent])
builder = GroupChatBuilder(participants=[agent])
with pytest.raises(
ValueError, match=r"No orchestrator has been configured\. Call with_orchestrator\(\) to set one\."
ValueError,
match=r"No orchestrator has been configured\.",
):
builder.build()
def test_build_without_participants_raises_error(self) -> None:
"""Test that building without participants raises ValueError."""
def selector(state: GroupChatState) -> str:
return "agent"
builder = GroupChatBuilder().with_orchestrator(selection_func=selector)
"""Test that constructing without participants raises ValueError."""
with pytest.raises(
ValueError,
match=r"No participants provided\. Call \.participants\(\) or \.register_participants\(\) first\.",
match=r"Either participants or participant_factories must be provided\.",
):
builder.build()
GroupChatBuilder()
def test_duplicate_manager_configuration_raises_error(self) -> None:
"""Test that configuring multiple managers raises ValueError."""
"""Test that configuring multiple orchestrator options raises ValueError."""
agent = StubAgent("test", "response")
def selector(state: GroupChatState) -> str:
return "agent"
builder = GroupChatBuilder().with_orchestrator(selection_func=selector)
with pytest.raises(
ValueError,
match=r"A selection function has already been configured\. Call with_orchestrator\(\.\.\.\) once only\.",
match=r"Exactly one of",
):
builder.with_orchestrator(selection_func=selector)
GroupChatBuilder(participants=[agent], selection_func=selector, orchestrator_agent=StubManagerAgent())
def test_empty_participants_raises_error(self) -> None:
"""Test that empty participants list raises ValueError."""
def selector(state: GroupChatState) -> str:
return "agent"
builder = GroupChatBuilder().with_orchestrator(selection_func=selector)
with pytest.raises(ValueError, match="participants cannot be empty"):
builder.participants([])
GroupChatBuilder(participants=[])
def test_duplicate_participant_names_raises_error(self) -> None:
"""Test that duplicate participant names raise ValueError."""
agent1 = StubAgent("test", "response1")
agent2 = StubAgent("test", "response2")
def selector(state: GroupChatState) -> str:
return "agent"
builder = GroupChatBuilder().with_orchestrator(selection_func=selector)
with pytest.raises(ValueError, match="Duplicate participant name 'test'"):
builder.participants([agent1, agent2])
GroupChatBuilder(participants=[agent1, agent2])
def test_agent_without_name_raises_error(self) -> None:
"""Test that agent without name attribute raises ValueError."""
@@ -315,25 +296,15 @@ class TestGroupChatBuilder:
agent = AgentWithoutName()
def selector(state: GroupChatState) -> str:
return "agent"
builder = GroupChatBuilder().with_orchestrator(selection_func=selector)
with pytest.raises(ValueError, match="SupportsAgentRun participants must have a non-empty name"):
builder.participants([agent])
GroupChatBuilder(participants=[agent])
def test_empty_participant_name_raises_error(self) -> None:
"""Test that empty participant name raises ValueError."""
agent = StubAgent("", "response") # Agent with empty name
def selector(state: GroupChatState) -> str:
return "agent"
builder = GroupChatBuilder().with_orchestrator(selection_func=selector)
with pytest.raises(ValueError, match="SupportsAgentRun participants must have a non-empty name"):
builder.participants([agent])
GroupChatBuilder(participants=[agent])
class TestGroupChatWorkflow:
@@ -350,13 +321,11 @@ class TestGroupChatWorkflow:
agent = StubAgent("agent", "response")
workflow = (
GroupChatBuilder()
.with_orchestrator(selection_func=selector)
.participants([agent])
.with_max_rounds(2) # Limit to 2 rounds
.build()
)
workflow = GroupChatBuilder(
participants=[agent],
max_rounds=2, # Limit to 2 rounds
selection_func=selector,
).build()
outputs: list[list[ChatMessage]] = []
async for event in workflow.run("test task", stream=True):
@@ -385,13 +354,11 @@ class TestGroupChatWorkflow:
agent = StubAgent("agent", "response")
workflow = (
GroupChatBuilder()
.with_orchestrator(selection_func=selector)
.participants([agent])
.with_termination_condition(termination_condition)
.build()
)
workflow = GroupChatBuilder(
participants=[agent],
termination_condition=termination_condition,
selection_func=selector,
).build()
outputs: list[list[ChatMessage]] = []
async for event in workflow.run("test task", stream=True):
@@ -413,13 +380,11 @@ class TestGroupChatWorkflow:
manager = StubManagerAgent()
worker = StubAgent("agent", "response")
workflow = (
GroupChatBuilder()
.with_orchestrator(agent=manager)
.participants([worker])
.with_termination_condition(lambda conv: any(msg.author_name == "agent" for msg in conv))
.build()
)
workflow = GroupChatBuilder(
participants=[worker],
termination_condition=lambda conv: any(msg.author_name == "agent" for msg in conv),
orchestrator_agent=manager,
).build()
outputs: list[list[ChatMessage]] = []
async for event in workflow.run("test task", stream=True):
@@ -441,7 +406,7 @@ class TestGroupChatWorkflow:
agent = StubAgent("agent", "response")
workflow = GroupChatBuilder().with_orchestrator(selection_func=selector).participants([agent]).build()
workflow = GroupChatBuilder(participants=[agent], selection_func=selector).build()
with pytest.raises(RuntimeError, match="Selection function returned unknown participant 'unknown_agent'"):
async for _ in workflow.run("test task", stream=True):
@@ -460,14 +425,12 @@ class TestCheckpointing:
agent = StubAgent("agent", "response")
storage = InMemoryCheckpointStorage()
workflow = (
GroupChatBuilder()
.with_orchestrator(selection_func=selector)
.participants([agent])
.with_max_rounds(1)
.with_checkpointing(storage)
.build()
)
workflow = GroupChatBuilder(
participants=[agent],
max_rounds=1,
checkpoint_storage=storage,
selection_func=selector,
).build()
outputs: list[list[ChatMessage]] = []
async for event in workflow.run("test task", stream=True):
@@ -490,13 +453,7 @@ class TestConversationHandling:
agent = StubAgent("agent", "response")
workflow = (
GroupChatBuilder()
.with_orchestrator(selection_func=selector)
.participants([agent])
.with_max_rounds(1)
.build()
)
workflow = GroupChatBuilder(participants=[agent], max_rounds=1, selection_func=selector).build()
with pytest.raises(ValueError, match="At least one ChatMessage is required to start the group chat workflow."):
async for _ in workflow.run([], stream=True):
@@ -514,13 +471,7 @@ class TestConversationHandling:
agent = StubAgent("agent", "response")
workflow = (
GroupChatBuilder()
.with_orchestrator(selection_func=selector)
.participants([agent])
.with_max_rounds(1)
.build()
)
workflow = GroupChatBuilder(participants=[agent], max_rounds=1, selection_func=selector).build()
outputs: list[list[ChatMessage]] = []
async for event in workflow.run("test string", stream=True):
@@ -543,13 +494,7 @@ class TestConversationHandling:
agent = StubAgent("agent", "response")
workflow = (
GroupChatBuilder()
.with_orchestrator(selection_func=selector)
.participants([agent])
.with_max_rounds(1)
.build()
)
workflow = GroupChatBuilder(participants=[agent], max_rounds=1, selection_func=selector).build()
outputs: list[list[ChatMessage]] = []
async for event in workflow.run(task_message, stream=True):
@@ -575,13 +520,7 @@ class TestConversationHandling:
agent = StubAgent("agent", "response")
workflow = (
GroupChatBuilder()
.with_orchestrator(selection_func=selector)
.participants([agent])
.with_max_rounds(1)
.build()
)
workflow = GroupChatBuilder(participants=[agent], max_rounds=1, selection_func=selector).build()
outputs: list[list[ChatMessage]] = []
async for event in workflow.run(conversation, stream=True):
@@ -607,13 +546,11 @@ class TestRoundLimitEnforcement:
agent = StubAgent("agent", "response")
workflow = (
GroupChatBuilder()
.with_orchestrator(selection_func=selector)
.participants([agent])
.with_max_rounds(1) # Very low limit
.build()
)
workflow = GroupChatBuilder(
participants=[agent],
max_rounds=1, # Very low limit
selection_func=selector,
).build()
outputs: list[list[ChatMessage]] = []
async for event in workflow.run("test", stream=True):
@@ -642,13 +579,11 @@ class TestRoundLimitEnforcement:
agent = StubAgent("agent", "response from agent")
workflow = (
GroupChatBuilder()
.with_orchestrator(selection_func=selector)
.participants([agent])
.with_max_rounds(1) # Hit limit after first response
.build()
)
workflow = GroupChatBuilder(
participants=[agent],
max_rounds=1, # Hit limit after first response
selection_func=selector,
).build()
outputs: list[list[ChatMessage]] = []
async for event in workflow.run("test", stream=True):
@@ -674,13 +609,7 @@ async def test_group_chat_checkpoint_runtime_only() -> None:
agent_b = StubAgent("agentB", "Reply from B")
selector = make_sequence_selector()
wf = (
GroupChatBuilder()
.participants([agent_a, agent_b])
.with_orchestrator(selection_func=selector)
.with_max_rounds(2)
.build()
)
wf = GroupChatBuilder(participants=[agent_a, agent_b], max_rounds=2, selection_func=selector).build()
baseline_output: list[ChatMessage] | None = None
async for ev in wf.run("runtime checkpoint test", checkpoint_storage=storage, stream=True):
@@ -712,14 +641,12 @@ async def test_group_chat_checkpoint_runtime_overrides_buildtime() -> None:
agent_b = StubAgent("agentB", "Reply from B")
selector = make_sequence_selector()
wf = (
GroupChatBuilder()
.participants([agent_a, agent_b])
.with_orchestrator(selection_func=selector)
.with_max_rounds(2)
.with_checkpointing(buildtime_storage)
.build()
)
wf = GroupChatBuilder(
participants=[agent_a, agent_b],
max_rounds=2,
checkpoint_storage=buildtime_storage,
selection_func=selector,
).build()
baseline_output: list[ChatMessage] | None = None
async for ev in wf.run("override test", checkpoint_storage=runtime_storage, stream=True):
if ev.type == "output":
@@ -759,10 +686,12 @@ async def test_group_chat_with_request_info_filtering():
return "alpha"
workflow = (
GroupChatBuilder()
.with_orchestrator(selection_func=selector, orchestrator_name="manager")
.participants([alpha, beta])
.with_max_rounds(2)
GroupChatBuilder(
participants=[alpha, beta],
max_rounds=2,
selection_func=selector,
orchestrator_name="manager",
)
.with_request_info(agents=["beta"]) # Only pause before beta runs
.build()
)
@@ -811,10 +740,12 @@ async def test_group_chat_with_request_info_no_filter_pauses_all():
return "alpha"
workflow = (
GroupChatBuilder()
.with_orchestrator(selection_func=selector, orchestrator_name="manager")
.participants([alpha])
.with_max_rounds(1)
GroupChatBuilder(
participants=[alpha],
max_rounds=1,
selection_func=selector,
orchestrator_name="manager",
)
.with_request_info() # No filter - pause for all
.build()
)
@@ -833,12 +764,13 @@ async def test_group_chat_with_request_info_no_filter_pauses_all():
def test_group_chat_builder_with_request_info_returns_self():
"""Test that with_request_info() returns self for method chaining."""
builder = GroupChatBuilder()
agent = StubAgent("test", "response")
builder = GroupChatBuilder(participants=[agent])
result = builder.with_request_info()
assert result is builder
# Also test with agents parameter
builder2 = GroupChatBuilder()
builder2 = GroupChatBuilder(participants=[agent])
result2 = builder2.with_request_info(agents=["test"])
assert result2 is builder2
@@ -853,47 +785,41 @@ def test_group_chat_builder_rejects_empty_participant_factories():
return list(state.participants.keys())[0]
with pytest.raises(ValueError, match=r"participant_factories cannot be empty"):
GroupChatBuilder().register_participants([])
GroupChatBuilder(participant_factories=[])
with pytest.raises(
ValueError,
match=r"No participants provided\. Call \.participants\(\) or \.register_participants\(\) first\.",
match=r"Either participants or participant_factories must be provided\.",
):
GroupChatBuilder().with_orchestrator(selection_func=selector).build()
GroupChatBuilder()
def test_group_chat_builder_rejects_mixing_participants_and_factories():
"""Test that mixing .participants() and .register_participants() raises an error."""
"""Test that passing both participants and participant_factories to the constructor raises an error."""
alpha = StubAgent("alpha", "reply from alpha")
# Case 1: participants first, then register_participants
with pytest.raises(ValueError, match="Cannot mix .participants"):
GroupChatBuilder().participants([alpha]).register_participants([lambda: StubAgent("beta", "reply from beta")])
# Case 2: register_participants first, then participants
with pytest.raises(ValueError, match="Cannot mix .participants"):
GroupChatBuilder().register_participants([lambda: alpha]).participants([StubAgent("beta", "reply from beta")])
def test_group_chat_builder_rejects_multiple_calls_to_register_participants():
"""Test that multiple calls to .register_participants() raises an error."""
with pytest.raises(
ValueError, match=r"register_participants\(\) has already been called on this builder instance."
):
(
GroupChatBuilder()
.register_participants([lambda: StubAgent("alpha", "reply from alpha")])
.register_participants([lambda: StubAgent("beta", "reply from beta")])
with pytest.raises(ValueError, match="Cannot provide both participants and participant_factories"):
GroupChatBuilder(
participants=[alpha],
participant_factories=[lambda: StubAgent("beta", "reply from beta")],
)
def test_group_chat_builder_rejects_multiple_calls_to_participants():
"""Test that multiple calls to .participants() raises an error."""
with pytest.raises(ValueError, match="participants have already been set"):
(
GroupChatBuilder()
.participants([StubAgent("alpha", "reply from alpha")])
.participants([StubAgent("beta", "reply from beta")])
def test_group_chat_builder_rejects_both_factories_and_participants():
"""Test that passing both participant_factories and participants raises an error."""
with pytest.raises(ValueError, match="Cannot provide both participants and participant_factories"):
GroupChatBuilder(
participant_factories=[lambda: StubAgent("alpha", "reply from alpha")],
participants=[StubAgent("beta", "reply from beta")],
)
def test_group_chat_builder_rejects_both_participants_and_factories():
"""Test that passing both participants and participant_factories raises an error."""
with pytest.raises(ValueError, match="Cannot provide both participants and participant_factories"):
GroupChatBuilder(
participants=[StubAgent("alpha", "reply from alpha")],
participant_factories=[lambda: StubAgent("beta", "reply from beta")],
)
@@ -913,13 +839,11 @@ async def test_group_chat_with_participant_factories():
selector = make_sequence_selector()
workflow = (
GroupChatBuilder()
.register_participants([create_alpha, create_beta])
.with_orchestrator(selection_func=selector)
.with_max_rounds(2)
.build()
)
workflow = GroupChatBuilder(
participant_factories=[create_alpha, create_beta],
max_rounds=2,
selection_func=selector,
).build()
# Factories should be called during build
assert call_count == 2
@@ -948,12 +872,7 @@ async def test_group_chat_participant_factories_reusable_builder():
selector = make_sequence_selector()
builder = (
GroupChatBuilder()
.register_participants([create_alpha, create_beta])
.with_orchestrator(selection_func=selector)
.with_max_rounds(2)
)
builder = GroupChatBuilder(participant_factories=[create_alpha, create_beta], max_rounds=2, selection_func=selector)
# Build first workflow
wf1 = builder.build()
@@ -980,14 +899,12 @@ async def test_group_chat_participant_factories_with_checkpointing():
selector = make_sequence_selector()
workflow = (
GroupChatBuilder()
.register_participants([create_alpha, create_beta])
.with_orchestrator(selection_func=selector)
.with_checkpointing(storage)
.with_max_rounds(2)
.build()
)
workflow = GroupChatBuilder(
participant_factories=[create_alpha, create_beta],
checkpoint_storage=storage,
max_rounds=2,
selection_func=selector,
).build()
outputs: list[WorkflowEvent] = []
async for event in workflow.run("checkpoint test", stream=True):
@@ -1014,16 +931,15 @@ def test_group_chat_builder_rejects_multiple_orchestrator_configurations():
def agent_factory() -> ChatAgent:
return cast(ChatAgent, StubManagerAgent())
builder = GroupChatBuilder().with_orchestrator(selection_func=selector)
agent = StubAgent("test", "response")
# Already has a selection_func, should fail on second call
with pytest.raises(ValueError, match=r"A selection function has already been configured"):
builder.with_orchestrator(selection_func=selector)
# Both selection_func and orchestrator_agent provided simultaneously - should fail
with pytest.raises(ValueError, match=r"Exactly one of"):
GroupChatBuilder(participants=[agent], selection_func=selector, orchestrator_agent=StubManagerAgent())
# Test with agent_factory
builder2 = GroupChatBuilder().with_orchestrator(agent=agent_factory)
with pytest.raises(ValueError, match=r"A factory has already been configured"):
builder2.with_orchestrator(agent=agent_factory)
# Test with agent_factory - already has factory, should fail with second config
with pytest.raises(ValueError, match=r"Exactly one of"):
GroupChatBuilder(participants=[agent], orchestrator_agent=agent_factory, selection_func=selector)
def test_group_chat_builder_requires_exactly_one_orchestrator_option():
@@ -1035,13 +951,15 @@ def test_group_chat_builder_requires_exactly_one_orchestrator_option():
def agent_factory() -> ChatAgent:
return cast(ChatAgent, StubManagerAgent())
# No options provided
with pytest.raises(ValueError, match="Exactly one of"):
GroupChatBuilder().with_orchestrator() # type: ignore
agent = StubAgent("test", "response")
# No orchestrator options provided - only fails at build() time
with pytest.raises(ValueError, match="No orchestrator has been configured"):
GroupChatBuilder(participants=[agent]).build()
# Multiple options provided
with pytest.raises(ValueError, match="Exactly one of"):
GroupChatBuilder().with_orchestrator(selection_func=selector, agent=agent_factory) # type: ignore
GroupChatBuilder(participants=[agent], selection_func=selector, orchestrator_agent=agent_factory)
async def test_group_chat_with_orchestrator_factory_returning_chat_agent():
@@ -1112,7 +1030,7 @@ async def test_group_chat_with_orchestrator_factory_returning_chat_agent():
alpha = StubAgent("alpha", "reply from alpha")
beta = StubAgent("beta", "reply from beta")
workflow = GroupChatBuilder().participants([alpha, beta]).with_orchestrator(agent=agent_factory).build()
workflow = GroupChatBuilder(participants=[alpha, beta], orchestrator_agent=agent_factory).build()
# Factory should be called during build
assert factory_call_count == 1
@@ -1156,7 +1074,7 @@ def test_group_chat_with_orchestrator_factory_returning_base_orchestrator():
alpha = StubAgent("alpha", "reply from alpha")
workflow = GroupChatBuilder().participants([alpha]).with_orchestrator(orchestrator=orchestrator_factory).build()
workflow = GroupChatBuilder(participants=[alpha], orchestrator=orchestrator_factory).build()
# Factory should be called during build
assert factory_call_count == 1
@@ -1176,7 +1094,7 @@ async def test_group_chat_orchestrator_factory_reusable_builder():
alpha = StubAgent("alpha", "reply from alpha")
beta = StubAgent("beta", "reply from beta")
builder = GroupChatBuilder().participants([alpha, beta]).with_orchestrator(agent=agent_factory)
builder = GroupChatBuilder(participants=[alpha, beta], orchestrator_agent=agent_factory)
# Build first workflow
wf1 = builder.build()
@@ -1202,13 +1120,13 @@ def test_group_chat_orchestrator_factory_invalid_return_type():
TypeError,
match=r"Orchestrator factory must return ChatAgent or BaseGroupChatOrchestrator instance",
):
(GroupChatBuilder().participants([alpha]).with_orchestrator(orchestrator=invalid_factory).build())
GroupChatBuilder(participants=[alpha], orchestrator=invalid_factory).build()
with pytest.raises(
TypeError,
match=r"Orchestrator factory must return ChatAgent or BaseGroupChatOrchestrator instance",
):
(GroupChatBuilder().participants([alpha]).with_orchestrator(agent=invalid_factory).build())
GroupChatBuilder(participants=[alpha], orchestrator_agent=invalid_factory).build()
def test_group_chat_with_both_participant_and_orchestrator_factories():
@@ -1231,12 +1149,10 @@ def test_group_chat_with_both_participant_and_orchestrator_factories():
agent_factory_call_count += 1
return cast(ChatAgent, StubManagerAgent())
workflow = (
GroupChatBuilder()
.register_participants([create_alpha, create_beta])
.with_orchestrator(agent=agent_factory)
.build()
)
workflow = GroupChatBuilder(
participant_factories=[create_alpha, create_beta],
orchestrator_agent=agent_factory,
).build()
# All factories should be called during build
assert participant_factory_call_count == 2
@@ -1268,9 +1184,7 @@ async def test_group_chat_factories_reusable_for_multiple_workflows():
agent_factory_call_count += 1
return cast(ChatAgent, StubManagerAgent())
builder = (
GroupChatBuilder().register_participants([create_alpha, create_beta]).with_orchestrator(agent=agent_factory)
)
builder = GroupChatBuilder(participant_factories=[create_alpha, create_beta], orchestrator_agent=agent_factory)
# Build first workflow
wf1 = builder.build()
@@ -140,9 +140,11 @@ async def test_handoff():
# Without explicitly defining handoffs, the builder will create connections
# between all agents.
workflow = (
HandoffBuilder(participants=[triage, specialist, escalation])
HandoffBuilder(
participants=[triage, specialist, escalation],
termination_condition=lambda conv: sum(1 for m in conv if m.role == "user") >= 2,
)
.with_start_agent(triage)
.with_termination_condition(lambda conv: sum(1 for m in conv if m.role == "user") >= 2)
.build()
)
@@ -166,7 +168,15 @@ async def test_autonomous_mode_yields_output_without_user_request():
specialist = MockHandoffAgent(name="specialist")
workflow = (
HandoffBuilder(participants=[triage, specialist])
HandoffBuilder(
participants=[triage, specialist],
# This termination condition ensures the workflow runs through both agents.
# First message is the user message to triage, second is triage's response, which
# is a handoff to specialist, third is specialist's response that should not request
# user input due to autonomous mode. Fourth message will come from the specialist
# again and will trigger termination.
termination_condition=lambda conv: len(conv) >= 4,
)
.with_start_agent(triage)
# Since specialist has no handoff, the specialist will be generating normal responses.
# With autonomous mode, this should continue until the termination condition is met.
@@ -174,12 +184,6 @@ async def test_autonomous_mode_yields_output_without_user_request():
agents=[specialist],
turn_limits={resolve_agent_id(specialist): 1},
)
# This termination condition ensures the workflow runs through both agents.
# First message is the user message to triage, second is triage's response, which
# is a handoff to specialist, third is specialist's response that should not request
# user input due to autonomous mode. Fourth message will come from the specialist
# again and will trigger termination.
.with_termination_condition(lambda conv: len(conv) >= 4)
.build()
)
@@ -202,10 +206,9 @@ async def test_autonomous_mode_resumes_user_input_on_turn_limit():
worker = MockHandoffAgent(name="worker")
workflow = (
HandoffBuilder(participants=[triage, worker])
HandoffBuilder(participants=[triage, worker], termination_condition=lambda conv: False)
.with_start_agent(triage)
.with_autonomous_mode(agents=[worker], turn_limits={resolve_agent_id(worker): 2})
.with_termination_condition(lambda conv: False)
.build()
)
@@ -246,9 +249,8 @@ async def test_handoff_async_termination_condition() -> None:
worker = MockHandoffAgent(name="worker")
workflow = (
HandoffBuilder(participants=[coordinator, worker])
HandoffBuilder(participants=[coordinator, worker], termination_condition=async_termination)
.with_start_agent(coordinator)
.with_termination_condition(async_termination)
.build()
)
@@ -537,9 +539,11 @@ async def test_handoff_with_participant_factories():
return MockHandoffAgent(name="specialist")
workflow = (
HandoffBuilder(participant_factories={"triage": create_triage, "specialist": create_specialist})
HandoffBuilder(
participant_factories={"triage": create_triage, "specialist": create_specialist},
termination_condition=lambda conv: sum(1 for m in conv if m.role == "user") >= 2,
)
.with_start_agent("triage")
.with_termination_condition(lambda conv: sum(1 for m in conv if m.role == "user") >= 2)
.build()
)
@@ -607,12 +611,12 @@ async def test_handoff_with_participant_factories_and_add_handoff():
"triage": create_triage,
"specialist_a": create_specialist_a,
"specialist_b": create_specialist_b,
}
},
termination_condition=lambda conv: sum(1 for m in conv if m.role == "user") >= 3,
)
.with_start_agent("triage")
.add_handoff("triage", ["specialist_a", "specialist_b"])
.add_handoff("specialist_a", ["specialist_b"])
.with_termination_condition(lambda conv: sum(1 for m in conv if m.role == "user") >= 3)
.build()
)
@@ -650,10 +654,12 @@ async def test_handoff_participant_factories_with_checkpointing():
return MockHandoffAgent(name="specialist")
workflow = (
HandoffBuilder(participant_factories={"triage": create_triage, "specialist": create_specialist})
HandoffBuilder(
participant_factories={"triage": create_triage, "specialist": create_specialist},
checkpoint_storage=storage,
termination_condition=lambda conv: sum(1 for m in conv if m.role == "user") >= 2,
)
.with_start_agent("triage")
.with_checkpointing(storage)
.with_termination_condition(lambda conv: sum(1 for m in conv if m.role == "user") >= 2)
.build()
)
@@ -186,7 +186,7 @@ async def test_magentic_builder_returns_workflow_and_runs() -> None:
manager = FakeManager()
agent = StubAgent(manager.next_speaker_name, "first draft")
workflow = MagenticBuilder().participants([agent]).with_manager(manager=manager).build()
workflow = MagenticBuilder(participants=[agent], manager=manager).build()
assert isinstance(workflow, Workflow)
@@ -212,7 +212,7 @@ async def test_magentic_as_agent_does_not_accept_conversation() -> None:
manager = FakeManager()
writer = StubAgent(manager.next_speaker_name, "summary response")
workflow = MagenticBuilder().participants([writer]).with_manager(manager=manager).build()
workflow = MagenticBuilder(participants=[writer], manager=manager).build()
agent = workflow.as_agent(name="magentic-agent")
conversation = [
@@ -240,7 +240,7 @@ async def test_standard_manager_plan_and_replan_combined_ledger():
async def test_magentic_workflow_plan_review_approval_to_completion():
manager = FakeManager()
wf = MagenticBuilder().participants([DummyExec("agentA")]).with_manager(manager=manager).with_plan_review().build()
wf = MagenticBuilder(participants=[DummyExec("agentA")], enable_plan_review=True, manager=manager).build()
req_event: WorkflowEvent | None = None
async for ev in wf.run("do work", stream=True):
@@ -278,13 +278,11 @@ async def test_magentic_plan_review_with_revise():
return await super().replan(magentic_context)
manager = CountingManager()
wf = (
MagenticBuilder()
.participants([DummyExec(name=manager.next_speaker_name)])
.with_manager(manager=manager)
.with_plan_review()
.build()
)
wf = MagenticBuilder(
participants=[DummyExec(name=manager.next_speaker_name)],
enable_plan_review=True,
manager=manager,
).build()
# Wait for the initial plan review request
req_event: WorkflowEvent | None = None
@@ -324,12 +322,7 @@ async def test_magentic_plan_review_with_revise():
async def test_magentic_orchestrator_round_limit_produces_partial_result():
manager = FakeManager(max_round_count=1)
wf = (
MagenticBuilder()
.participants([DummyExec(name=manager.next_speaker_name)])
.with_manager(manager=manager)
.build()
)
wf = MagenticBuilder(participants=[DummyExec(name=manager.next_speaker_name)], manager=manager).build()
events: list[WorkflowEvent] = []
async for ev in wf.run("round limit test", stream=True):
@@ -354,14 +347,12 @@ async def test_magentic_checkpoint_resume_round_trip():
storage = InMemoryCheckpointStorage()
manager1 = FakeManager()
wf = (
MagenticBuilder()
.participants([DummyExec(name=manager1.next_speaker_name)])
.with_manager(manager=manager1)
.with_plan_review()
.with_checkpointing(storage)
.build()
)
wf = MagenticBuilder(
participants=[DummyExec(name=manager1.next_speaker_name)],
enable_plan_review=True,
checkpoint_storage=storage,
manager=manager1,
).build()
task_text = "checkpoint task"
req_event: WorkflowEvent | None = None
@@ -377,14 +368,12 @@ async def test_magentic_checkpoint_resume_round_trip():
resume_checkpoint = checkpoints[-1]
manager2 = FakeManager()
wf_resume = (
MagenticBuilder()
.participants([DummyExec(name=manager2.next_speaker_name)])
.with_manager(manager=manager2)
.with_plan_review()
.with_checkpointing(storage)
.build()
)
wf_resume = MagenticBuilder(
participants=[DummyExec(name=manager2.next_speaker_name)],
enable_plan_review=True,
checkpoint_storage=storage,
manager=manager2,
).build()
completed: WorkflowEvent | None = None
req_event = None
@@ -580,13 +569,7 @@ class StubAssistantsAgent(BaseAgent):
async def _collect_agent_responses_setup(participant: SupportsAgentRun) -> list[ChatMessage]:
captured: list[ChatMessage] = []
wf = (
MagenticBuilder()
.participants([participant])
.with_manager(manager=InvokeOnceManager())
.with_intermediate_outputs()
.build()
)
wf = MagenticBuilder(participants=[participant], intermediate_outputs=True, manager=InvokeOnceManager()).build()
# Run a bounded stream to allow one invoke and then completion
events: list[WorkflowEvent] = []
@@ -632,13 +615,9 @@ async def _collect_checkpoints(
async def test_magentic_checkpoint_resume_inner_loop_superstep():
storage = InMemoryCheckpointStorage()
workflow = (
MagenticBuilder()
.participants([StubThreadAgent()])
.with_manager(manager=InvokeOnceManager())
.with_checkpointing(storage)
.build()
)
workflow = MagenticBuilder(
participants=[StubThreadAgent()], checkpoint_storage=storage, manager=InvokeOnceManager()
).build()
async for event in workflow.run("inner-loop task", stream=True):
if event.type == "output":
@@ -647,13 +626,9 @@ async def test_magentic_checkpoint_resume_inner_loop_superstep():
checkpoints = await _collect_checkpoints(storage)
inner_loop_checkpoint = next(cp for cp in checkpoints if cp.metadata.get("superstep") == 1) # type: ignore[reportUnknownMemberType]
resumed = (
MagenticBuilder()
.participants([StubThreadAgent()])
.with_manager(manager=InvokeOnceManager())
.with_checkpointing(storage)
.build()
)
resumed = MagenticBuilder(
participants=[StubThreadAgent()], checkpoint_storage=storage, manager=InvokeOnceManager()
).build()
completed: WorkflowEvent | None = None
async for event in resumed.run(checkpoint_id=inner_loop_checkpoint.checkpoint_id, stream=True): # type: ignore[reportUnknownMemberType]
@@ -670,13 +645,7 @@ async def test_magentic_checkpoint_resume_from_saved_state():
# Use the working InvokeOnceManager first to get a completed workflow
manager = InvokeOnceManager()
workflow = (
MagenticBuilder()
.participants([StubThreadAgent()])
.with_manager(manager=manager)
.with_checkpointing(storage)
.build()
)
workflow = MagenticBuilder(participants=[StubThreadAgent()], checkpoint_storage=storage, manager=manager).build()
async for event in workflow.run("checkpoint resume task", stream=True):
if event.type == "output":
@@ -687,13 +656,9 @@ async def test_magentic_checkpoint_resume_from_saved_state():
# Verify we can resume from the last saved checkpoint
resumed_state = checkpoints[-1] # Use the last checkpoint
resumed_workflow = (
MagenticBuilder()
.participants([StubThreadAgent()])
.with_manager(manager=InvokeOnceManager())
.with_checkpointing(storage)
.build()
)
resumed_workflow = MagenticBuilder(
participants=[StubThreadAgent()], checkpoint_storage=storage, manager=InvokeOnceManager()
).build()
completed: WorkflowEvent | None = None
async for event in resumed_workflow.run(checkpoint_id=resumed_state.checkpoint_id, stream=True):
@@ -708,14 +673,12 @@ async def test_magentic_checkpoint_resume_rejects_participant_renames():
manager = InvokeOnceManager()
workflow = (
MagenticBuilder()
.participants([StubThreadAgent()])
.with_manager(manager=manager)
.with_plan_review()
.with_checkpointing(storage)
.build()
)
workflow = MagenticBuilder(
participants=[StubThreadAgent()],
enable_plan_review=True,
checkpoint_storage=storage,
manager=manager,
).build()
req_event: WorkflowEvent | None = None
async for event in workflow.run("task", stream=True):
@@ -728,14 +691,12 @@ async def test_magentic_checkpoint_resume_rejects_participant_renames():
checkpoints = await _collect_checkpoints(storage)
target_checkpoint = checkpoints[-1]
renamed_workflow = (
MagenticBuilder()
.participants([StubThreadAgent(name="renamedAgent")])
.with_manager(manager=InvokeOnceManager())
.with_plan_review()
.with_checkpointing(storage)
.build()
)
renamed_workflow = MagenticBuilder(
participants=[StubThreadAgent(name="renamedAgent")],
enable_plan_review=True,
checkpoint_storage=storage,
manager=InvokeOnceManager(),
).build()
with pytest.raises(WorkflowCheckpointException, match="Workflow graph has changed"):
async for _ in renamed_workflow.run(
@@ -772,7 +733,7 @@ class NotProgressingManager(MagenticManagerBase):
async def test_magentic_stall_and_reset_reach_limits():
manager = NotProgressingManager(max_round_count=10, max_stall_count=0, max_reset_count=1)
wf = MagenticBuilder().participants([DummyExec("agentA")]).with_manager(manager=manager).build()
wf = MagenticBuilder(participants=[DummyExec("agentA")], manager=manager).build()
events: list[WorkflowEvent] = []
async for ev in wf.run("test limits", stream=True):
@@ -797,7 +758,7 @@ async def test_magentic_checkpoint_runtime_only() -> None:
storage = InMemoryCheckpointStorage()
manager = FakeManager(max_round_count=10)
wf = MagenticBuilder().participants([DummyExec("agentA")]).with_manager(manager=manager).build()
wf = MagenticBuilder(participants=[DummyExec("agentA")], manager=manager).build()
baseline_output: ChatMessage | None = None
async for ev in wf.run("runtime checkpoint test", checkpoint_storage=storage, stream=True):
@@ -829,13 +790,9 @@ async def test_magentic_checkpoint_runtime_overrides_buildtime() -> None:
runtime_storage = FileCheckpointStorage(temp_dir2)
manager = FakeManager(max_round_count=10)
wf = (
MagenticBuilder()
.participants([DummyExec("agentA")])
.with_manager(manager=manager)
.with_checkpointing(buildtime_storage)
.build()
)
wf = MagenticBuilder(
participants=[DummyExec("agentA")], checkpoint_storage=buildtime_storage, manager=manager
).build()
baseline_output: ChatMessage | None = None
async for ev in wf.run("override test", checkpoint_storage=runtime_storage, stream=True):
@@ -884,13 +841,7 @@ async def test_magentic_checkpoint_restore_no_duplicate_history():
manager = FakeManager(max_round_count=10)
storage = InMemoryCheckpointStorage()
wf = (
MagenticBuilder()
.participants([DummyExec("agentA")])
.with_manager(manager=manager)
.with_checkpointing(storage)
.build()
)
wf = MagenticBuilder(participants=[DummyExec("agentA")], checkpoint_storage=storage, manager=manager).build()
# Run with conversation history to create initial checkpoint
conversation: list[ChatMessage] = [
@@ -947,47 +898,41 @@ async def test_magentic_checkpoint_restore_no_duplicate_history():
def test_magentic_builder_rejects_empty_participant_factories():
"""Test that MagenticBuilder rejects empty participant_factories list."""
with pytest.raises(ValueError, match=r"participant_factories cannot be empty"):
MagenticBuilder().register_participants([])
MagenticBuilder(participant_factories=[])
with pytest.raises(
ValueError,
match=r"No participants provided\. Call \.participants\(\) or \.register_participants\(\) first\.",
match=r"Either participants or participant_factories must be provided\.",
):
MagenticBuilder().with_manager(manager=FakeManager()).build()
MagenticBuilder()
def test_magentic_builder_rejects_mixing_participants_and_factories():
"""Test that mixing .participants() and .register_participants() raises an error."""
"""Test that passing both participants and participant_factories to the constructor raises an error."""
agent = StubAgent("agentA", "reply from agentA")
# Case 1: participants first, then register_participants
with pytest.raises(ValueError, match="Cannot mix .participants"):
MagenticBuilder().participants([agent]).register_participants([lambda: StubAgent("agentB", "reply")])
# Case 2: register_participants first, then participants
with pytest.raises(ValueError, match="Cannot mix .participants"):
MagenticBuilder().register_participants([lambda: agent]).participants([StubAgent("agentB", "reply")])
def test_magentic_builder_rejects_multiple_calls_to_register_participants():
"""Test that multiple calls to .register_participants() raises an error."""
with pytest.raises(
ValueError, match=r"register_participants\(\) has already been called on this builder instance."
):
(
MagenticBuilder()
.register_participants([lambda: StubAgent("agentA", "reply from agentA")])
.register_participants([lambda: StubAgent("agentB", "reply from agentB")])
with pytest.raises(ValueError, match="Cannot provide both participants and participant_factories"):
MagenticBuilder(
participants=[agent],
participant_factories=[lambda: StubAgent("agentB", "reply")],
)
def test_magentic_builder_rejects_multiple_calls_to_participants():
"""Test that multiple calls to .participants() raises an error."""
with pytest.raises(ValueError, match="participants have already been set"):
(
MagenticBuilder()
.participants([StubAgent("agentA", "reply from agentA")])
.participants([StubAgent("agentB", "reply from agentB")])
def test_magentic_builder_rejects_both_factories_and_participants():
"""Test that passing both participant_factories and participants raises an error."""
with pytest.raises(ValueError, match="Cannot provide both participants and participant_factories"):
MagenticBuilder(
participant_factories=[lambda: StubAgent("agentA", "reply from agentA")],
participants=[StubAgent("agentB", "reply from agentB")],
)
def test_magentic_builder_rejects_both_participants_and_factories():
"""Test that passing both participants and participant_factories raises an error."""
with pytest.raises(ValueError, match="Cannot provide both participants and participant_factories"):
MagenticBuilder(
participants=[StubAgent("agentA", "reply from agentA")],
participant_factories=[lambda: StubAgent("agentB", "reply from agentB")],
)
@@ -1001,7 +946,7 @@ async def test_magentic_with_participant_factories():
return StubAgent("agentA", "reply from agentA")
manager = FakeManager()
workflow = MagenticBuilder().register_participants([create_agent]).with_manager(manager=manager).build()
workflow = MagenticBuilder(participant_factories=[create_agent], manager=manager).build()
# Factory should be called during build
assert call_count == 1
@@ -1023,7 +968,7 @@ async def test_magentic_participant_factories_reusable_builder():
call_count += 1
return StubAgent("agentA", "reply from agentA")
builder = MagenticBuilder().register_participants([create_agent]).with_manager(manager=FakeManager())
builder = MagenticBuilder(participant_factories=[create_agent], manager=FakeManager())
# Build first workflow
wf1 = builder.build()
@@ -1045,13 +990,9 @@ async def test_magentic_participant_factories_with_checkpointing():
return StubAgent("agentA", "reply from agentA")
manager = FakeManager()
workflow = (
MagenticBuilder()
.register_participants([create_agent])
.with_manager(manager=manager)
.with_checkpointing(storage)
.build()
)
workflow = MagenticBuilder(
participant_factories=[create_agent], checkpoint_storage=storage, manager=manager
).build()
outputs: list[WorkflowEvent] = []
async for event in workflow.run("checkpoint test", stream=True):
@@ -1072,27 +1013,27 @@ async def test_magentic_participant_factories_with_checkpointing():
def test_magentic_builder_rejects_multiple_manager_configurations():
"""Test that configuring multiple managers raises ValueError."""
manager = FakeManager()
agent = StubAgent("agentA", "reply")
builder = MagenticBuilder().with_manager(manager=manager)
with pytest.raises(ValueError, match=r"with_manager\(\) has already been called"):
builder.with_manager(manager=manager)
with pytest.raises(ValueError, match=r"Exactly one of"):
MagenticBuilder(participants=[agent], manager=manager, manager_agent=StubManagerAgent())
def test_magentic_builder_requires_exactly_one_manager_option():
"""Test that exactly one manager option must be provided."""
manager = FakeManager()
agent = StubAgent("agentA", "reply")
def manager_factory() -> MagenticManagerBase:
return FakeManager()
# No options provided
with pytest.raises(ValueError, match="Exactly one of"):
MagenticBuilder().with_manager() # type: ignore
# No options provided - only fails at build() time
with pytest.raises(ValueError, match="No manager configured"):
MagenticBuilder(participants=[agent]).build()
# Multiple options provided
with pytest.raises(ValueError, match="Exactly one of"):
MagenticBuilder().with_manager(manager=manager, manager_factory=manager_factory) # type: ignore
MagenticBuilder(participants=[agent], manager=manager, manager_factory=manager_factory)
async def test_magentic_with_manager_factory():
@@ -1105,7 +1046,7 @@ async def test_magentic_with_manager_factory():
return FakeManager()
agent = StubAgent("agentA", "reply from agentA")
workflow = MagenticBuilder().participants([agent]).with_manager(manager_factory=manager_factory).build()
workflow = MagenticBuilder(participants=[agent], manager_factory=manager_factory).build()
# Factory should be called during build
assert factory_call_count == 1
@@ -1128,12 +1069,9 @@ async def test_magentic_with_agent_factory():
return cast(SupportsAgentRun, StubManagerAgent())
participant = StubAgent("agentA", "reply from agentA")
workflow = (
MagenticBuilder()
.participants([participant])
.with_manager(agent_factory=agent_factory, max_round_count=1)
.build()
)
workflow = MagenticBuilder(
participants=[participant], manager_agent_factory=agent_factory, max_round_count=1
).build()
# Factory should be called during build
assert factory_call_count == 1
@@ -1158,7 +1096,7 @@ async def test_magentic_manager_factory_reusable_builder():
return FakeManager()
agent = StubAgent("agentA", "reply from agentA")
builder = MagenticBuilder().participants([agent]).with_manager(manager_factory=manager_factory)
builder = MagenticBuilder(participants=[agent], manager_factory=manager_factory)
# Build first workflow
wf1 = builder.build()
@@ -1189,9 +1127,7 @@ def test_magentic_with_both_participant_and_manager_factories():
manager_factory_call_count += 1
return FakeManager()
workflow = (
MagenticBuilder().register_participants([create_agent]).with_manager(manager_factory=manager_factory).build()
)
workflow = MagenticBuilder(participant_factories=[create_agent], manager_factory=manager_factory).build()
# All factories should be called during build
assert participant_factory_call_count == 1
@@ -1216,7 +1152,7 @@ async def test_magentic_factories_reusable_for_multiple_workflows():
manager_factory_call_count += 1
return FakeManager()
builder = MagenticBuilder().register_participants([create_agent]).with_manager(manager_factory=manager_factory)
builder = MagenticBuilder(participant_factories=[create_agent], manager_factory=manager_factory)
# Build first workflow
wf1 = builder.build()
@@ -1266,25 +1202,21 @@ def test_magentic_agent_factory_with_standard_manager_options():
)
participant = StubAgent("agentA", "reply from agentA")
workflow = (
MagenticBuilder()
.participants([participant])
.with_manager(
agent_factory=agent_factory,
task_ledger=custom_task_ledger,
max_stall_count=custom_max_stall_count,
max_reset_count=custom_max_reset_count,
max_round_count=custom_max_round_count,
task_ledger_facts_prompt=custom_facts_prompt,
task_ledger_plan_prompt=custom_plan_prompt,
task_ledger_full_prompt=custom_full_prompt,
task_ledger_facts_update_prompt=custom_facts_update_prompt,
task_ledger_plan_update_prompt=custom_plan_update_prompt,
progress_ledger_prompt=custom_progress_prompt,
final_answer_prompt=custom_final_prompt,
)
.build()
)
workflow = MagenticBuilder(
participants=[participant],
manager_agent_factory=agent_factory,
task_ledger=custom_task_ledger,
max_stall_count=custom_max_stall_count,
max_reset_count=custom_max_reset_count,
max_round_count=custom_max_round_count,
task_ledger_facts_prompt=custom_facts_prompt,
task_ledger_plan_prompt=custom_plan_prompt,
task_ledger_full_prompt=custom_full_prompt,
task_ledger_facts_update_prompt=custom_facts_update_prompt,
task_ledger_plan_update_prompt=custom_plan_update_prompt,
progress_ledger_prompt=custom_progress_prompt,
final_answer_prompt=custom_final_prompt,
).build()
# Factory should be called during build
assert factory_call_count == 1
@@ -68,38 +68,36 @@ class _InvalidExecutor(Executor):
def test_sequential_builder_rejects_empty_participants() -> None:
with pytest.raises(ValueError):
SequentialBuilder().participants([])
SequentialBuilder(participants=[])
def test_sequential_builder_rejects_empty_participant_factories() -> None:
with pytest.raises(ValueError):
SequentialBuilder().register_participants([])
SequentialBuilder(participant_factories=[])
def test_sequential_builder_rejects_mixing_participants_and_factories() -> None:
"""Test that mixing .participants() and .register_participants() raises an error."""
"""Test that passing both participants and participant_factories to the constructor raises an error."""
a1 = _EchoAgent(id="agent1", name="A1")
# Try .participants() then .register_participants()
with pytest.raises(ValueError, match="Cannot mix"):
SequentialBuilder().participants([a1]).register_participants([lambda: _EchoAgent(id="agent2", name="A2")])
# Try .register_participants() then .participants()
with pytest.raises(ValueError, match="Cannot mix"):
SequentialBuilder().register_participants([lambda: _EchoAgent(id="agent1", name="A1")]).participants([a1])
with pytest.raises(ValueError, match="Cannot provide both participants and participant_factories"):
SequentialBuilder(
participants=[a1],
participant_factories=[lambda: _EchoAgent(id="agent2", name="A2")],
)
def test_sequential_builder_validation_rejects_invalid_executor() -> None:
"""Test that adding an invalid executor to the builder raises an error."""
with pytest.raises(TypeCompatibilityError):
SequentialBuilder().participants([_EchoAgent(id="agent1", name="A1"), _InvalidExecutor(id="invalid")]).build()
SequentialBuilder(participants=[_EchoAgent(id="agent1", name="A1"), _InvalidExecutor(id="invalid")]).build()
async def test_sequential_agents_append_to_context() -> None:
a1 = _EchoAgent(id="agent1", name="A1")
a2 = _EchoAgent(id="agent2", name="A2")
wf = SequentialBuilder().participants([a1, a2]).build()
wf = SequentialBuilder(participants=[a1, a2]).build()
completed = False
output: list[ChatMessage] | None = None
@@ -132,7 +130,7 @@ async def test_sequential_register_participants_with_agent_factories() -> None:
def create_agent2() -> _EchoAgent:
return _EchoAgent(id="agent2", name="A2")
wf = SequentialBuilder().register_participants([create_agent1, create_agent2]).build()
wf = SequentialBuilder(participant_factories=[create_agent1, create_agent2]).build()
completed = False
output: list[ChatMessage] | None = None
@@ -158,7 +156,7 @@ async def test_sequential_with_custom_executor_summary() -> None:
a1 = _EchoAgent(id="agent1", name="A1")
summarizer = _SummarizerExec(id="summarizer")
wf = SequentialBuilder().participants([a1, summarizer]).build()
wf = SequentialBuilder(participants=[a1, summarizer]).build()
completed = False
output: list[ChatMessage] | None = None
@@ -189,7 +187,7 @@ async def test_sequential_register_participants_mixed_agents_and_executors() ->
def create_summarizer() -> _SummarizerExec:
return _SummarizerExec(id="summarizer")
wf = SequentialBuilder().register_participants([create_agent, create_summarizer]).build()
wf = SequentialBuilder(participant_factories=[create_agent, create_summarizer]).build()
completed = False
output: list[ChatMessage] | None = None
@@ -215,7 +213,7 @@ async def test_sequential_checkpoint_resume_round_trip() -> None:
storage = InMemoryCheckpointStorage()
initial_agents = (_EchoAgent(id="agent1", name="A1"), _EchoAgent(id="agent2", name="A2"))
wf = SequentialBuilder().participants(list(initial_agents)).with_checkpointing(storage).build()
wf = SequentialBuilder(participants=list(initial_agents), checkpoint_storage=storage).build()
baseline_output: list[ChatMessage] | None = None
async for ev in wf.run("checkpoint sequential", stream=True):
@@ -236,7 +234,7 @@ async def test_sequential_checkpoint_resume_round_trip() -> None:
)
resumed_agents = (_EchoAgent(id="agent1", name="A1"), _EchoAgent(id="agent2", name="A2"))
wf_resume = SequentialBuilder().participants(list(resumed_agents)).with_checkpointing(storage).build()
wf_resume = SequentialBuilder(participants=list(resumed_agents), checkpoint_storage=storage).build()
resumed_output: list[ChatMessage] | None = None
async for ev in wf_resume.run(checkpoint_id=resume_checkpoint.checkpoint_id, stream=True):
@@ -258,7 +256,7 @@ async def test_sequential_checkpoint_runtime_only() -> None:
storage = InMemoryCheckpointStorage()
agents = (_EchoAgent(id="agent1", name="A1"), _EchoAgent(id="agent2", name="A2"))
wf = SequentialBuilder().participants(list(agents)).build()
wf = SequentialBuilder(participants=list(agents)).build()
baseline_output: list[ChatMessage] | None = None
async for ev in wf.run("runtime checkpoint test", checkpoint_storage=storage, stream=True):
@@ -279,7 +277,7 @@ async def test_sequential_checkpoint_runtime_only() -> None:
)
resumed_agents = (_EchoAgent(id="agent1", name="A1"), _EchoAgent(id="agent2", name="A2"))
wf_resume = SequentialBuilder().participants(list(resumed_agents)).build()
wf_resume = SequentialBuilder(participants=list(resumed_agents)).build()
resumed_output: list[ChatMessage] | None = None
async for ev in wf_resume.run(
@@ -309,7 +307,7 @@ async def test_sequential_checkpoint_runtime_overrides_buildtime() -> None:
runtime_storage = FileCheckpointStorage(temp_dir2)
agents = (_EchoAgent(id="agent1", name="A1"), _EchoAgent(id="agent2", name="A2"))
wf = SequentialBuilder().participants(list(agents)).with_checkpointing(buildtime_storage).build()
wf = SequentialBuilder(participants=list(agents), checkpoint_storage=buildtime_storage).build()
baseline_output: list[ChatMessage] | None = None
async for ev in wf.run("override test", checkpoint_storage=runtime_storage, stream=True):
@@ -337,7 +335,7 @@ async def test_sequential_register_participants_with_checkpointing() -> None:
def create_agent2() -> _EchoAgent:
return _EchoAgent(id="agent2", name="A2")
wf = SequentialBuilder().register_participants([create_agent1, create_agent2]).with_checkpointing(storage).build()
wf = SequentialBuilder(participant_factories=[create_agent1, create_agent2], checkpoint_storage=storage).build()
baseline_output: list[ChatMessage] | None = None
async for ev in wf.run("checkpoint with factories", stream=True):
@@ -357,9 +355,9 @@ async def test_sequential_register_participants_with_checkpointing() -> None:
checkpoints[-1],
)
wf_resume = (
SequentialBuilder().register_participants([create_agent1, create_agent2]).with_checkpointing(storage).build()
)
wf_resume = SequentialBuilder(
participant_factories=[create_agent1, create_agent2], checkpoint_storage=storage
).build()
resumed_output: list[ChatMessage] | None = None
async for ev in wf_resume.run(checkpoint_id=resume_checkpoint.checkpoint_id, stream=True):
@@ -385,7 +383,7 @@ async def test_sequential_register_participants_factories_called_on_build() -> N
call_count += 1
return _EchoAgent(id=f"agent{call_count}", name=f"A{call_count}")
builder = SequentialBuilder().register_participants([create_agent, create_agent])
builder = SequentialBuilder(participant_factories=[create_agent, create_agent])
# Factories should not be called yet
assert call_count == 0
@@ -418,7 +416,7 @@ async def test_sequential_builder_reusable_after_build_with_participants() -> No
a1 = _EchoAgent(id="agent1", name="A1")
a2 = _EchoAgent(id="agent2", name="A2")
builder = SequentialBuilder().participants([a1, a2])
builder = SequentialBuilder(participants=[a1, a2])
# Build first workflow
builder.build()
@@ -442,7 +440,7 @@ async def test_sequential_builder_reusable_after_build_with_factories() -> None:
call_count += 1
return _EchoAgent(id="agent2", name="A2")
builder = SequentialBuilder().register_participants([create_agent1, create_agent2])
builder = SequentialBuilder(participant_factories=[create_agent1, create_agent2])
# Build first workflow - factories should be called
builder.build()