[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 06:01:52 +00:00
committed by GitHub
parent 5d355ac507
commit 74ac470a56
132 changed files with 1341 additions and 2386 deletions
@@ -152,7 +152,7 @@ class Workflow(DictConvertible):
Checkpointing can be configured at build time or runtime:
Build-time (via WorkflowBuilder):
workflow = WorkflowBuilder().with_checkpointing(storage).build()
workflow = WorkflowBuilder(checkpoint_storage=storage).build()
Runtime (via run parameters):
result = await workflow.run(message, checkpoint_storage=runtime_storage)
@@ -428,7 +428,7 @@ class Workflow(DictConvertible):
if not has_checkpointing and checkpoint_storage is None:
raise ValueError(
"Cannot restore from checkpoint: either provide checkpoint_storage parameter "
"or build workflow with WorkflowBuilder.with_checkpointing(checkpoint_storage)."
"or build workflow with WorkflowBuilder(checkpoint_storage=checkpoint_storage)."
)
await self._runner.restore_from_checkpoint(checkpoint_id, checkpoint_storage)
@@ -138,11 +138,10 @@ class WorkflowBuilder:
# Build a workflow
workflow = (
WorkflowBuilder()
WorkflowBuilder(start_executor="UpperCase")
.register_executor(lambda: UpperCaseExecutor(id="upper"), name="UpperCase")
.register_executor(lambda: ReverseExecutor(id="reverse"), name="Reverse")
.add_edge("UpperCase", "Reverse")
.set_start_executor("UpperCase")
.build()
)
@@ -156,23 +155,32 @@ class WorkflowBuilder:
max_iterations: int = DEFAULT_MAX_ITERATIONS,
name: str | None = None,
description: str | None = None,
*,
start_executor: Executor | SupportsAgentRun | str,
checkpoint_storage: CheckpointStorage | None = None,
output_executors: list[Executor | SupportsAgentRun | str] | None = None,
):
"""Initialize the WorkflowBuilder with an empty list of edges and no starting executor.
"""Initialize the WorkflowBuilder.
Args:
max_iterations: Maximum number of iterations for workflow convergence. Default is 100.
name: Optional human-readable name for the workflow.
description: Optional description of what the workflow does.
start_executor: The starting executor for the workflow. Can be an Executor instance,
SupportsAgentRun instance, or the name of a registered executor factory.
checkpoint_storage: Optional checkpoint storage for enabling workflow state persistence.
output_executors: Optional list of executors whose outputs should be collected.
If not provided, outputs from all executors are collected.
"""
self._edge_groups: list[EdgeGroup] = []
self._executors: dict[str, Executor] = {}
self._start_executor: Executor | str | None = None
self._checkpoint_storage: CheckpointStorage | None = None
self._checkpoint_storage: CheckpointStorage | None = checkpoint_storage
self._max_iterations: int = max_iterations
self._name: str | None = name
self._description: str | None = description
# Maps underlying SupportsAgentRun object id -> wrapped Executor so we reuse the same wrapper
# across set_start_executor / add_edge calls. This avoids multiple AgentExecutor instances
# across start_executor / add_edge calls. This avoids multiple AgentExecutor instances
# being created for the same agent.
self._agent_wrappers: dict[str, Executor] = {}
@@ -187,7 +195,10 @@ class WorkflowBuilder:
self._executor_registry: dict[str, Callable[[], Executor]] = {}
# Output executors filter; if set, only outputs from these executors are yielded
self._output_executors: list[Executor | SupportsAgentRun | str] = []
self._output_executors: list[Executor | SupportsAgentRun | str] = output_executors if output_executors else []
# Set the start executor
self._set_start_executor(start_executor)
# Agents auto-wrapped by builder now always stream incremental updates.
@@ -279,10 +290,9 @@ class WorkflowBuilder:
# Build a workflow
workflow = (
WorkflowBuilder()
WorkflowBuilder(start_executor="UpperCase")
.register_executor(lambda: UpperCaseExecutor(id="upper"), name="UpperCase")
.register_executor(lambda: ReverseExecutor(id="reverse"), name="Reverse")
.set_start_executor("UpperCase")
.add_edge("UpperCase", "Reverse")
.build()
)
@@ -302,9 +312,8 @@ class WorkflowBuilder:
# Register the same executor factory under multiple names
workflow = (
WorkflowBuilder()
WorkflowBuilder(start_executor="ExecutorA")
.register_executor(lambda: LoggerExecutor(id="logger"), name=["ExecutorA", "ExecutorB"])
.set_start_executor("ExecutorA")
.add_edge("ExecutorA", "ExecutorB")
.build()
"""
@@ -347,7 +356,7 @@ class WorkflowBuilder:
# Build a workflow
workflow = (
WorkflowBuilder()
WorkflowBuilder(start_executor="SomeOtherExecutor")
.register_executor(lambda: ..., name="SomeOtherExecutor")
.register_agent(
lambda: AnthropicAgent(name="writer", model="claude-3-5-sonnet-20241022"),
@@ -355,7 +364,6 @@ class WorkflowBuilder:
output_response=True,
)
.add_edge("SomeOtherExecutor", "WriterAgent")
.set_start_executor("SomeOtherExecutor")
.build()
)
"""
@@ -420,20 +428,18 @@ class WorkflowBuilder:
# Connect executors with an edge
workflow = (
WorkflowBuilder()
WorkflowBuilder(start_executor="ProcessorA")
.register_executor(lambda: ProcessorA(id="a"), name="ProcessorA")
.register_executor(lambda: ProcessorB(id="b"), name="ProcessorB")
.add_edge("ProcessorA", "ProcessorB")
.set_start_executor("ProcessorA")
.build()
)
workflow = (
WorkflowBuilder()
WorkflowBuilder(start_executor="ProcessorA")
.register_executor(lambda: ProcessorA(id="a"), name="ProcessorA")
.register_executor(lambda: ProcessorB(id="b"), name="ProcessorB")
.add_edge("ProcessorA", "ProcessorB", condition=only_large_numbers)
.set_start_executor("ProcessorA")
.build()
)
"""
@@ -507,12 +513,11 @@ class WorkflowBuilder:
# Broadcast to multiple validators
workflow = (
WorkflowBuilder()
WorkflowBuilder(start_executor="DataSource")
.register_executor(lambda: DataSource(id="source"), name="DataSource")
.register_executor(lambda: ValidatorA(id="val_a"), name="ValidatorA")
.register_executor(lambda: ValidatorB(id="val_b"), name="ValidatorB")
.add_fan_out_edges("DataSource", ["ValidatorA", "ValidatorB"])
.set_start_executor("DataSource")
.build()
)
"""
@@ -600,7 +605,7 @@ class WorkflowBuilder:
# Route based on score value
workflow = (
WorkflowBuilder()
WorkflowBuilder(start_executor="Evaluator")
.register_executor(lambda: Evaluator(id="eval"), name="Evaluator")
.register_executor(lambda: HighScoreHandler(id="high"), name="HighScoreHandler")
.register_executor(lambda: LowScoreHandler(id="low"), name="LowScoreHandler")
@@ -611,7 +616,6 @@ class WorkflowBuilder:
Default(target="LowScoreHandler"),
],
)
.set_start_executor("Evaluator")
.build()
)
"""
@@ -714,7 +718,7 @@ class WorkflowBuilder:
workflow = (
WorkflowBuilder()
WorkflowBuilder(start_executor="TaskDispatcher")
.register_executor(lambda: TaskDispatcher(id="dispatcher"), name="TaskDispatcher")
.register_executor(lambda: WorkerA(id="worker_a"), name="WorkerA")
.register_executor(lambda: WorkerB(id="worker_b"), name="WorkerB")
@@ -723,7 +727,6 @@ class WorkflowBuilder:
["WorkerA", "WorkerB"],
selection_func=select_workers,
)
.set_start_executor("TaskDispatcher")
.build()
)
"""
@@ -803,12 +806,11 @@ class WorkflowBuilder:
# Collect results from multiple producers
workflow = (
WorkflowBuilder()
WorkflowBuilder(start_executor="Producer1")
.register_executor(lambda: Producer(id="prod_1"), name="Producer1")
.register_executor(lambda: Producer(id="prod_2"), name="Producer2")
.register_executor(lambda: Aggregator(id="agg"), name="Aggregator")
.add_fan_in_edges(["Producer1", "Producer2"], "Aggregator")
.set_start_executor("Producer1")
.build()
)
"""
@@ -880,12 +882,11 @@ class WorkflowBuilder:
# Chain executors in sequence
workflow = (
WorkflowBuilder()
WorkflowBuilder(start_executor="step1")
.register_executor(lambda: Step1(id="step1"), name="step1")
.register_executor(lambda: Step2(id="step2"), name="step2")
.register_executor(lambda: Step3(id="step3"), name="step3")
.add_chain(["step1", "step2", "step3"])
.set_start_executor("step1")
.build()
)
"""
@@ -911,46 +912,12 @@ class WorkflowBuilder:
self.add_edge(wrapped[i], wrapped[i + 1])
return self
def set_start_executor(self, executor: Executor | SupportsAgentRun | str) -> Self:
"""Set the starting executor for the workflow.
The start executor is the entry point for the workflow. When the workflow is executed,
the initial message will be sent to this executor.
def _set_start_executor(self, executor: Executor | SupportsAgentRun | str) -> None:
"""Set the starting executor for the workflow (internal method).
Args:
executor: The starting executor, which can be an Executor instance, SupportsAgentRun instance,
or the name of a registered executor factory.
Returns:
Self: The WorkflowBuilder instance for method chaining.
Example:
.. code-block:: python
from typing_extensions import Never
from agent_framework import Executor, WorkflowBuilder, WorkflowContext, handler
class EntryPoint(Executor):
@handler
async def process(self, text: str, ctx: WorkflowContext[str]) -> None:
await ctx.send_message(text.upper())
class Processor(Executor):
@handler
async def process(self, text: str, ctx: WorkflowContext[Never, str]) -> None:
await ctx.yield_output(text)
workflow = (
WorkflowBuilder()
.register_executor(lambda: EntryPoint(id="entry"), name="EntryPoint")
.register_executor(lambda: Processor(id="proc"), name="Processor")
.add_edge("EntryPoint", "Processor")
.set_start_executor("EntryPoint")
.build()
)
"""
if self._start_executor is not None:
start_id = self._start_executor if isinstance(self._start_executor, str) else self._start_executor.id
@@ -966,123 +933,9 @@ class WorkflowBuilder:
existing = self._executors.get(wrapped.id)
if existing is not wrapped:
self._add_executor(wrapped)
return self
def set_max_iterations(self, max_iterations: int) -> Self:
"""Set the maximum number of iterations for the workflow.
When a workflow contains cycles, this limit prevents infinite loops by capping
the total number of executor invocations. The default is 100 iterations.
Args:
max_iterations: The maximum number of iterations the workflow will run for convergence.
Returns:
Self: The WorkflowBuilder instance for method chaining.
Example:
.. code-block:: python
from agent_framework import Executor, WorkflowBuilder, WorkflowContext, handler
class StepA(Executor):
@handler
async def process(self, count: int, ctx: WorkflowContext[int]) -> None:
if count < 10:
await ctx.send_message(count + 1)
class StepB(Executor):
@handler
async def process(self, count: int, ctx: WorkflowContext[int]) -> None:
await ctx.send_message(count)
# Set a custom iteration limit for workflow with cycles
workflow = (
WorkflowBuilder()
.set_max_iterations(500)
.register_executor(lambda: StepA(id="step_a"), name="StepA")
.register_executor(lambda: StepB(id="step_b"), name="StepB")
.add_edge("StepA", "StepB")
.add_edge("StepB", "StepA") # Cycle
.set_start_executor("StepA")
.build()
)
"""
self._max_iterations = max_iterations
return self
# Removed explicit set_agent_streaming() API; agents always stream updates.
def with_checkpointing(self, checkpoint_storage: CheckpointStorage) -> Self:
"""Enable checkpointing with the specified storage.
Checkpointing allows workflows to save their state periodically, enabling
pause/resume functionality and recovery from failures. The checkpoint storage
implementation determines where checkpoints are persisted.
Args:
checkpoint_storage: The checkpoint storage implementation to use.
Returns:
Self: The WorkflowBuilder instance for method chaining.
Example:
.. code-block:: python
from typing_extensions import Never
from agent_framework import Executor, WorkflowBuilder, WorkflowContext, handler
from agent_framework import FileCheckpointStorage
class ProcessorA(Executor):
@handler
async def process(self, text: str, ctx: WorkflowContext[str]) -> None:
await ctx.send_message(text.upper())
class ProcessorB(Executor):
@handler
async def process(self, text: str, ctx: WorkflowContext[Never, str]) -> None:
await ctx.yield_output(text)
# Enable checkpointing with file-based storage
storage = FileCheckpointStorage("./checkpoints")
workflow = (
WorkflowBuilder()
.register_executor(lambda: ProcessorA(id="proc_a"), name="ProcessorA")
.register_executor(lambda: ProcessorB(id="proc_b"), name="ProcessorB")
.add_edge("ProcessorA", "ProcessorB")
.set_start_executor("ProcessorA")
.with_checkpointing(storage)
.build()
)
# Run with checkpoint saving
events = await workflow.run("input")
"""
self._checkpoint_storage = checkpoint_storage
return self
def with_output_from(self, executors: list[Executor | SupportsAgentRun | str]) -> Self:
"""Specify which executors' outputs should be collected as workflow outputs.
By default, outputs from all executors are collected. This method allows
filtering to only include outputs from specified executors.
Args:
executors: A list of executors or registered names of the executor factories
whose outputs should be collected.
Returns:
Self: The WorkflowBuilder instance for method chaining.
"""
self._output_executors = list(executors)
return self
def _resolve_edge_registry(self) -> tuple[Executor, dict[str, Executor], list[EdgeGroup]]:
"""Resolve deferred edge registrations into executors and edge groups.
@@ -1097,7 +950,9 @@ class WorkflowBuilder:
as they are already part of the workflow builder's internal state.
"""
if not self._start_executor:
raise ValueError("Starting executor must be set using set_start_executor before building the workflow.")
raise ValueError(
"Starting executor must be set via the start_executor constructor parameter before building."
)
start_executor: Executor | None = None
if isinstance(self._start_executor, Executor):
@@ -1200,9 +1055,8 @@ class WorkflowBuilder:
# Build and execute a workflow
workflow = (
WorkflowBuilder()
WorkflowBuilder(start_executor="MyExecutor")
.register_executor(lambda: MyExecutor(id="executor"), name="MyExecutor")
.set_start_executor("MyExecutor")
.build()
)
@@ -70,7 +70,7 @@ async def test_agent_executor_checkpoint_stores_and_restores_state() -> None:
executor = AgentExecutor(initial_agent, agent_thread=initial_thread)
# Build workflow with checkpointing enabled
wf = SequentialBuilder().participants([executor]).with_checkpointing(storage).build()
wf = SequentialBuilder(participants=[executor], checkpoint_storage=storage).build()
# Run the workflow with a user message
first_run_output: AgentExecutorResponse | None = None
@@ -124,7 +124,7 @@ async def test_agent_executor_checkpoint_stores_and_restores_state() -> None:
assert restored_agent.call_count == 0
# Build new workflow with the restored executor
wf_resume = SequentialBuilder().participants([restored_executor]).with_checkpointing(storage).build()
wf_resume = SequentialBuilder(participants=[restored_executor], checkpoint_storage=storage).build()
# Resume from checkpoint
resumed_output: AgentExecutorResponse | None = None
@@ -96,7 +96,7 @@ async def test_agent_executor_emits_tool_calls_in_streaming_mode() -> None:
agent = _ToolCallingAgent(id="tool_agent", name="ToolAgent")
agent_exec = AgentExecutor(agent, id="tool_exec")
workflow = WorkflowBuilder().set_start_executor(agent_exec).build()
workflow = WorkflowBuilder(start_executor=agent_exec).build()
# Act: run in streaming mode
events: list[WorkflowEvent[AgentResponseUpdate]] = []
@@ -249,11 +249,7 @@ async def test_agent_executor_tool_call_with_approval() -> None:
)
workflow = (
WorkflowBuilder()
.set_start_executor(agent)
.add_edge(agent, test_executor)
.with_output_from([test_executor])
.build()
WorkflowBuilder(start_executor=agent, output_executors=[test_executor]).add_edge(agent, test_executor).build()
)
# Act
@@ -286,7 +282,7 @@ async def test_agent_executor_tool_call_with_approval_streaming() -> None:
tools=[mock_tool_requiring_approval],
)
workflow = WorkflowBuilder().set_start_executor(agent).add_edge(agent, test_executor).build()
workflow = WorkflowBuilder(start_executor=agent).add_edge(agent, test_executor).build()
# Act
request_info_events: list[WorkflowEvent] = []
@@ -324,11 +320,7 @@ async def test_agent_executor_parallel_tool_call_with_approval() -> None:
)
workflow = (
WorkflowBuilder()
.set_start_executor(agent)
.add_edge(agent, test_executor)
.with_output_from([test_executor])
.build()
WorkflowBuilder(start_executor=agent, output_executors=[test_executor]).add_edge(agent, test_executor).build()
)
# Act
@@ -363,7 +355,7 @@ async def test_agent_executor_parallel_tool_call_with_approval_streaming() -> No
tools=[mock_tool_requiring_approval],
)
workflow = WorkflowBuilder().set_start_executor(agent).add_edge(agent, test_executor).build()
workflow = WorkflowBuilder(start_executor=agent).add_edge(agent, test_executor).build()
# Act
request_info_events: list[WorkflowEvent] = []
@@ -30,8 +30,9 @@ def build_workflow(storage: InMemoryCheckpointStorage, finish_id: str = "finish"
start = StartExecutor(id="start")
finish = FinishExecutor(id=finish_id)
builder = WorkflowBuilder(max_iterations=3).set_start_executor(start).add_edge(start, finish)
builder = builder.with_checkpointing(checkpoint_storage=storage)
builder = WorkflowBuilder(max_iterations=3, start_executor=start, checkpoint_storage=storage).add_edge(
start, finish
)
return builder.build()
@@ -153,7 +153,7 @@ async def test_executor_invoked_event_contains_input_data():
upper = UpperCaseExecutor(id="upper")
collector = CollectorExecutor(id="collector")
workflow = WorkflowBuilder().add_edge(upper, collector).set_start_executor(upper).build()
workflow = WorkflowBuilder(start_executor=upper).add_edge(upper, collector).build()
events = await workflow.run("hello world")
invoked_events = [e for e in events if isinstance(e, WorkflowEvent) and e.type == "executor_invoked"]
@@ -190,7 +190,7 @@ async def test_executor_completed_event_contains_sent_messages():
sender = MultiSenderExecutor(id="sender")
collector = CollectorExecutor(id="collector")
workflow = WorkflowBuilder().add_edge(sender, collector).set_start_executor(sender).build()
workflow = WorkflowBuilder(start_executor=sender).add_edge(sender, collector).build()
events = await workflow.run("hello")
completed_events = [e for e in events if isinstance(e, WorkflowEvent) and e.type == "executor_completed"]
@@ -217,7 +217,7 @@ async def test_executor_completed_event_includes_yielded_outputs():
await ctx.yield_output(text.upper())
executor = YieldOnlyExecutor(id="yielder")
workflow = WorkflowBuilder().set_start_executor(executor).build()
workflow = WorkflowBuilder(start_executor=executor).build()
events = await workflow.run("test")
completed_events = [e for e in events if isinstance(e, WorkflowEvent) and e.type == "executor_completed"]
@@ -260,7 +260,7 @@ async def test_executor_events_with_complex_message_types():
processor = ProcessorExecutor(id="processor")
collector = CollectorExecutor(id="collector")
workflow = WorkflowBuilder().add_edge(processor, collector).set_start_executor(processor).build()
workflow = WorkflowBuilder(start_executor=processor).add_edge(processor, collector).build()
input_request = Request(query="hello", limit=3)
events = await workflow.run(input_request)
@@ -539,7 +539,7 @@ async def test_executor_invoked_event_data_not_mutated_by_handler():
# Verify mutation happened
assert len(messages) == original_len + 1
workflow = WorkflowBuilder().set_start_executor(mutator).build()
workflow = WorkflowBuilder(start_executor=mutator).build()
# Run with a single user message
input_messages = [ChatMessage(role="user", text="hello")]
@@ -76,13 +76,7 @@ async def test_agent_executor_populates_full_conversation_non_streaming() -> Non
agent_exec = AgentExecutor(agent, id="agent1-exec")
capturer = _CaptureFullConversation(id="capture")
wf = (
WorkflowBuilder()
.set_start_executor(agent_exec)
.add_edge(agent_exec, capturer)
.with_output_from([capturer])
.build()
)
wf = WorkflowBuilder(start_executor=agent_exec, output_executors=[capturer]).add_edge(agent_exec, capturer).build()
# Act: use run() to test non-streaming mode
result = await wf.run("hello world")
@@ -144,7 +138,7 @@ async def test_sequential_adapter_uses_full_conversation() -> None:
a1 = _CaptureAgent(id="agent1", name="A1", reply_text="A1 reply")
a2 = _CaptureAgent(id="agent2", name="A2", reply_text="A2 reply")
wf = SequentialBuilder().participants([a1, a2]).build()
wf = SequentialBuilder(participants=[a1, a2]).build()
# Act
async for ev in wf.run("hello seq", stream=True):
@@ -236,7 +236,7 @@ class TestFunctionExecutor:
assert reverse_spec["output_types"] == [Any] # First parameter is Any
assert reverse_spec["workflow_output_types"] == [str] # Second parameter is str
workflow = WorkflowBuilder().add_edge(to_upper, reverse_text).set_start_executor(to_upper).build()
workflow = WorkflowBuilder(start_executor=to_upper).add_edge(to_upper, reverse_text).build()
# Run workflow
events = await workflow.run("hello world")
@@ -345,7 +345,7 @@ class TestFunctionExecutor:
# Since single-parameter functions can't send messages,
# they're typically used as terminal nodes or for side effects
WorkflowBuilder().set_start_executor(double_value).build()
WorkflowBuilder(start_executor=double_value).build()
# For testing purposes, we can check that the handler is registered correctly
assert double_value.can_handle(Message(data=5, source_id="mock"))
@@ -178,7 +178,7 @@ class TestRequestInfoAndResponse:
async def test_approval_workflow(self):
"""Test end-to-end workflow with approval request."""
executor = ApprovalRequiredExecutor(id="approval_executor")
workflow = WorkflowBuilder().set_start_executor(executor).build()
workflow = WorkflowBuilder(start_executor=executor).build()
# First run the workflow until it emits a request
request_info_event: WorkflowEvent | None = None
@@ -203,7 +203,7 @@ class TestRequestInfoAndResponse:
async def test_calculation_workflow(self):
"""Test end-to-end workflow with calculation request."""
executor = CalculationExecutor(id="calc_executor")
workflow = WorkflowBuilder().set_start_executor(executor).build()
workflow = WorkflowBuilder(start_executor=executor).build()
# First run the workflow until it emits a calculation request
request_info_event: WorkflowEvent | None = None
@@ -230,7 +230,7 @@ class TestRequestInfoAndResponse:
async def test_multiple_requests_workflow(self):
"""Test workflow with multiple concurrent requests."""
executor = MultiRequestExecutor(id="multi_executor")
workflow = WorkflowBuilder().set_start_executor(executor).build()
workflow = WorkflowBuilder(start_executor=executor).build()
# Collect all request events by running the full stream
request_events: list[WorkflowEvent] = []
@@ -264,7 +264,7 @@ class TestRequestInfoAndResponse:
async def test_denied_approval_workflow(self):
"""Test workflow when approval is denied."""
executor = ApprovalRequiredExecutor(id="approval_executor")
workflow = WorkflowBuilder().set_start_executor(executor).build()
workflow = WorkflowBuilder(start_executor=executor).build()
# First run the workflow until it emits a request
request_info_event: WorkflowEvent | None = None
@@ -287,7 +287,7 @@ class TestRequestInfoAndResponse:
async def test_workflow_state_with_pending_requests(self):
"""Test workflow state when waiting for responses."""
executor = ApprovalRequiredExecutor(id="approval_executor")
workflow = WorkflowBuilder().set_start_executor(executor).build()
workflow = WorkflowBuilder(start_executor=executor).build()
# Run workflow until idle with pending requests
request_info_event: WorkflowEvent | None = None
@@ -312,7 +312,7 @@ class TestRequestInfoAndResponse:
async def test_invalid_calculation_input(self):
"""Test workflow handling of invalid calculation input."""
executor = CalculationExecutor(id="calc_executor")
workflow = WorkflowBuilder().set_start_executor(executor).build()
workflow = WorkflowBuilder(start_executor=executor).build()
# Send invalid input (no numbers)
completed = False
@@ -334,7 +334,7 @@ class TestRequestInfoAndResponse:
# Create workflow with checkpointing enabled
executor = ApprovalRequiredExecutor(id="approval_executor")
workflow = WorkflowBuilder().set_start_executor(executor).with_checkpointing(storage).build()
workflow = WorkflowBuilder(start_executor=executor, checkpoint_storage=storage).build()
# Step 1: Run workflow to completion to ensure checkpoints are created
request_info_event: WorkflowEvent | None = None
@@ -372,7 +372,7 @@ class TestRequestInfoAndResponse:
# Step 4: Create a fresh workflow and restore from checkpoint
new_executor = ApprovalRequiredExecutor(id="approval_executor")
restored_workflow = WorkflowBuilder().set_start_executor(new_executor).with_checkpointing(storage).build()
restored_workflow = WorkflowBuilder(start_executor=new_executor, checkpoint_storage=storage).build()
# Step 5: Resume from checkpoint and verify the request can be continued
completed = False
@@ -413,16 +413,14 @@ class TestSerializationWorkflowClasses:
"""
# Create innermost workflow
inner_executor = SampleExecutor(id="inner-exec")
inner_workflow = WorkflowBuilder().set_start_executor(inner_executor).set_max_iterations(10).build()
inner_workflow = WorkflowBuilder(max_iterations=10, start_executor=inner_executor).build()
# Create middle workflow with WorkflowExecutor
inner_workflow_executor = WorkflowExecutor(workflow=inner_workflow, id="inner-workflow-exec")
middle_executor = SampleExecutor(id="middle-exec")
middle_workflow = (
WorkflowBuilder()
.set_start_executor(middle_executor)
WorkflowBuilder(max_iterations=20, start_executor=middle_executor)
.add_edge(middle_executor, inner_workflow_executor)
.set_max_iterations(20)
.build()
)
@@ -430,10 +428,8 @@ class TestSerializationWorkflowClasses:
middle_workflow_executor = WorkflowExecutor(workflow=middle_workflow, id="middle-workflow-exec")
outer_executor = SampleExecutor(id="outer-exec")
outer_workflow = (
WorkflowBuilder()
.set_start_executor(outer_executor)
WorkflowBuilder(max_iterations=30, start_executor=outer_executor)
.add_edge(outer_executor, middle_workflow_executor)
.set_max_iterations(30)
.build()
)
@@ -543,7 +539,7 @@ class TestSerializationWorkflowClasses:
executor1 = SampleExecutor(id="executor1")
executor2 = SampleExecutor(id="executor2")
workflow = WorkflowBuilder().add_edge(executor1, executor2).set_start_executor(executor1).build()
workflow = WorkflowBuilder(start_executor=executor1).add_edge(executor1, executor2).build()
# Test model_dump
data = workflow.to_dict()
@@ -616,7 +612,7 @@ class TestSerializationWorkflowClasses:
executor1 = SampleExecutor(id="executor1")
executor2 = SampleExecutor(id="executor2")
workflow = WorkflowBuilder().add_edge(executor1, executor2).set_start_executor(executor1).build()
workflow = WorkflowBuilder(start_executor=executor1).add_edge(executor1, executor2).build()
# Test model_dump - should not include private runtime objects
data = workflow.to_dict()
@@ -629,11 +625,11 @@ class TestSerializationWorkflowClasses:
def test_workflow_name_description_serialization(self) -> None:
"""Test that workflow name and description are serialized correctly."""
# Test 1: With name and description
workflow1 = (
WorkflowBuilder(name="Test Pipeline", description="Test workflow description")
.set_start_executor(SampleExecutor(id="e1"))
.build()
)
workflow1 = WorkflowBuilder(
name="Test Pipeline",
description="Test workflow description",
start_executor=SampleExecutor(id="e1"),
).build()
assert workflow1.name == "Test Pipeline"
assert workflow1.description == "Test workflow description"
@@ -649,7 +645,7 @@ class TestSerializationWorkflowClasses:
assert parsed1["description"] == "Test workflow description"
# Test 2: Without name and description (defaults)
workflow2 = WorkflowBuilder().set_start_executor(SampleExecutor(id="e2")).build()
workflow2 = WorkflowBuilder(start_executor=SampleExecutor(id="e2")).build()
assert workflow2.name is None
assert workflow2.description is None
@@ -659,7 +655,7 @@ class TestSerializationWorkflowClasses:
assert "description" not in data2
# Test 3: With only name (no description)
workflow3 = WorkflowBuilder(name="Named Only").set_start_executor(SampleExecutor(id="e3")).build()
workflow3 = WorkflowBuilder(name="Named Only", start_executor=SampleExecutor(id="e3")).build()
assert workflow3.name == "Named Only"
assert workflow3.description is None
@@ -706,8 +702,7 @@ def test_comprehensive_edge_groups_workflow_serialization() -> None:
# Build workflow with all three edge group types
workflow = (
WorkflowBuilder()
.set_start_executor(router)
WorkflowBuilder(start_executor=router)
# 1. SwitchCaseEdgeGroup: Conditional routing
.add_switch_case_edge_group(
router,
@@ -167,8 +167,7 @@ def create_email_validation_workflow() -> Workflow:
email_domain_validator = EmailDomainValidator()
return (
WorkflowBuilder()
.set_start_executor(email_format_validator)
WorkflowBuilder(start_executor=email_format_validator)
.add_edge(email_format_validator, email_domain_validator)
.build()
)
@@ -184,8 +183,7 @@ async def test_basic_sub_workflow() -> None:
workflow_executor = WorkflowExecutor(validation_workflow, "email_validation_workflow")
main_workflow = (
WorkflowBuilder()
.set_start_executor(parent)
WorkflowBuilder(start_executor=parent)
.add_edge(parent, workflow_executor)
.add_edge(workflow_executor, parent)
.build()
@@ -223,8 +221,7 @@ async def test_sub_workflow_with_interception():
workflow_executor = WorkflowExecutor(validation_workflow, "email_workflow")
main_workflow = (
WorkflowBuilder()
.set_start_executor(parent)
WorkflowBuilder(start_executor=parent)
.add_edge(parent, workflow_executor)
.add_edge(workflow_executor, parent)
.build()
@@ -340,8 +337,7 @@ async def test_workflow_scoped_interception() -> None:
executor_b = WorkflowExecutor(workflow_b, "workflow_b")
main_workflow = (
WorkflowBuilder()
.set_start_executor(parent)
WorkflowBuilder(start_executor=parent)
.add_edge(parent, executor_a)
.add_edge(parent, executor_b)
.add_edge(executor_a, parent)
@@ -422,8 +418,7 @@ async def test_concurrent_sub_workflow_execution() -> None:
workflow_executor = WorkflowExecutor(validation_workflow, "email_workflow")
main_workflow = (
WorkflowBuilder()
.set_start_executor(processor)
WorkflowBuilder(start_executor=processor)
.add_edge(processor, workflow_executor)
.add_edge(workflow_executor, processor)
.build()
@@ -564,16 +559,14 @@ class CheckpointTestCoordinator(Executor):
def _build_checkpoint_test_workflow(storage: InMemoryCheckpointStorage) -> Workflow:
"""Build the main workflow with checkpointing for testing."""
two_step_executor = TwoStepSubWorkflowExecutor()
sub_workflow = WorkflowBuilder().set_start_executor(two_step_executor).build()
sub_workflow = WorkflowBuilder(start_executor=two_step_executor).build()
sub_workflow_executor = WorkflowExecutor(sub_workflow, id="sub_workflow_executor")
coordinator = CheckpointTestCoordinator()
return (
WorkflowBuilder()
.set_start_executor(coordinator)
WorkflowBuilder(start_executor=coordinator, checkpoint_storage=storage)
.add_edge(coordinator, sub_workflow_executor)
.add_edge(sub_workflow_executor, coordinator)
.with_checkpointing(storage)
.build()
)
@@ -69,9 +69,8 @@ def test_valid_workflow_passes_validation():
# Create a valid workflow
workflow = (
WorkflowBuilder()
WorkflowBuilder(start_executor=executor1)
.add_edge(executor1, executor2)
.set_start_executor(executor1)
.build() # This should not raise any exceptions
)
@@ -83,7 +82,7 @@ def test_duplicate_executor_ids_fail_validation():
executor2 = IntExecutor(id="dup")
with pytest.raises(ValueError) as exc_info:
(WorkflowBuilder().add_edge(executor1, executor2).set_start_executor(executor1).build())
(WorkflowBuilder(start_executor=executor1).add_edge(executor1, executor2).build())
assert str(exc_info.value) == "Duplicate executor ID 'dup' detected in workflow."
@@ -93,9 +92,7 @@ def test_edge_duplication_validation_fails():
executor2 = StringExecutor(id="executor2")
with pytest.raises(EdgeDuplicationError) as exc_info:
WorkflowBuilder().add_edge(executor1, executor2).add_edge(executor1, executor2).set_start_executor(
executor1
).build()
WorkflowBuilder(start_executor=executor1).add_edge(executor1, executor2).add_edge(executor1, executor2).build()
assert "executor1->executor2" in str(exc_info.value)
assert exc_info.value.validation_type == ValidationTypeEnum.EDGE_DUPLICATION
@@ -106,7 +103,7 @@ def test_type_compatibility_validation_fails():
int_executor = IntExecutor(id="int_executor")
with pytest.raises(TypeCompatibilityError) as exc_info:
WorkflowBuilder().add_edge(string_executor, int_executor).set_start_executor(string_executor).build()
WorkflowBuilder(start_executor=string_executor).add_edge(string_executor, int_executor).build()
error = exc_info.value
assert error.source_executor_id == "string_executor"
@@ -119,7 +116,7 @@ def test_type_compatibility_with_any_type_passes():
any_executor = AnyExecutor(id="any_executor")
# This should not raise an exception
workflow = WorkflowBuilder().add_edge(string_executor, any_executor).set_start_executor(string_executor).build()
workflow = WorkflowBuilder(start_executor=string_executor).add_edge(string_executor, any_executor).build()
assert workflow is not None
@@ -129,9 +126,7 @@ def test_type_compatibility_with_no_output_types():
string_executor = StringExecutor(id="string_executor")
# This should pass validation since no output types are specified
workflow = (
WorkflowBuilder().add_edge(no_output_executor, string_executor).set_start_executor(no_output_executor).build()
)
workflow = WorkflowBuilder(start_executor=no_output_executor).add_edge(no_output_executor, string_executor).build()
assert workflow is not None
@@ -141,9 +136,7 @@ def test_multi_type_executor_compatibility():
multi_type_executor = MultiTypeExecutor(id="multi_type")
# String executor outputs strings, multi-type can handle strings
workflow = (
WorkflowBuilder().add_edge(string_executor, multi_type_executor).set_start_executor(string_executor).build()
)
workflow = WorkflowBuilder(start_executor=string_executor).add_edge(string_executor, multi_type_executor).build()
assert workflow is not None
@@ -154,9 +147,7 @@ def test_graph_connectivity_unreachable_executors():
executor3 = StringExecutor(id="executor3") # This will be unreachable
with pytest.raises(GraphConnectivityError) as exc_info:
WorkflowBuilder().add_edge(executor1, executor2).add_edge(executor3, executor2).set_start_executor(
executor1
).build()
WorkflowBuilder(start_executor=executor1).add_edge(executor1, executor2).add_edge(executor3, executor2).build()
assert "unreachable" in str(exc_info.value).lower()
assert "executor3" in str(exc_info.value)
@@ -189,19 +180,14 @@ def test_disconnected_start_executor_not_in_graph():
executor3 = StringExecutor(id="executor3") # Not in graph
with pytest.raises(GraphConnectivityError) as exc_info:
WorkflowBuilder().add_edge(executor1, executor2).set_start_executor(executor3).build()
WorkflowBuilder(start_executor=executor3).add_edge(executor1, executor2).build()
assert "The following executors are unreachable from the start executor 'executor3'" in str(exc_info.value)
def test_missing_start_executor():
executor1 = StringExecutor(id="executor1")
executor2 = StringExecutor(id="executor2")
with pytest.raises(ValueError) as exc_info:
WorkflowBuilder().add_edge(executor1, executor2).build()
assert "Starting executor must be set" in str(exc_info.value)
with pytest.raises(TypeError):
WorkflowBuilder() # type: ignore[call-arg]
def test_workflow_validation_error_base_class():
@@ -219,12 +205,11 @@ def test_complex_workflow_validation():
executor4 = AnyExecutor(id="executor4")
workflow = (
WorkflowBuilder()
WorkflowBuilder(start_executor=executor1)
.add_edge(executor1, executor2) # str -> MultiType (compatible)
.add_edge(executor2, executor3) # MultiType -> str (compatible)
.add_edge(executor2, executor4) # MultiType -> Any (compatible)
.add_edge(executor3, executor4) # str -> Any (compatible)
.set_start_executor(executor1)
.build()
)
@@ -246,7 +231,7 @@ def test_type_compatibility_inheritance():
derived_executor = DerivedExecutor(id="derived")
# This should pass since both handle str
workflow = WorkflowBuilder().add_edge(base_executor, derived_executor).set_start_executor(base_executor).build()
workflow = WorkflowBuilder(start_executor=base_executor).add_edge(base_executor, derived_executor).build()
assert workflow is not None
@@ -271,7 +256,7 @@ def test_fan_out_validation():
target1 = StringExecutor(id="target1")
target2 = AnyExecutor(id="target2")
workflow = WorkflowBuilder().add_fan_out_edges(source, [target1, target2]).set_start_executor(source).build()
workflow = WorkflowBuilder(start_executor=source).add_fan_out_edges(source, [target1, target2]).build()
assert workflow is not None
@@ -284,11 +269,10 @@ def test_fan_in_validation():
# Create a proper fan-in by having a start executor that connects to both sources
workflow = (
WorkflowBuilder()
WorkflowBuilder(start_executor=start_executor)
.add_edge(start_executor, source1) # Start connects to source1
.add_edge(start_executor, source2) # Start connects to source2
.add_fan_in_edges([source1, source2], target) # Both sources fan-in to target
.set_start_executor(start_executor)
.build()
)
@@ -300,7 +284,7 @@ def test_chain_validation():
executor2 = StringExecutor(id="executor2")
executor3 = AnyExecutor(id="executor3")
workflow = WorkflowBuilder().add_chain([executor1, executor2, executor3]).set_start_executor(executor1).build()
workflow = WorkflowBuilder(start_executor=executor1).add_chain([executor1, executor2, executor3]).build()
assert workflow is not None
@@ -313,9 +297,7 @@ def test_logging_for_missing_output_types(caplog: Any) -> None:
string_executor = StringExecutor(id="string_executor")
# This should trigger a warning log
workflow = (
WorkflowBuilder().add_edge(no_output_executor, string_executor).set_start_executor(no_output_executor).build()
)
workflow = WorkflowBuilder(start_executor=no_output_executor).add_edge(no_output_executor, string_executor).build()
assert workflow is not None
assert "has no output type annotations" in caplog.text
@@ -338,9 +320,7 @@ def test_logging_for_missing_input_types(caplog: Any) -> None:
no_input_executor = NoInputTypesExecutor(id="no_input")
# This should pass since NoInputTypesExecutor has no proper input types
workflow = (
WorkflowBuilder().add_edge(string_executor, no_input_executor).set_start_executor(string_executor).build()
)
workflow = WorkflowBuilder(start_executor=string_executor).add_edge(string_executor, no_input_executor).build()
assert workflow is not None
@@ -351,7 +331,7 @@ def test_self_loop_detection_warning(caplog: Any) -> None:
executor = StringExecutor(id="self_loop_executor")
# Create a self-loop
workflow = WorkflowBuilder().add_edge(executor, executor).set_start_executor(executor).build()
workflow = WorkflowBuilder(start_executor=executor).add_edge(executor, executor).build()
assert workflow is not None
assert "Self-loop detected" in caplog.text
@@ -365,7 +345,7 @@ def test_handler_validation_basic(caplog: Any) -> None:
start_executor = StringExecutor(id="start")
target_executor = StringExecutor(id="target")
workflow = WorkflowBuilder().add_edge(start_executor, target_executor).set_start_executor(start_executor).build()
workflow = WorkflowBuilder(start_executor=start_executor).add_edge(start_executor, target_executor).build()
assert workflow is not None
# Just ensure the validation runs without errors
@@ -377,7 +357,7 @@ def test_dead_end_detection(caplog: Any) -> None:
executor1 = StringExecutor(id="executor1")
executor2 = StringExecutor(id="executor2") # This will be a dead end
workflow = WorkflowBuilder().add_edge(executor1, executor2).set_start_executor(executor1).build()
workflow = WorkflowBuilder(start_executor=executor1).add_edge(executor1, executor2).build()
assert workflow is not None
assert "Dead-end executors detected" in caplog.text
@@ -391,7 +371,7 @@ def test_successful_type_compatibility_logging(caplog: Any) -> None:
executor1 = StringExecutor(id="executor1")
executor2 = StringExecutor(id="executor2")
workflow = WorkflowBuilder().add_edge(executor1, executor2).set_start_executor(executor1).build()
workflow = WorkflowBuilder(start_executor=executor1).add_edge(executor1, executor2).build()
assert workflow is not None
assert "Type compatibility validated for edge" in caplog.text
@@ -406,11 +386,7 @@ def test_multiple_dead_ends_detection(caplog: Any) -> None:
executor3 = StringExecutor(id="executor3") # Dead end
workflow = (
WorkflowBuilder()
.add_edge(executor1, executor2)
.add_edge(executor1, executor3)
.set_start_executor(executor1)
.build()
WorkflowBuilder(start_executor=executor1).add_edge(executor1, executor2).add_edge(executor1, executor3).build()
)
assert workflow is not None
@@ -426,7 +402,7 @@ def test_single_executor_workflow(caplog: Any) -> None:
executor2 = StringExecutor(id="executor2")
# Create a simple two-executor workflow to avoid graph validation issues
workflow = WorkflowBuilder().add_edge(executor1, executor2).set_start_executor(executor1).build()
workflow = WorkflowBuilder(start_executor=executor1).add_edge(executor1, executor2).build()
assert workflow is not None
# Should detect executor2 as dead end
@@ -438,7 +414,7 @@ def test_enhanced_type_compatibility_error_details():
int_executor = IntExecutor(id="int_executor")
with pytest.raises(TypeCompatibilityError) as exc_info:
WorkflowBuilder().add_edge(string_executor, int_executor).set_start_executor(string_executor).build()
WorkflowBuilder(start_executor=string_executor).add_edge(string_executor, int_executor).build()
error = exc_info.value
# Verify enhanced error contains detailed type information
@@ -463,7 +439,7 @@ def test_union_type_compatibility_validation() -> None:
union_input = UnionInputExecutor(id="union_input")
# This should pass validation due to type compatibility (str)
workflow = WorkflowBuilder().add_edge(union_output, union_input).set_start_executor(union_output).build()
workflow = WorkflowBuilder(start_executor=union_output).add_edge(union_output, union_input).build()
assert workflow is not None
@@ -483,7 +459,7 @@ def test_generic_type_compatibility() -> None:
list_input = ListInputExecutor(id="list_input")
# This should pass validation for generic type compatibility
workflow = WorkflowBuilder().add_edge(list_output, list_input).set_start_executor(list_output).build()
workflow = WorkflowBuilder(start_executor=list_output).add_edge(list_output, list_input).build()
assert workflow is not None
@@ -539,7 +515,7 @@ def test_handler_ctx_none_is_allowed() -> None:
none_exec = NoneExecutor(id="n")
# Should build successfully
wf = WorkflowBuilder().add_edge(start, none_exec).set_start_executor(start).build()
wf = WorkflowBuilder(start_executor=start).add_edge(start, none_exec).build()
assert wf is not None
@@ -555,7 +531,7 @@ def test_handler_ctx_any_is_allowed_but_skips_type_checks(caplog: Any) -> None:
any_out = AnyOutExecutor(id="a")
# Builds; later edges from this executor will skip type compatibility when outputs are unspecified
wf = WorkflowBuilder().add_edge(start, any_out).set_start_executor(start).build()
wf = WorkflowBuilder(start_executor=start).add_edge(start, any_out).build()
assert wf is not None
@@ -575,11 +551,7 @@ def test_output_validation_with_valid_output_executors():
# Build workflow with valid output executors
workflow = (
WorkflowBuilder()
.add_edge(executor1, executor2)
.set_start_executor(executor1)
.with_output_from([executor2])
.build()
WorkflowBuilder(start_executor=executor1, output_executors=[executor2]).add_edge(executor1, executor2).build()
)
assert workflow is not None
@@ -593,11 +565,9 @@ def test_output_validation_with_multiple_valid_output_executors():
executor3 = OutputExecutor(id="executor3")
workflow = (
WorkflowBuilder()
WorkflowBuilder(start_executor=executor1, output_executors=[executor1, executor3])
.add_edge(executor1, executor2)
.add_edge(executor2, executor3)
.set_start_executor(executor1)
.with_output_from([executor1, executor3])
.build()
)
@@ -628,10 +598,8 @@ def test_output_validation_fails_for_executor_without_output_types():
with pytest.raises(WorkflowValidationError) as exc_info:
(
WorkflowBuilder()
WorkflowBuilder(start_executor=executor1, output_executors=[no_output_executor])
.add_edge(executor1, no_output_executor)
.set_start_executor(executor1)
.with_output_from([no_output_executor])
.build()
)
@@ -645,9 +613,7 @@ def test_output_validation_empty_list_passes():
executor1 = OutputExecutor(id="executor1")
executor2 = OutputExecutor(id="executor2")
workflow = (
WorkflowBuilder().add_edge(executor1, executor2).set_start_executor(executor1).with_output_from([]).build()
)
workflow = WorkflowBuilder(start_executor=executor1, output_executors=[]).add_edge(executor1, executor2).build()
assert workflow is not None
# All executors are outputs
+17 -41
View File
@@ -31,7 +31,7 @@ def basic_sub_workflow():
sub_exec1 = MockExecutor(id="sub_exec1")
sub_exec2 = MockExecutor(id="sub_exec2")
sub_workflow = WorkflowBuilder().add_edge(sub_exec1, sub_exec2).set_start_executor(sub_exec1).build()
sub_workflow = WorkflowBuilder(start_executor=sub_exec1).add_edge(sub_exec1, sub_exec2).build()
# Create a workflow executor that wraps the sub-workflow
workflow_executor = WorkflowExecutor(sub_workflow, id="workflow_executor_1")
@@ -41,10 +41,9 @@ def basic_sub_workflow():
final_exec = MockExecutor(id="final_executor")
main_workflow = (
WorkflowBuilder()
WorkflowBuilder(start_executor=main_exec)
.add_edge(main_exec, workflow_executor)
.add_edge(workflow_executor, final_exec)
.set_start_executor(main_exec)
.build()
)
@@ -65,7 +64,7 @@ def test_workflow_viz_to_digraph():
executor1 = MockExecutor(id="executor1")
executor2 = MockExecutor(id="executor2")
workflow = WorkflowBuilder().add_edge(executor1, executor2).set_start_executor(executor1).build()
workflow = WorkflowBuilder(start_executor=executor1).add_edge(executor1, executor2).build()
viz = WorkflowViz(workflow)
dot_content = viz.to_digraph()
@@ -84,7 +83,7 @@ def test_workflow_viz_export_dot():
executor1 = MockExecutor(id="executor1")
executor2 = MockExecutor(id="executor2")
workflow = WorkflowBuilder().add_edge(executor1, executor2).set_start_executor(executor1).build()
workflow = WorkflowBuilder(start_executor=executor1).add_edge(executor1, executor2).build()
viz = WorkflowViz(workflow)
@@ -104,7 +103,7 @@ def test_workflow_viz_export_dot_with_filename(tmp_path):
executor1 = MockExecutor(id="executor1")
executor2 = MockExecutor(id="executor2")
workflow = WorkflowBuilder().add_edge(executor1, executor2).set_start_executor(executor1).build()
workflow = WorkflowBuilder(start_executor=executor1).add_edge(executor1, executor2).build()
viz = WorkflowViz(workflow)
@@ -128,12 +127,11 @@ def test_workflow_viz_complex_workflow():
executor4 = MockExecutor(id="end")
workflow = (
WorkflowBuilder()
WorkflowBuilder(start_executor=executor1)
.add_edge(executor1, executor2)
.add_edge(executor1, executor3)
.add_edge(executor2, executor4)
.add_edge(executor3, executor4)
.set_start_executor(executor1)
.build()
)
@@ -162,7 +160,7 @@ def test_workflow_viz_export_svg():
executor1 = MockExecutor(id="executor1")
executor2 = MockExecutor(id="executor2")
workflow = WorkflowBuilder().add_edge(executor1, executor2).set_start_executor(executor1).build()
workflow = WorkflowBuilder(start_executor=executor1).add_edge(executor1, executor2).build()
viz = WorkflowViz(workflow)
@@ -178,7 +176,7 @@ def test_workflow_viz_unsupported_format():
executor1 = MockExecutor(id="executor1")
executor2 = MockExecutor(id="executor2")
workflow = WorkflowBuilder().add_edge(executor1, executor2).set_start_executor(executor1).build()
workflow = WorkflowBuilder(start_executor=executor1).add_edge(executor1, executor2).build()
viz = WorkflowViz(workflow)
@@ -196,7 +194,7 @@ def test_workflow_viz_graphviz_binary_not_found():
executor1 = MockExecutor(id="executor1")
executor2 = MockExecutor(id="executor2")
workflow = WorkflowBuilder().add_edge(executor1, executor2).set_start_executor(executor1).build()
workflow = WorkflowBuilder(start_executor=executor1).add_edge(executor1, executor2).build()
viz = WorkflowViz(workflow)
# Mock graphviz.Source.render to raise ExecutableNotFound
@@ -224,13 +222,7 @@ def test_workflow_viz_conditional_edge():
def only_if_foo(msg: str) -> bool: # pragma: no cover - simple predicate
return msg == "foo"
wf = (
WorkflowBuilder()
.add_edge(start, mid, condition=only_if_foo)
.add_edge(mid, end)
.set_start_executor(start)
.build()
)
wf = WorkflowBuilder(start_executor=start).add_edge(start, mid, condition=only_if_foo).add_edge(mid, end).build()
dot = WorkflowViz(wf).to_digraph()
@@ -249,13 +241,7 @@ def test_workflow_viz_fan_in_edge_group():
t = ListStrTargetExecutor(id="t")
# Build a connected workflow: start fans out to s1 and s2, which then fan-in to t
wf = (
WorkflowBuilder()
.add_fan_out_edges(start, [s1, s2])
.add_fan_in_edges([s1, s2], t)
.set_start_executor(start)
.build()
)
wf = WorkflowBuilder(start_executor=start).add_fan_out_edges(start, [s1, s2]).add_fan_in_edges([s1, s2], t).build()
dot = WorkflowViz(wf).to_digraph()
@@ -287,7 +273,7 @@ def test_workflow_viz_to_mermaid_basic():
executor1 = MockExecutor(id="executor1")
executor2 = MockExecutor(id="executor2")
workflow = WorkflowBuilder().add_edge(executor1, executor2).set_start_executor(executor1).build()
workflow = WorkflowBuilder(start_executor=executor1).add_edge(executor1, executor2).build()
mermaid = WorkflowViz(workflow).to_mermaid()
# Start node and normal node
@@ -305,7 +291,7 @@ def test_workflow_viz_mermaid_conditional_edge():
def only_if_foo(msg: str) -> bool: # pragma: no cover - simple predicate
return msg == "foo"
wf = WorkflowBuilder().add_edge(start, mid, condition=only_if_foo).set_start_executor(start).build()
wf = WorkflowBuilder(start_executor=start).add_edge(start, mid, condition=only_if_foo).build()
mermaid = WorkflowViz(wf).to_mermaid()
assert "start -. conditional .-> mid" in mermaid
@@ -318,13 +304,7 @@ def test_workflow_viz_mermaid_fan_in_edge_group():
s2 = MockExecutor(id="s2")
t = ListStrTargetExecutor(id="t")
wf = (
WorkflowBuilder()
.add_fan_out_edges(start, [s1, s2])
.add_fan_in_edges([s1, s2], t)
.set_start_executor(start)
.build()
)
wf = WorkflowBuilder(start_executor=start).add_fan_out_edges(start, [s1, s2]).add_fan_in_edges([s1, s2], t).build()
mermaid = WorkflowViz(wf).to_mermaid()
lines = [line.strip() for line in mermaid.splitlines()]
@@ -398,23 +378,19 @@ def test_workflow_viz_nested_sub_workflows():
"""Test visualization of deeply nested sub-workflows."""
# Create innermost sub-workflow
inner_exec = MockExecutor(id="inner_exec")
inner_workflow = WorkflowBuilder().set_start_executor(inner_exec).build()
inner_workflow = WorkflowBuilder(start_executor=inner_exec).build()
# Create middle sub-workflow that contains the inner one
inner_workflow_executor = WorkflowExecutor(inner_workflow, id="inner_wf_exec")
middle_exec = MockExecutor(id="middle_exec")
middle_workflow = (
WorkflowBuilder().add_edge(middle_exec, inner_workflow_executor).set_start_executor(middle_exec).build()
)
middle_workflow = WorkflowBuilder(start_executor=middle_exec).add_edge(middle_exec, inner_workflow_executor).build()
# Create outer workflow
middle_workflow_executor = WorkflowExecutor(middle_workflow, id="middle_wf_exec")
outer_exec = MockExecutor(id="outer_exec")
outer_workflow = (
WorkflowBuilder().add_edge(outer_exec, middle_workflow_executor).set_start_executor(outer_exec).build()
)
outer_workflow = WorkflowBuilder(start_executor=outer_exec).add_edge(outer_exec, middle_workflow_executor).build()
viz = WorkflowViz(outer_workflow)
dot_content = viz.to_digraph()
@@ -110,8 +110,7 @@ async def test_workflow_run_streaming() -> None:
executor_b = IncrementExecutor(id="executor_b")
workflow = (
WorkflowBuilder()
.set_start_executor(executor_a)
WorkflowBuilder(start_executor=executor_a)
.add_edge(executor_a, executor_b)
.add_edge(executor_b, executor_a)
.build()
@@ -132,11 +131,9 @@ async def test_workflow_run_stream_not_completed():
executor_b = IncrementExecutor(id="executor_b")
workflow = (
WorkflowBuilder()
.set_start_executor(executor_a)
WorkflowBuilder(max_iterations=5, start_executor=executor_a)
.add_edge(executor_a, executor_b)
.add_edge(executor_b, executor_a)
.set_max_iterations(5)
.build()
)
@@ -151,8 +148,7 @@ async def test_workflow_run():
executor_b = IncrementExecutor(id="executor_b")
workflow = (
WorkflowBuilder()
.set_start_executor(executor_a)
WorkflowBuilder(start_executor=executor_a)
.add_edge(executor_a, executor_b)
.add_edge(executor_b, executor_a)
.build()
@@ -170,11 +166,9 @@ async def test_workflow_run_not_completed():
executor_b = IncrementExecutor(id="executor_b")
workflow = (
WorkflowBuilder()
.set_start_executor(executor_a)
WorkflowBuilder(max_iterations=5, start_executor=executor_a)
.add_edge(executor_a, executor_b)
.add_edge(executor_b, executor_a)
.set_max_iterations(5)
.build()
)
@@ -189,7 +183,7 @@ async def test_fan_out():
executor_c = IncrementExecutor(id="executor_c", limit=2) # This executor will not complete the workflow
workflow = (
WorkflowBuilder().set_start_executor(executor_a).add_fan_out_edges(executor_a, [executor_b, executor_c]).build()
WorkflowBuilder(start_executor=executor_a).add_fan_out_edges(executor_a, [executor_b, executor_c]).build()
)
events = await workflow.run(NumberMessage(data=0))
@@ -214,7 +208,7 @@ async def test_fan_out_multiple_completed_events():
executor_c = IncrementExecutor(id="executor_c", limit=1)
workflow = (
WorkflowBuilder().set_start_executor(executor_a).add_fan_out_edges(executor_a, [executor_b, executor_c]).build()
WorkflowBuilder(start_executor=executor_a).add_fan_out_edges(executor_a, [executor_b, executor_c]).build()
)
events = await workflow.run(NumberMessage(data=0))
@@ -239,8 +233,7 @@ async def test_fan_in():
aggregator = AggregatorExecutor(id="aggregator")
workflow = (
WorkflowBuilder()
.set_start_executor(executor_a)
WorkflowBuilder(start_executor=executor_a)
.add_fan_out_edges(executor_a, [executor_b, executor_c])
.add_fan_in_edges([executor_b, executor_c], aggregator)
.build()
@@ -276,10 +269,8 @@ async def test_workflow_with_checkpointing_enabled(simple_executor: Executor):
# Build workflow with checkpointing - should not raise any errors
workflow = (
WorkflowBuilder()
WorkflowBuilder(start_executor=simple_executor, checkpoint_storage=storage)
.add_edge(simple_executor, simple_executor) # Self-loop to satisfy graph requirements
.set_start_executor(simple_executor)
.with_checkpointing(storage)
.build()
)
@@ -295,9 +286,8 @@ async def test_workflow_checkpointing_not_enabled_for_external_restore(
"""Test that external checkpoint restoration fails when workflow doesn't support checkpointing."""
# Build workflow WITHOUT checkpointing
workflow = (
WorkflowBuilder()
WorkflowBuilder(start_executor=simple_executor)
.add_edge(simple_executor, simple_executor) # Self-loop to satisfy graph requirements
.set_start_executor(simple_executor)
.build()
)
@@ -315,9 +305,8 @@ async def test_workflow_run_stream_from_checkpoint_no_checkpointing_enabled(
):
# Build workflow WITHOUT checkpointing
workflow = (
WorkflowBuilder()
WorkflowBuilder(start_executor=simple_executor)
.add_edge(simple_executor, simple_executor) # Self-loop to satisfy graph requirements
.set_start_executor(simple_executor)
.build()
)
@@ -340,10 +329,8 @@ async def test_workflow_run_stream_from_checkpoint_invalid_checkpoint(
# Build workflow with checkpointing
workflow = (
WorkflowBuilder()
WorkflowBuilder(start_executor=simple_executor, checkpoint_storage=storage)
.add_edge(simple_executor, simple_executor) # Self-loop to satisfy graph requirements
.set_start_executor(simple_executor)
.with_checkpointing(storage)
.build()
)
@@ -376,7 +363,7 @@ async def test_workflow_run_stream_from_checkpoint_with_external_storage(
# Create a workflow WITHOUT checkpointing
workflow_without_checkpointing = (
WorkflowBuilder().add_edge(simple_executor, simple_executor).set_start_executor(simple_executor).build()
WorkflowBuilder(start_executor=simple_executor).add_edge(simple_executor, simple_executor).build()
)
# Resume from checkpoint using external storage parameter
@@ -411,10 +398,8 @@ async def test_workflow_run_from_checkpoint_non_streaming(simple_executor: Execu
# Build workflow with checkpointing
workflow = (
WorkflowBuilder()
WorkflowBuilder(start_executor=simple_executor, checkpoint_storage=storage)
.add_edge(simple_executor, simple_executor)
.set_start_executor(simple_executor)
.with_checkpointing(storage)
.build()
)
@@ -452,10 +437,8 @@ async def test_workflow_run_stream_from_checkpoint_with_responses(
# Build workflow with checkpointing
workflow = (
WorkflowBuilder()
WorkflowBuilder(start_executor=simple_executor, checkpoint_storage=storage)
.add_edge(simple_executor, simple_executor)
.set_start_executor(simple_executor)
.with_checkpointing(storage)
.build()
)
@@ -512,10 +495,8 @@ async def test_workflow_multiple_runs_no_state_collision():
# Build workflow with checkpointing
workflow = (
WorkflowBuilder()
WorkflowBuilder(start_executor=state_executor, checkpoint_storage=storage)
.add_edge(state_executor, state_executor) # Self-loop to satisfy graph requirements
.set_start_executor(state_executor)
.with_checkpointing(storage)
.build()
)
@@ -552,9 +533,7 @@ async def test_workflow_checkpoint_runtime_only_configuration(
storage = FileCheckpointStorage(temp_dir)
# Build workflow WITHOUT checkpointing at build time
workflow = (
WorkflowBuilder().add_edge(simple_executor, simple_executor).set_start_executor(simple_executor).build()
)
workflow = WorkflowBuilder(start_executor=simple_executor).add_edge(simple_executor, simple_executor).build()
# Run with runtime checkpoint storage - should create checkpoints
test_message = Message(data="runtime checkpoint test", source_id="test", target_id=None)
@@ -575,7 +554,7 @@ async def test_workflow_checkpoint_runtime_only_configuration(
# Create new workflow instance (still without build-time checkpointing)
workflow_resume = (
WorkflowBuilder().add_edge(simple_executor, simple_executor).set_start_executor(simple_executor).build()
WorkflowBuilder(start_executor=simple_executor).add_edge(simple_executor, simple_executor).build()
)
# Resume from checkpoint using runtime checkpoint storage
@@ -602,10 +581,8 @@ async def test_workflow_checkpoint_runtime_overrides_buildtime(
# Build workflow with build-time checkpointing
workflow = (
WorkflowBuilder()
WorkflowBuilder(start_executor=simple_executor, checkpoint_storage=buildtime_storage)
.add_edge(simple_executor, simple_executor)
.set_start_executor(simple_executor)
.with_checkpointing(buildtime_storage)
.build()
)
@@ -643,8 +620,7 @@ async def test_comprehensive_edge_groups_workflow():
# 3. FanOut: fanout_hub -> [parallel_1, parallel_2]
# 4. FanIn: [parallel_1, parallel_2] -> aggregator
workflow = (
WorkflowBuilder()
.set_start_executor(router)
WorkflowBuilder(start_executor=router)
# Switch-case routing based on message data
.add_switch_case_edge_group(
router,
@@ -713,8 +689,7 @@ async def test_workflow_with_simple_cycle_and_exit_condition():
# Simple cycle: A -> B -> A, A exits when limit reached
workflow = (
WorkflowBuilder()
.set_start_executor(executor_a)
WorkflowBuilder(start_executor=executor_a)
.add_edge(executor_a, executor_b) # A -> B
.add_edge(executor_b, executor_a) # B -> A (creates cycle)
.build()
@@ -746,7 +721,7 @@ async def test_workflow_concurrent_execution_prevention():
"""Test that concurrent workflow executions are prevented."""
# Create a simple workflow that takes some time to execute
executor = IncrementExecutor(id="slow_executor", limit=3, increment=1)
workflow = WorkflowBuilder().set_start_executor(executor).build()
workflow = WorkflowBuilder(start_executor=executor).build()
# Create a task that will run the workflow
async def run_workflow():
@@ -778,7 +753,7 @@ async def test_workflow_concurrent_execution_prevention_streaming():
"""Test that concurrent workflow streaming executions are prevented."""
# Create a simple workflow
executor = IncrementExecutor(id="slow_executor", limit=3, increment=1)
workflow = WorkflowBuilder().set_start_executor(executor).build()
workflow = WorkflowBuilder(start_executor=executor).build()
# Create an async generator that will consume the stream slowly
async def consume_stream_slowly():
@@ -814,7 +789,7 @@ async def test_workflow_concurrent_execution_prevention_mixed_methods():
"""Test that concurrent executions are prevented across different execution methods."""
# Create a simple workflow
executor = IncrementExecutor(id="slow_executor", limit=3, increment=1)
workflow = WorkflowBuilder().set_start_executor(executor).build()
workflow = WorkflowBuilder(start_executor=executor).build()
# Start a streaming execution
async def consume_stream():
@@ -884,7 +859,7 @@ async def test_agent_streaming_vs_non_streaming() -> None:
agent = _StreamingTestAgent(id="test_agent", name="TestAgent", reply_text="Hello World")
agent_exec = AgentExecutor(agent, id="agent_exec")
workflow = WorkflowBuilder().set_start_executor(agent_exec).build()
workflow = WorkflowBuilder(start_executor=agent_exec).build()
# Test non-streaming mode with run()
result = await workflow.run("test message")
@@ -934,7 +909,7 @@ async def test_agent_streaming_vs_non_streaming() -> None:
async def test_workflow_run_parameter_validation(simple_executor: Executor) -> None:
"""Test that stream properly validate parameter combinations."""
workflow = WorkflowBuilder().add_edge(simple_executor, simple_executor).set_start_executor(simple_executor).build()
workflow = WorkflowBuilder(start_executor=simple_executor).add_edge(simple_executor, simple_executor).build()
test_message = Message(data="test", source_id="test", target_id=None)
@@ -965,7 +940,7 @@ async def test_workflow_run_stream_parameter_validation(
simple_executor: Executor,
) -> None:
"""Test stream=True specific parameter validation scenarios."""
workflow = WorkflowBuilder().add_edge(simple_executor, simple_executor).set_start_executor(simple_executor).build()
workflow = WorkflowBuilder(start_executor=simple_executor).add_edge(simple_executor, simple_executor).build()
test_message = Message(data="test", source_id="test", target_id=None)
@@ -1014,7 +989,7 @@ async def test_output_executors_empty_yields_all_outputs() -> None:
executor_b = OutputProducerExecutor(id="executor_b", output_value=20)
# Build workflow with a -> b
workflow = WorkflowBuilder().set_start_executor(executor_a).add_edge(executor_a, executor_b).build()
workflow = WorkflowBuilder(start_executor=executor_a).add_edge(executor_a, executor_b).build()
result = await workflow.run(NumberMessage(data=0))
outputs = result.get_outputs()
@@ -1037,10 +1012,8 @@ async def test_output_executors_filters_outputs_non_streaming() -> None:
# Build workflow with a -> b
workflow = (
WorkflowBuilder()
.set_start_executor(executor_a)
WorkflowBuilder(start_executor=executor_a, output_executors=[executor_b])
.add_edge(executor_a, executor_b)
.with_output_from([executor_b])
.build()
)
@@ -1064,10 +1037,8 @@ async def test_output_executors_filters_outputs_streaming() -> None:
# Build workflow with a -> b
workflow = (
WorkflowBuilder()
.set_start_executor(executor_a)
WorkflowBuilder(start_executor=executor_a, output_executors=[executor_a])
.add_edge(executor_a, executor_b)
.with_output_from([executor_a])
.build()
)
@@ -1092,11 +1063,9 @@ async def test_output_executors_with_multiple_specified_executors() -> None:
# Build workflow with a -> b -> c
workflow = (
WorkflowBuilder()
.set_start_executor(executor_a)
WorkflowBuilder(start_executor=executor_a, output_executors=[executor_a, executor_c])
.add_edge(executor_a, executor_b)
.add_edge(executor_b, executor_c)
.with_output_from([executor_a, executor_c])
.build()
)
@@ -1114,7 +1083,7 @@ async def test_output_executors_with_nonexistent_executor_id() -> None:
"""Test that specifying a non-existent executor ID doesn't break the workflow."""
executor_a = OutputProducerExecutor(id="executor_a", output_value=42)
workflow = WorkflowBuilder().set_start_executor(executor_a).build()
workflow = WorkflowBuilder(start_executor=executor_a).build()
# Set output_executors to an ID that doesn't exist
workflow._output_executors = ["nonexistent_executor"] # type: ignore
@@ -1157,11 +1126,9 @@ async def test_output_executors_filtering_with_fan_in() -> None:
# Build fan-in workflow: start -> [a, b] -> aggregator
workflow = (
WorkflowBuilder()
.set_start_executor(executor_start)
WorkflowBuilder(start_executor=executor_start, output_executors=[aggregator])
.add_fan_out_edges(executor_start, [executor_a, executor_b])
.add_fan_in_edges([executor_a, executor_b], aggregator)
.with_output_from([aggregator])
.build()
)
@@ -1178,7 +1145,7 @@ async def test_output_executors_filtering_with_run_responses() -> None:
"""Test output filtering works correctly with run(responses=...) method."""
executor = MockExecutorRequestApproval(id="approval_executor")
workflow = WorkflowBuilder().set_start_executor(executor).with_output_from([executor]).build()
workflow = WorkflowBuilder(start_executor=executor, output_executors=[executor]).build()
# Run workflow which will request approval
result = await workflow.run(NumberMessage(data=42))
@@ -1201,7 +1168,7 @@ async def test_output_executors_filtering_with_run_responses_streaming() -> None
"""Test output filtering works correctly with run(responses=..., stream=True) method."""
executor = MockExecutorRequestApproval(id="approval_executor")
workflow = WorkflowBuilder().set_start_executor(executor).build()
workflow = WorkflowBuilder(start_executor=executor).build()
# Run workflow which will request approval
events_list: list[WorkflowEvent] = []
@@ -150,7 +150,7 @@ class TestWorkflowAgent:
executor1 = SimpleExecutor(id="executor1", response_text="Step1", streaming=False)
executor2 = SimpleExecutor(id="executor2", response_text="Step2", streaming=False)
workflow = WorkflowBuilder().set_start_executor(executor1).add_edge(executor1, executor2).build()
workflow = WorkflowBuilder(start_executor=executor1).add_edge(executor1, executor2).build()
agent = WorkflowAgent(workflow=workflow, name="Test Agent")
@@ -194,7 +194,7 @@ class TestWorkflowAgent:
executor2 = SimpleExecutor(id="stream2", response_text="Streaming2")
# Create workflow with just one executor
workflow = WorkflowBuilder().set_start_executor(executor1).add_edge(executor1, executor2).build()
workflow = WorkflowBuilder(start_executor=executor1).add_edge(executor1, executor2).build()
agent = WorkflowAgent(workflow=workflow, name="Streaming Test Agent")
@@ -224,7 +224,7 @@ class TestWorkflowAgent:
requesting_executor = RequestingExecutor(id="requester", streaming=False)
workflow = (
WorkflowBuilder().set_start_executor(simple_executor).add_edge(simple_executor, requesting_executor).build()
WorkflowBuilder(start_executor=simple_executor).add_edge(simple_executor, requesting_executor).build()
)
agent = WorkflowAgent(workflow=workflow, name="Request Test Agent")
@@ -296,7 +296,7 @@ class TestWorkflowAgent:
"""Test that Workflow.as_agent() creates a properly configured WorkflowAgent."""
# Create a simple workflow
executor = SimpleExecutor(id="executor1", response_text="Response")
workflow = WorkflowBuilder().set_start_executor(executor).build()
workflow = WorkflowBuilder(start_executor=executor).build()
# Test as_agent with a name
agent = workflow.as_agent(name="TestAgent")
@@ -322,7 +322,7 @@ class TestWorkflowAgent:
# Create a simple workflow
executor = _Executor(id="test")
workflow = WorkflowBuilder().set_start_executor(executor).build()
workflow = WorkflowBuilder(start_executor=executor).build()
# Try to create an agent with unsupported input types
with pytest.raises(ValueError, match="Workflow's start executor cannot handle list\\[ChatMessage\\]"):
@@ -341,7 +341,7 @@ class TestWorkflowAgent:
input_text = messages[0].text if messages else "no input"
await ctx.yield_output(f"processed: {input_text}")
workflow = WorkflowBuilder().set_start_executor(yielding_executor).build()
workflow = WorkflowBuilder(start_executor=yielding_executor).build()
# Run directly - should return output event (type='output') in result
direct_result = await workflow.run([ChatMessage(role="user", text="hello")])
@@ -365,7 +365,7 @@ class TestWorkflowAgent:
await ctx.yield_output("first output")
await ctx.yield_output("second output")
workflow = WorkflowBuilder().set_start_executor(yielding_executor).build()
workflow = WorkflowBuilder(start_executor=yielding_executor).build()
agent = workflow.as_agent("test-agent")
updates: list[AgentResponseUpdate] = []
@@ -387,7 +387,7 @@ class TestWorkflowAgent:
await ctx.yield_output(Content.from_data(data=b"binary data", media_type="application/octet-stream"))
await ctx.yield_output(Content.from_uri(uri="https://example.com/image.png", media_type="image/png"))
workflow = WorkflowBuilder().set_start_executor(content_yielding_executor).build()
workflow = WorkflowBuilder(start_executor=content_yielding_executor).build()
agent = workflow.as_agent("content-test-agent")
result = await agent.run("test")
@@ -417,7 +417,7 @@ class TestWorkflowAgent:
)
await ctx.yield_output(msg)
workflow = WorkflowBuilder().set_start_executor(chat_message_executor).build()
workflow = WorkflowBuilder(start_executor=chat_message_executor).build()
agent = workflow.as_agent("chat-msg-agent")
result = await agent.run("test")
@@ -448,7 +448,7 @@ class TestWorkflowAgent:
custom = CustomData(42)
await ctx.yield_output(custom)
workflow = WorkflowBuilder().set_start_executor(raw_yielding_executor).build()
workflow = WorkflowBuilder(start_executor=raw_yielding_executor).build()
agent = workflow.as_agent("raw-test-agent")
updates: list[AgentResponseUpdate] = []
@@ -490,7 +490,7 @@ class TestWorkflowAgent:
]
await ctx.yield_output(msg_list)
workflow = WorkflowBuilder().set_start_executor(list_yielding_executor).build()
workflow = WorkflowBuilder(start_executor=list_yielding_executor).build()
agent = workflow.as_agent("list-msg-agent")
# Verify streaming returns the update with all 4 contents before coalescing
@@ -521,7 +521,7 @@ class TestWorkflowAgent:
"""
# Create an executor that captures all received messages
capturing_executor = ConversationHistoryCapturingExecutor(id="capturing", streaming=False)
workflow = WorkflowBuilder().set_start_executor(capturing_executor).build()
workflow = WorkflowBuilder(start_executor=capturing_executor).build()
agent = WorkflowAgent(workflow=workflow, name="Thread History Test Agent")
# Create a thread with existing conversation history
@@ -551,7 +551,7 @@ class TestWorkflowAgent:
"""
# Create an executor that captures all received messages
capturing_executor = ConversationHistoryCapturingExecutor(id="capturing_stream")
workflow = WorkflowBuilder().set_start_executor(capturing_executor).build()
workflow = WorkflowBuilder(start_executor=capturing_executor).build()
agent = WorkflowAgent(workflow=workflow, name="Thread Stream Test Agent")
# Create a thread with existing conversation history
@@ -579,7 +579,7 @@ class TestWorkflowAgent:
async def test_empty_thread_works_correctly(self) -> None:
"""Test that an empty thread (no message store) works correctly."""
capturing_executor = ConversationHistoryCapturingExecutor(id="empty_thread_test")
workflow = WorkflowBuilder().set_start_executor(capturing_executor).build()
workflow = WorkflowBuilder(start_executor=capturing_executor).build()
agent = WorkflowAgent(workflow=workflow, name="Empty Thread Test Agent")
# Create an empty thread
@@ -597,7 +597,7 @@ class TestWorkflowAgent:
from agent_framework import InMemoryCheckpointStorage
capturing_executor = ConversationHistoryCapturingExecutor(id="checkpoint_test")
workflow = WorkflowBuilder().set_start_executor(capturing_executor).build()
workflow = WorkflowBuilder(start_executor=capturing_executor).build()
agent = WorkflowAgent(workflow=workflow, name="Checkpoint Test Agent")
# Create checkpoint storage
@@ -675,17 +675,11 @@ class TestWorkflowAgent:
await ctx.send_message(AgentExecutorRequest(messages=messages, should_respond=True))
# Build workflow: start -> agent1 (no output) -> agent2 (output_response=True)
workflow = (
WorkflowBuilder()
.register_executor(lambda: start_executor, "start")
.register_agent(lambda: MockAgent("agent1", "Agent1 output - should NOT appear"), "agent1")
.register_agent(lambda: MockAgent("agent2", "Agent2 output - SHOULD appear"), "agent2")
.set_start_executor("start")
.add_edge("start", "agent1")
.add_edge("agent1", "agent2")
.with_output_from(["start", "agent2"])
.build()
)
builder = WorkflowBuilder(start_executor="start", output_executors=["start", "agent2"])
builder.register_executor(lambda: start_executor, "start")
builder.register_agent(lambda: MockAgent("agent1", "Agent1 output - should NOT appear"), "agent1")
builder.register_agent(lambda: MockAgent("agent2", "Agent2 output - SHOULD appear"), "agent2")
workflow = builder.add_edge("start", "agent1").add_edge("agent1", "agent2").build()
agent = WorkflowAgent(workflow=workflow, name="Test Agent")
result = await agent.run("Test input")
@@ -765,10 +759,9 @@ class TestWorkflowAgent:
# Build workflow with single agent
workflow = (
WorkflowBuilder()
WorkflowBuilder(start_executor="start")
.register_executor(lambda: start_executor, "start")
.register_agent(lambda: MockAgent("agent", "Unique response text"), "agent")
.set_start_executor("start")
.add_edge("start", "agent")
.build()
)
@@ -794,7 +787,7 @@ class TestWorkflowAgentAuthorName:
"""
# Create workflow with executor that emits AgentResponseUpdate without author_name
executor1 = SimpleExecutor(id="my_executor_id", response_text="Response", streaming=True)
workflow = WorkflowBuilder().set_start_executor(executor1).build()
workflow = WorkflowBuilder(start_executor=executor1).build()
agent = WorkflowAgent(workflow=workflow, name="Test Agent")
# Collect streaming updates
@@ -830,7 +823,7 @@ class TestWorkflowAgentAuthorName:
await ctx.yield_output(update)
executor = AuthorNameExecutor(id="executor_id")
workflow = WorkflowBuilder().set_start_executor(executor).build()
workflow = WorkflowBuilder(start_executor=executor).build()
agent = WorkflowAgent(workflow=workflow, name="Test Agent")
# Collect streaming updates
@@ -848,7 +841,7 @@ class TestWorkflowAgentAuthorName:
executor1 = SimpleExecutor(id="first_executor", response_text="First")
executor2 = SimpleExecutor(id="second_executor", response_text="Second")
workflow = WorkflowBuilder().set_start_executor(executor1).add_edge(executor1, executor2).build()
workflow = WorkflowBuilder(start_executor=executor1).add_edge(executor1, executor2).build()
agent = WorkflowAgent(workflow=workflow, name="Multi-Executor Agent")
# Collect streaming updates
@@ -45,7 +45,7 @@ def test_builder_accepts_agents_directly():
agent1 = DummyAgent(id="agent1", name="writer")
agent2 = DummyAgent(id="agent2", name="reviewer")
wf = WorkflowBuilder().set_start_executor(agent1).add_edge(agent1, agent2).build()
wf = WorkflowBuilder(start_executor=agent1).add_edge(agent1, agent2).build()
# Confirm auto-wrapped executors use agent names as IDs
assert wf.start_executor_id == "writer"
@@ -79,10 +79,8 @@ class MockAggregator(Executor):
def test_workflow_builder_without_start_executor_throws():
"""Test creating a workflow builder without a start executor."""
builder = WorkflowBuilder()
with pytest.raises(ValueError):
builder.build()
with pytest.raises(TypeError):
WorkflowBuilder() # type: ignore[call-arg]
def test_workflow_builder_fluent_api():
@@ -95,13 +93,11 @@ def test_workflow_builder_fluent_api():
executor_f = MockExecutor(id="executor_f")
workflow = (
WorkflowBuilder()
.set_start_executor(executor_a)
WorkflowBuilder(max_iterations=5, start_executor=executor_a)
.add_edge(executor_a, executor_b)
.add_fan_out_edges(executor_b, [executor_c, executor_d])
.add_fan_in_edges([executor_c, executor_d], executor_e)
.add_chain([executor_e, executor_f])
.set_max_iterations(5)
.build()
)
@@ -115,9 +111,8 @@ def test_add_agent_reuses_same_wrapper():
reuse_agent = DummyAgent(id="agent_reuse", name="reuse_agent")
agent_a = DummyAgent(id="agent_a", name="agent_a")
builder = WorkflowBuilder()
builder = WorkflowBuilder(start_executor=reuse_agent)
# Use the same agent instance in add_edge - should reuse the same wrapper
builder.set_start_executor(reuse_agent)
builder.add_edge(reuse_agent, agent_a)
builder.add_edge(agent_a, reuse_agent)
@@ -133,10 +128,10 @@ def test_add_agent_duplicate_id_raises_error():
"""Test that adding agents with duplicate IDs raises an error."""
agent1 = DummyAgent(id="agent1", name="first")
agent2 = DummyAgent(id="agent2", name="first") # Same name as agent1
builder = WorkflowBuilder()
builder = WorkflowBuilder(start_executor=agent1)
with pytest.raises(ValueError, match="Duplicate executor ID"):
builder.set_start_executor(agent1).add_edge(agent1, agent2).build()
builder.add_edge(agent1, agent2).build()
# Tests for new executor registration patterns
@@ -144,7 +139,7 @@ def test_add_agent_duplicate_id_raises_error():
def test_register_executor_basic():
"""Test basic executor registration with lazy initialization."""
builder = WorkflowBuilder()
builder = WorkflowBuilder(start_executor="TestExecutor")
# Register an executor factory - ID must match the registered name
result = builder.register_executor(lambda: MockExecutor(id="TestExecutor"), name="TestExecutor")
@@ -153,14 +148,14 @@ def test_register_executor_basic():
assert result is builder
# Build workflow and verify executor is instantiated
workflow = builder.set_start_executor("TestExecutor").build()
workflow = builder.build()
assert "TestExecutor" in workflow.executors
assert isinstance(workflow.executors["TestExecutor"], MockExecutor)
def test_register_multiple_executors():
"""Test registering multiple executors and connecting them with edges."""
builder = WorkflowBuilder()
builder = WorkflowBuilder(start_executor="ExecutorA")
# Register multiple executors - IDs must match registered names
builder.register_executor(lambda: MockExecutor(id="ExecutorA"), name="ExecutorA")
@@ -168,13 +163,7 @@ def test_register_multiple_executors():
builder.register_executor(lambda: MockExecutor(id="ExecutorC"), name="ExecutorC")
# Build workflow with edges using registered names
workflow = (
builder
.set_start_executor("ExecutorA")
.add_edge("ExecutorA", "ExecutorB")
.add_edge("ExecutorB", "ExecutorC")
.build()
)
workflow = builder.add_edge("ExecutorA", "ExecutorB").add_edge("ExecutorB", "ExecutorC").build()
# Verify all executors are present
assert "ExecutorA" in workflow.executors
@@ -185,7 +174,7 @@ def test_register_multiple_executors():
def test_register_with_multiple_names():
"""Test registering the same factory function under multiple names."""
builder = WorkflowBuilder()
builder = WorkflowBuilder(start_executor="ExecutorA")
# Register same executor factory under multiple names
# Note: Each call creates a new instance, so IDs won't conflict
@@ -198,7 +187,7 @@ def test_register_with_multiple_names():
builder.register_executor(make_executor, name=["ExecutorA", "ExecutorB"])
# Set up workflow
workflow = builder.set_start_executor("ExecutorA").add_edge("ExecutorA", "ExecutorB").build()
workflow = builder.add_edge("ExecutorA", "ExecutorB").build()
# Verify both executors are present
assert "ExecutorA" in workflow.executors
@@ -208,7 +197,7 @@ def test_register_with_multiple_names():
def test_register_duplicate_name_raises_error():
"""Test that registering duplicate names raises an error."""
builder = WorkflowBuilder()
builder = WorkflowBuilder(start_executor="MyExecutor")
# Register first executor
builder.register_executor(lambda: MockExecutor(id="executor_1"), name="MyExecutor")
@@ -220,12 +209,11 @@ def test_register_duplicate_name_raises_error():
def test_register_duplicate_id_raises_error():
"""Test that registering duplicate id raises an error."""
builder = WorkflowBuilder()
builder = WorkflowBuilder(start_executor="MyExecutor1")
# Register first executor
builder.register_executor(lambda: MockExecutor(id="executor"), name="MyExecutor1")
builder.register_executor(lambda: MockExecutor(id="executor"), name="MyExecutor2")
builder.set_start_executor("MyExecutor1")
# Registering second executor with same ID should raise ValueError
with pytest.raises(ValueError, match="Executor with ID 'executor' has already been registered."):
@@ -234,7 +222,7 @@ def test_register_duplicate_id_raises_error():
def test_register_agent_basic():
"""Test basic agent registration with lazy initialization."""
builder = WorkflowBuilder()
builder = WorkflowBuilder(start_executor="TestAgent")
# Register an agent factory
result = builder.register_agent(lambda: DummyAgent(id="agent_test", name="test_agent"), name="TestAgent")
@@ -243,14 +231,14 @@ def test_register_agent_basic():
assert result is builder
# Build workflow and verify agent is wrapped in AgentExecutor
workflow = builder.set_start_executor("TestAgent").build()
workflow = builder.build()
assert "test_agent" in workflow.executors
assert isinstance(workflow.executors["test_agent"], AgentExecutor)
def test_register_agent_with_thread():
"""Test registering an agent with a custom thread."""
builder = WorkflowBuilder()
builder = WorkflowBuilder(start_executor="ThreadedAgent")
custom_thread = AgentThread()
# Register agent with custom thread
@@ -261,7 +249,7 @@ def test_register_agent_with_thread():
)
# Build workflow and verify agent executor configuration
workflow = builder.set_start_executor("ThreadedAgent").build()
workflow = builder.build()
executor = workflow.executors["threaded_agent"]
assert isinstance(executor, AgentExecutor)
@@ -271,7 +259,7 @@ def test_register_agent_with_thread():
def test_register_agent_duplicate_name_raises_error():
"""Test that registering agents with duplicate names raises an error."""
builder = WorkflowBuilder()
builder = WorkflowBuilder(start_executor="MyAgent")
# Register first agent
builder.register_agent(lambda: DummyAgent(id="agent1", name="first"), name="MyAgent")
@@ -283,14 +271,14 @@ def test_register_agent_duplicate_name_raises_error():
def test_register_and_add_edge_with_strings():
"""Test that registered executors can be connected using string names."""
builder = WorkflowBuilder()
builder = WorkflowBuilder(start_executor="Source")
# Register executors
builder.register_executor(lambda: MockExecutor(id="source"), name="Source")
builder.register_executor(lambda: MockExecutor(id="target"), name="Target")
# Add edge using string names
workflow = builder.set_start_executor("Source").add_edge("Source", "Target").build()
workflow = builder.add_edge("Source", "Target").build()
# Verify edge is created correctly
assert workflow.start_executor_id == "source"
@@ -300,14 +288,14 @@ def test_register_and_add_edge_with_strings():
def test_register_agent_and_add_edge_with_strings():
"""Test that registered agents can be connected using string names."""
builder = WorkflowBuilder()
builder = WorkflowBuilder(start_executor="Writer")
# Register agents
builder.register_agent(lambda: DummyAgent(id="writer_id", name="writer"), name="Writer")
builder.register_agent(lambda: DummyAgent(id="reviewer_id", name="reviewer"), name="Reviewer")
# Add edge using string names
workflow = builder.set_start_executor("Writer").add_edge("Writer", "Reviewer").build()
workflow = builder.add_edge("Writer", "Reviewer").build()
# Verify edge is created correctly
assert workflow.start_executor_id == "writer"
@@ -318,7 +306,7 @@ def test_register_agent_and_add_edge_with_strings():
def test_register_with_fan_out_edges():
"""Test using registered names with fan-out edge groups."""
builder = WorkflowBuilder()
builder = WorkflowBuilder(start_executor="Source")
# Register executors - IDs must match registered names
builder.register_executor(lambda: MockExecutor(id="Source"), name="Source")
@@ -326,7 +314,7 @@ def test_register_with_fan_out_edges():
builder.register_executor(lambda: MockExecutor(id="Target2"), name="Target2")
# Add fan-out edges using registered names
workflow = builder.set_start_executor("Source").add_fan_out_edges("Source", ["Target1", "Target2"]).build()
workflow = builder.add_fan_out_edges("Source", ["Target1", "Target2"]).build()
# Verify all executors are present
assert "Source" in workflow.executors
@@ -336,7 +324,7 @@ def test_register_with_fan_out_edges():
def test_register_with_fan_in_edges():
"""Test using registered names with fan-in edge groups."""
builder = WorkflowBuilder()
builder = WorkflowBuilder(start_executor="Source1")
# Register executors - IDs must match registered names
builder.register_executor(lambda: MockExecutor(id="Source1"), name="Source1")
@@ -345,13 +333,7 @@ def test_register_with_fan_in_edges():
# Add fan-in edges using registered names
# Both Source1 and Source2 need to be reachable, so connect Source1 to Source2
workflow = (
builder
.set_start_executor("Source1")
.add_edge("Source1", "Source2")
.add_fan_in_edges(["Source1", "Source2"], "Aggregator")
.build()
)
workflow = builder.add_edge("Source1", "Source2").add_fan_in_edges(["Source1", "Source2"], "Aggregator").build()
# Verify all executors are present
assert "Source1" in workflow.executors
@@ -361,7 +343,7 @@ def test_register_with_fan_in_edges():
def test_register_with_chain():
"""Test using registered names with add_chain."""
builder = WorkflowBuilder()
builder = WorkflowBuilder(start_executor="Step1")
# Register executors - IDs must match registered names
builder.register_executor(lambda: MockExecutor(id="Step1"), name="Step1")
@@ -369,7 +351,7 @@ def test_register_with_chain():
builder.register_executor(lambda: MockExecutor(id="Step3"), name="Step3")
# Add chain using registered names
workflow = builder.add_chain(["Step1", "Step2", "Step3"]).set_start_executor("Step1").build()
workflow = builder.add_chain(["Step1", "Step2", "Step3"]).build()
# Verify all executors are present
assert "Step1" in workflow.executors
@@ -387,15 +369,12 @@ def test_register_factory_called_only_once():
call_count += 1
return MockExecutor(id="Test")
builder = WorkflowBuilder()
builder = WorkflowBuilder(start_executor="Test")
builder.register_executor(factory, name="Test")
# Factory should not be called yet
assert call_count == 0
# Add edge without building
builder.set_start_executor("Test")
# Factory should still not be called
assert call_count == 0
@@ -409,7 +388,7 @@ def test_register_factory_called_only_once():
def test_mixing_eager_and_lazy_initialization_error():
"""Test that mixing eager executor instances with lazy string names raises appropriate error."""
builder = WorkflowBuilder()
builder = WorkflowBuilder(start_executor="Lazy")
# Create an eager executor instance
eager_executor = MockExecutor(id="eager")
@@ -430,7 +409,7 @@ def test_mixing_eager_and_lazy_initialization_error():
def test_register_with_condition():
"""Test adding edges with conditions using registered names."""
builder = WorkflowBuilder()
builder = WorkflowBuilder(start_executor="Source")
def condition_func(msg: MockMessage) -> bool:
return msg.data > 0
@@ -440,7 +419,7 @@ def test_register_with_condition():
builder.register_executor(lambda: MockExecutor(id="Target"), name="Target")
# Add edge with condition
workflow = builder.set_start_executor("Source").add_edge("Source", "Target", condition=condition_func).build()
workflow = builder.add_edge("Source", "Target", condition=condition_func).build()
# Verify workflow is built correctly
assert "Source" in workflow.executors
@@ -457,14 +436,14 @@ def test_register_agent_creates_unique_instances():
return agent
# Build first workflow
builder1 = WorkflowBuilder()
builder1 = WorkflowBuilder(start_executor="Agent")
builder1.register_agent(agent_factory, name="Agent")
_ = builder1.set_start_executor("Agent").build()
_ = builder1.build()
# Build second workflow
builder2 = WorkflowBuilder()
builder2 = WorkflowBuilder(start_executor="Agent")
builder2.register_agent(agent_factory, name="Agent")
_ = builder2.set_start_executor("Agent").build()
_ = builder2.build()
# Verify that two different agent instances were created
assert len(instance_ids) == 2
@@ -477,11 +456,10 @@ def test_register_agent_creates_unique_instances():
def test_with_output_from_returns_builder():
"""Test that with_output_from returns the builder for method chaining."""
executor_a = MockExecutor(id="executor_a")
builder = WorkflowBuilder()
builder = WorkflowBuilder(output_executors=[executor_a], start_executor=executor_a)
result = builder.with_output_from([executor_a])
assert result is builder
# Verify builder was created with output_executors
assert builder._output_executors == [executor_a]
def test_with_output_from_with_executor_instances():
@@ -490,10 +468,8 @@ def test_with_output_from_with_executor_instances():
executor_b = MockExecutor(id="executor_b")
workflow = (
WorkflowBuilder()
.set_start_executor(executor_a)
WorkflowBuilder(start_executor=executor_a, output_executors=[executor_b])
.add_edge(executor_a, executor_b)
.with_output_from([executor_b])
.build()
)
@@ -506,9 +482,7 @@ def test_with_output_from_with_agent_instances():
agent_a = DummyAgent(id="agent_a", name="writer")
agent_b = DummyAgent(id="agent_b", name="reviewer")
workflow = (
WorkflowBuilder().set_start_executor(agent_a).add_edge(agent_a, agent_b).with_output_from([agent_b]).build()
)
workflow = WorkflowBuilder(start_executor=agent_a, output_executors=[agent_b]).add_edge(agent_a, agent_b).build()
# Verify that the workflow was built with the agent's name as output executor
assert workflow._output_executors == ["reviewer"] # type: ignore
@@ -516,15 +490,10 @@ def test_with_output_from_with_agent_instances():
def test_with_output_from_with_registered_names():
"""Test with_output_from with registered factory names (strings)."""
workflow = (
WorkflowBuilder()
.register_executor(lambda: MockExecutor(id="ExecutorA"), name="ExecutorAFactory")
.register_executor(lambda: MockExecutor(id="ExecutorB"), name="ExecutorBFactory")
.set_start_executor("ExecutorAFactory")
.add_edge("ExecutorAFactory", "ExecutorBFactory")
.with_output_from(["ExecutorBFactory"])
.build()
)
builder = WorkflowBuilder(start_executor="ExecutorAFactory", output_executors=["ExecutorBFactory"])
builder.register_executor(lambda: MockExecutor(id="ExecutorA"), name="ExecutorAFactory")
builder.register_executor(lambda: MockExecutor(id="ExecutorB"), name="ExecutorBFactory")
workflow = builder.add_edge("ExecutorAFactory", "ExecutorBFactory").build()
# Verify that the workflow was built with the correct output executors
assert workflow._output_executors == ["ExecutorB"] # type: ignore
@@ -537,11 +506,9 @@ def test_with_output_from_with_multiple_executors():
executor_c = MockExecutor(id="executor_c")
workflow = (
WorkflowBuilder()
.set_start_executor(executor_a)
WorkflowBuilder(start_executor=executor_a, output_executors=[executor_a, executor_c])
.add_edge(executor_a, executor_b)
.add_edge(executor_b, executor_c)
.with_output_from([executor_a, executor_c])
.build()
)
@@ -549,51 +516,41 @@ def test_with_output_from_with_multiple_executors():
assert set(workflow._output_executors) == {"executor_a", "executor_c"} # type: ignore
def test_with_output_from_can_be_called_multiple_times():
"""Test that calling with_output_from multiple times overwrites the previous setting."""
def test_with_output_from_can_be_set_to_different_value():
"""Test that output_executors can be set at construction time."""
executor_a = MockExecutor(id="executor_a")
executor_b = MockExecutor(id="executor_b")
workflow = (
WorkflowBuilder()
.set_start_executor(executor_a)
WorkflowBuilder(start_executor=executor_a, output_executors=[executor_b])
.add_edge(executor_a, executor_b)
.with_output_from([executor_a])
.with_output_from([executor_b]) # This should overwrite the previous setting
.build()
)
# Verify that only the last setting is applied
# Verify that the setting is applied
assert workflow._output_executors == ["executor_b"] # type: ignore
def test_with_output_from_with_registered_agents():
"""Test with_output_from with registered agent factory names."""
workflow = (
WorkflowBuilder()
.register_agent(lambda: DummyAgent(id="agent1", name="writer"), name="WriterAgent")
.register_agent(lambda: DummyAgent(id="agent2", name="reviewer"), name="ReviewerAgent")
.set_start_executor("WriterAgent")
.add_edge("WriterAgent", "ReviewerAgent")
.with_output_from(["ReviewerAgent"])
.build()
)
builder = WorkflowBuilder(start_executor="WriterAgent", output_executors=["ReviewerAgent"])
builder.register_agent(lambda: DummyAgent(id="agent1", name="writer"), name="WriterAgent")
builder.register_agent(lambda: DummyAgent(id="agent2", name="reviewer"), name="ReviewerAgent")
workflow = builder.add_edge("WriterAgent", "ReviewerAgent").build()
# Verify that the workflow was built with the agent's resolved name
assert workflow._output_executors == ["reviewer"] # type: ignore
def test_with_output_from_in_fluent_chain():
"""Test that with_output_from works correctly in a fluent builder chain."""
def test_with_output_from_in_constructor():
"""Test that output_executors works correctly when set in the constructor."""
executor_a = MockExecutor(id="executor_a")
executor_b = MockExecutor(id="executor_b")
executor_c = MockExecutor(id="executor_c")
# Build workflow with with_output_from in the middle of the chain
# Build workflow with output_executors in the constructor
workflow = (
WorkflowBuilder()
.set_start_executor(executor_a)
.with_output_from([executor_c]) # Set early in the chain
WorkflowBuilder(start_executor=executor_a, output_executors=[executor_c])
.add_edge(executor_a, executor_b)
.add_edge(executor_b, executor_c)
.build()
@@ -607,13 +564,13 @@ def test_with_output_from_with_invalid_executor_raises_validation_error():
"""Test that with_output_from with an invalid executor raises an error."""
executor_a = MockExecutor(id="executor_a")
builder = WorkflowBuilder().set_start_executor(executor_a)
builder = WorkflowBuilder(start_executor=executor_a, output_executors=[MockExecutor(id="executor_b")])
# Attempting to set output from an executor not in the workflow should raise an error
with pytest.raises(
WorkflowValidationError, match="Output executor 'executor_b' is not present in the workflow graph"
):
builder.with_output_from([MockExecutor(id="executor_b")]).build()
builder.build()
# endregion
@@ -93,7 +93,7 @@ async def test_workflow_context_type_annotations_no_parameter() -> None:
async def func1(text: str, ctx: WorkflowContext) -> None:
await ctx.add_event(_TestEvent())
wf = WorkflowBuilder().set_start_executor(func1).build()
wf = WorkflowBuilder(start_executor=func1).build()
events = await wf.run("hello")
test_events = [e for e in events if isinstance(e, _TestEvent)]
assert len(test_events) == 1
@@ -110,7 +110,7 @@ async def test_workflow_context_type_annotations_no_parameter() -> None:
assert executor1.output_types == []
assert executor1.workflow_output_types == []
wf2 = WorkflowBuilder().set_start_executor(executor1).build()
wf2 = WorkflowBuilder(start_executor=executor1).build()
events2 = await wf2.run("hello")
test_events2 = [e for e in events2 if isinstance(e, _TestEvent)]
assert len(test_events2) == 1
@@ -126,7 +126,7 @@ async def test_workflow_context_type_annotations_message_type_parameter() -> Non
async def func2(text: str, ctx: WorkflowContext) -> None:
await ctx.add_event(_TestEvent(data=text))
wf = WorkflowBuilder().add_edge(func1, func2).set_start_executor(func1).build()
wf = WorkflowBuilder(start_executor=func1).add_edge(func1, func2).build()
events = await wf.run("hello")
test_events = [e for e in events if isinstance(e, _TestEvent)]
assert len(test_events) == 1
@@ -153,7 +153,7 @@ async def test_workflow_context_type_annotations_message_type_parameter() -> Non
assert executor2.output_types == []
assert executor2.workflow_output_types == []
wf2 = WorkflowBuilder().add_edge(executor1, executor2).set_start_executor(executor1).build()
wf2 = WorkflowBuilder(start_executor=executor1).add_edge(executor1, executor2).build()
events2 = await wf2.run("hello")
test_events2 = [e for e in events2 if isinstance(e, _TestEvent)]
assert len(test_events2) == 1
@@ -171,7 +171,7 @@ async def test_workflow_context_type_annotations_message_and_output_type_paramet
await ctx.add_event(_TestEvent(data=text))
await ctx.yield_output(text)
wf = WorkflowBuilder().add_edge(func1, func2).set_start_executor(func1).build()
wf = WorkflowBuilder(start_executor=func1).add_edge(func1, func2).build()
events = await wf.run("hello")
outputs = events.get_outputs()
assert len(outputs) == 1
@@ -199,7 +199,7 @@ async def test_workflow_context_type_annotations_message_and_output_type_paramet
assert executor2.output_types == []
assert executor2.workflow_output_types == [str]
wf2 = WorkflowBuilder().add_edge(executor1, executor2).set_start_executor(executor1).build()
wf2 = WorkflowBuilder(start_executor=executor1).add_edge(executor1, executor2).build()
events2 = await wf2.run("hello")
outputs2 = events2.get_outputs()
assert len(outputs2) == 1
@@ -78,7 +78,7 @@ class _KwargsCapturingAgent(BaseAgent):
async def test_sequential_kwargs_flow_to_agent() -> None:
"""Test that kwargs passed to SequentialBuilder workflow flow through to agent."""
agent = _KwargsCapturingAgent(name="seq_agent")
workflow = SequentialBuilder().participants([agent]).build()
workflow = SequentialBuilder(participants=[agent]).build()
custom_data = {"endpoint": "https://api.example.com", "version": "v1"}
user_token = {"user_name": "alice", "access_level": "admin"}
@@ -105,7 +105,7 @@ async def test_sequential_kwargs_flow_to_multiple_agents() -> None:
"""Test that kwargs flow to all agents in a sequential workflow."""
agent1 = _KwargsCapturingAgent(name="agent1")
agent2 = _KwargsCapturingAgent(name="agent2")
workflow = SequentialBuilder().participants([agent1, agent2]).build()
workflow = SequentialBuilder(participants=[agent1, agent2]).build()
custom_data = {"key": "value"}
@@ -123,7 +123,7 @@ async def test_sequential_kwargs_flow_to_multiple_agents() -> None:
async def test_sequential_run_kwargs_flow() -> None:
"""Test that kwargs flow through workflow.run() (non-streaming)."""
agent = _KwargsCapturingAgent(name="run_agent")
workflow = SequentialBuilder().participants([agent]).build()
workflow = SequentialBuilder(participants=[agent]).build()
_ = await workflow.run("test message", custom_data={"test": True})
@@ -141,7 +141,7 @@ async def test_concurrent_kwargs_flow_to_agents() -> None:
"""Test that kwargs flow to all agents in a concurrent workflow."""
agent1 = _KwargsCapturingAgent(name="concurrent1")
agent2 = _KwargsCapturingAgent(name="concurrent2")
workflow = ConcurrentBuilder().participants([agent1, agent2]).build()
workflow = ConcurrentBuilder(participants=[agent1, agent2]).build()
custom_data = {"batch_id": "123"}
user_token = {"user_name": "bob"}
@@ -188,13 +188,11 @@ async def test_groupchat_kwargs_flow_to_agents() -> None:
names = list(state.participants.keys())
return names[(turn_count - 1) % len(names)]
workflow = (
GroupChatBuilder()
.participants([agent1, agent2])
.with_orchestrator(selection_func=simple_selector)
.with_max_rounds(2) # Limit rounds to prevent infinite loop
.build()
)
workflow = GroupChatBuilder(
participants=[agent1, agent2],
max_rounds=2, # Limit rounds to prevent infinite loop
selection_func=simple_selector,
).build()
custom_data = {"session_id": "group123"}
@@ -230,7 +228,7 @@ async def test_kwargs_stored_in_state() -> None:
await ctx.send_message(msgs)
inspector = _StateInspector(id="inspector")
workflow = SequentialBuilder().participants([inspector]).build()
workflow = SequentialBuilder(participants=[inspector]).build()
async for event in workflow.run("test", my_kwarg="my_value", another=123, stream=True):
if event.type == "status" and event.state == WorkflowRunState.IDLE:
@@ -255,7 +253,7 @@ async def test_empty_kwargs_stored_as_empty_dict() -> None:
await ctx.send_message(msgs)
checker = _StateChecker(id="checker")
workflow = SequentialBuilder().participants([checker]).build()
workflow = SequentialBuilder(participants=[checker]).build()
# Run without any kwargs
async for event in workflow.run("test", stream=True):
@@ -275,7 +273,7 @@ async def test_empty_kwargs_stored_as_empty_dict() -> None:
async def test_kwargs_with_none_values() -> None:
"""Test that kwargs with None values are passed through correctly."""
agent = _KwargsCapturingAgent(name="none_test")
workflow = SequentialBuilder().participants([agent]).build()
workflow = SequentialBuilder(participants=[agent]).build()
async for event in workflow.run("test", optional_param=None, other_param="value", stream=True):
if event.type == "status" and event.state == WorkflowRunState.IDLE:
@@ -291,7 +289,7 @@ async def test_kwargs_with_none_values() -> None:
async def test_kwargs_with_complex_nested_data() -> None:
"""Test that complex nested data structures flow through correctly."""
agent = _KwargsCapturingAgent(name="nested_test")
workflow = SequentialBuilder().participants([agent]).build()
workflow = SequentialBuilder(participants=[agent]).build()
complex_data = {
"level1": {
@@ -318,8 +316,8 @@ async def test_kwargs_preserved_across_workflow_reruns() -> None:
agent = _KwargsCapturingAgent(name="rerun_test")
# Build separate workflows for each run to avoid "already running" error
workflow1 = SequentialBuilder().participants([agent]).build()
workflow2 = SequentialBuilder().participants([agent]).build()
workflow1 = SequentialBuilder(participants=[agent]).build()
workflow2 = SequentialBuilder(participants=[agent]).build()
# First run
async for event in workflow1.run("run1", run_id="first", stream=True):
@@ -349,11 +347,10 @@ async def test_handoff_kwargs_flow_to_agents() -> None:
agent2 = _KwargsCapturingAgent(name="specialist")
workflow = (
HandoffBuilder()
HandoffBuilder(termination_condition=lambda conv: len(conv) >= 4)
.participants([agent1, agent2])
.with_start_agent(agent1)
.with_autonomous_mode()
.with_termination_condition(lambda conv: len(conv) >= 4)
.build()
)
@@ -413,7 +410,7 @@ async def test_magentic_kwargs_flow_to_agents() -> None:
agent = _KwargsCapturingAgent(name="agent1")
manager = _MockManager()
workflow = MagenticBuilder().participants([agent]).with_manager(manager=manager).build()
workflow = MagenticBuilder(participants=[agent], manager=manager).build()
custom_data = {"session_id": "magentic123"}
@@ -463,7 +460,7 @@ async def test_magentic_kwargs_stored_in_state() -> None:
agent = _KwargsCapturingAgent(name="agent1")
manager = _MockManager()
magentic_workflow = MagenticBuilder().participants([agent]).with_manager(manager=manager).build()
magentic_workflow = MagenticBuilder(participants=[agent], manager=manager).build()
# Use MagenticWorkflow.run() which goes through the kwargs attachment path
custom_data = {"magentic_key": "magentic_value"}
@@ -485,7 +482,7 @@ async def test_magentic_kwargs_stored_in_state() -> None:
async def test_workflow_as_agent_run_propagates_kwargs_to_underlying_agent() -> None:
"""Test that kwargs passed to workflow_agent.run() flow through to the underlying agents."""
agent = _KwargsCapturingAgent(name="inner_agent")
workflow = SequentialBuilder().participants([agent]).build()
workflow = SequentialBuilder(participants=[agent]).build()
workflow_agent = workflow.as_agent(name="TestWorkflowAgent")
custom_data = {"endpoint": "https://api.example.com", "version": "v1"}
@@ -509,7 +506,7 @@ async def test_workflow_as_agent_run_propagates_kwargs_to_underlying_agent() ->
async def test_workflow_as_agent_run_stream_propagates_kwargs_to_underlying_agent() -> None:
"""Test that kwargs passed to workflow_agent.run() flow through to the underlying agents."""
agent = _KwargsCapturingAgent(name="inner_agent")
workflow = SequentialBuilder().participants([agent]).build()
workflow = SequentialBuilder(participants=[agent]).build()
workflow_agent = workflow.as_agent(name="TestWorkflowAgent")
custom_data = {"session_id": "xyz123"}
@@ -536,7 +533,7 @@ async def test_workflow_as_agent_propagates_kwargs_to_multiple_agents() -> None:
"""Test that kwargs flow to all agents when using workflow.as_agent()."""
agent1 = _KwargsCapturingAgent(name="agent1")
agent2 = _KwargsCapturingAgent(name="agent2")
workflow = SequentialBuilder().participants([agent1, agent2]).build()
workflow = SequentialBuilder(participants=[agent1, agent2]).build()
workflow_agent = workflow.as_agent(name="MultiAgentWorkflow")
custom_data = {"batch_id": "batch-001"}
@@ -553,7 +550,7 @@ async def test_workflow_as_agent_propagates_kwargs_to_multiple_agents() -> None:
async def test_workflow_as_agent_kwargs_with_none_values() -> None:
"""Test that kwargs with None values are passed through correctly via as_agent()."""
agent = _KwargsCapturingAgent(name="none_test_agent")
workflow = SequentialBuilder().participants([agent]).build()
workflow = SequentialBuilder(participants=[agent]).build()
workflow_agent = workflow.as_agent(name="NoneTestWorkflow")
_ = await workflow_agent.run("test", optional_param=None, other_param="value")
@@ -568,7 +565,7 @@ async def test_workflow_as_agent_kwargs_with_none_values() -> None:
async def test_workflow_as_agent_kwargs_with_complex_nested_data() -> None:
"""Test that complex nested data structures flow through correctly via as_agent()."""
agent = _KwargsCapturingAgent(name="nested_agent")
workflow = SequentialBuilder().participants([agent]).build()
workflow = SequentialBuilder(participants=[agent]).build()
workflow_agent = workflow.as_agent(name="NestedDataWorkflow")
complex_data = {
@@ -606,13 +603,13 @@ async def test_subworkflow_kwargs_propagation() -> None:
inner_agent = _KwargsCapturingAgent(name="inner_agent")
# Build the inner (sub) workflow with the agent
inner_workflow = SequentialBuilder().participants([inner_agent]).build()
inner_workflow = SequentialBuilder(participants=[inner_agent]).build()
# Wrap the inner workflow in a WorkflowExecutor so it can be used as a subworkflow
subworkflow_executor = WorkflowExecutor(workflow=inner_workflow, id="subworkflow_executor")
# Build the outer (parent) workflow containing the subworkflow
outer_workflow = SequentialBuilder().participants([subworkflow_executor]).build()
outer_workflow = SequentialBuilder(participants=[subworkflow_executor]).build()
# Define kwargs that should propagate to subworkflow
custom_data = {"api_key": "secret123", "endpoint": "https://api.example.com"}
@@ -670,13 +667,13 @@ async def test_subworkflow_kwargs_accessible_via_state() -> None:
# Build inner workflow with State reader
state_reader = _StateReader(id="state_reader")
inner_workflow = SequentialBuilder().participants([state_reader]).build()
inner_workflow = SequentialBuilder(participants=[state_reader]).build()
# Wrap as subworkflow
subworkflow_executor = WorkflowExecutor(workflow=inner_workflow, id="subworkflow")
# Build outer workflow
outer_workflow = SequentialBuilder().participants([subworkflow_executor]).build()
outer_workflow = SequentialBuilder(participants=[subworkflow_executor]).build()
# Run with kwargs
async for event in outer_workflow.run(
@@ -715,15 +712,15 @@ async def test_nested_subworkflow_kwargs_propagation() -> None:
inner_agent = _KwargsCapturingAgent(name="deeply_nested_agent")
# Build inner workflow
inner_workflow = SequentialBuilder().participants([inner_agent]).build()
inner_workflow = SequentialBuilder(participants=[inner_agent]).build()
inner_executor = WorkflowExecutor(workflow=inner_workflow, id="inner_executor")
# Build middle workflow containing inner
middle_workflow = SequentialBuilder().participants([inner_executor]).build()
middle_workflow = SequentialBuilder(participants=[inner_executor]).build()
middle_executor = WorkflowExecutor(workflow=middle_workflow, id="middle_executor")
# Build outer workflow containing middle
outer_workflow = SequentialBuilder().participants([middle_executor]).build()
outer_workflow = SequentialBuilder(participants=[middle_executor]).build()
# Run with kwargs
async for event in outer_workflow.run(
@@ -268,8 +268,7 @@ async def test_end_to_end_workflow_tracing(span_exporter: InMemorySpanExporter)
# Create workflow with fan-in: executor1 -> [executor2, executor3] -> aggregator
workflow = (
WorkflowBuilder()
.set_start_executor(executor1)
WorkflowBuilder(start_executor=executor1)
.add_fan_out_edges(executor1, [executor2, executor3])
.add_fan_in_edges([executor2, executor3], aggregator)
.build()
@@ -297,11 +296,11 @@ async def test_end_to_end_workflow_tracing(span_exporter: InMemorySpanExporter)
span_exporter.clear()
# Test workflow with name and description - verify OTEL attributes
(
WorkflowBuilder(name="Test Pipeline", description="Test workflow description")
.set_start_executor(MockExecutor("start"))
.build()
)
WorkflowBuilder(
name="Test Pipeline",
description="Test workflow description",
start_executor=MockExecutor("start"),
).build()
build_spans_with_metadata = [s for s in span_exporter.get_finished_spans() if s.name == "workflow.build"]
assert len(build_spans_with_metadata) == 1
@@ -412,7 +411,7 @@ async def test_workflow_error_handling_in_tracing(span_exporter: InMemorySpanExp
raise ValueError("Test error")
failing_executor = FailingExecutor()
workflow = WorkflowBuilder().set_start_executor(failing_executor).build()
workflow = WorkflowBuilder(start_executor=failing_executor).build()
# Run workflow and expect error
with pytest.raises(ValueError, match="Test error"):
@@ -475,10 +474,10 @@ async def test_message_trace_context_serialization(span_exporter: InMemorySpanEx
async def test_workflow_build_error_tracing(span_exporter: InMemorySpanExporter) -> None:
"""Test that build errors are properly recorded in build spans."""
# Test validation error by not setting start executor
builder = WorkflowBuilder()
# Test validation error by referencing a non-existent start executor
builder = WorkflowBuilder(start_executor="NonExistent")
with pytest.raises(ValueError, match="Starting executor must be set"):
with pytest.raises(ValueError):
builder.build()
spans = span_exporter.get_finished_spans()
@@ -501,5 +500,5 @@ async def test_workflow_build_error_tracing(span_exporter: InMemorySpanExporter)
error_event = error_events[0]
assert error_event.attributes is not None
assert "Starting executor must be set" in str(error_event.attributes.get("build.error.message"))
assert "starting executor" in str(error_event.attributes.get("build.error.message")).lower()
assert error_event.attributes.get("build.error.type") == "ValueError"
@@ -28,7 +28,7 @@ class FailingExecutor(Executor):
async def test_executor_failed_and_workflow_failed_events_streaming():
failing = FailingExecutor(id="f")
wf: Workflow = WorkflowBuilder().set_start_executor(failing).build()
wf: Workflow = WorkflowBuilder(start_executor=failing).build()
events: list[object] = []
with pytest.raises(RuntimeError, match="boom"):
@@ -86,7 +86,7 @@ async def test_executor_failed_event_from_second_executor_in_chain():
"""Test that executor_failed event is emitted when a non-start executor fails."""
passthrough = PassthroughExecutor(id="passthrough")
failing = FailingExecutor(id="failing")
wf: Workflow = WorkflowBuilder().set_start_executor(passthrough).add_edge(passthrough, failing).build()
wf: Workflow = WorkflowBuilder(start_executor=passthrough).add_edge(passthrough, failing).build()
events: list[object] = []
with pytest.raises(RuntimeError, match="boom"):
@@ -131,7 +131,7 @@ class Requester(Executor):
async def test_idle_with_pending_requests_status_streaming():
simple_executor = SimpleExecutor(id="simple")
requester = Requester(id="req")
wf = WorkflowBuilder().set_start_executor(simple_executor).add_edge(simple_executor, requester).build()
wf = WorkflowBuilder(start_executor=simple_executor).add_edge(simple_executor, requester).build()
events = [ev async for ev in wf.run("start", stream=True)] # Consume stream fully
@@ -153,7 +153,7 @@ class Completer(Executor):
async def test_completed_status_streaming():
c = Completer(id="c")
wf = WorkflowBuilder().set_start_executor(c).build()
wf = WorkflowBuilder(start_executor=c).build()
events = [ev async for ev in wf.run("ok", stream=True)] # no raise
# Last status should be IDLE
status = [e for e in events if isinstance(e, WorkflowEvent) and e.type == "status"]
@@ -163,7 +163,7 @@ async def test_completed_status_streaming():
async def test_started_and_completed_event_origins():
c = Completer(id="c-origin")
wf = WorkflowBuilder().set_start_executor(c).build()
wf = WorkflowBuilder(start_executor=c).build()
events = [ev async for ev in wf.run("payload", stream=True)]
started = next(e for e in events if isinstance(e, WorkflowEvent) and e.type == "started")
@@ -181,21 +181,21 @@ async def test_started_and_completed_event_origins():
async def test_non_streaming_final_state_helpers():
# Completed case
c = Completer(id="c")
wf1 = WorkflowBuilder().set_start_executor(c).build()
wf1 = WorkflowBuilder(start_executor=c).build()
result1: WorkflowRunResult = await wf1.run("done")
assert result1.get_final_state() == WorkflowRunState.IDLE
# Idle-with-pending-request case
simple_executor = SimpleExecutor(id="simple")
requester = Requester(id="req")
wf2 = WorkflowBuilder().set_start_executor(simple_executor).add_edge(simple_executor, requester).build()
wf2 = WorkflowBuilder(start_executor=simple_executor).add_edge(simple_executor, requester).build()
result2: WorkflowRunResult = await wf2.run("start")
assert result2.get_final_state() == WorkflowRunState.IDLE_WITH_PENDING_REQUESTS
async def test_run_includes_status_events_completed():
c = Completer(id="c2")
wf = WorkflowBuilder().set_start_executor(c).build()
wf = WorkflowBuilder(start_executor=c).build()
result: WorkflowRunResult = await wf.run("ok")
timeline = result.status_timeline()
assert timeline, "Expected status timeline in non-streaming run() results"
@@ -205,7 +205,7 @@ async def test_run_includes_status_events_completed():
async def test_run_includes_status_events_idle_with_requests():
simple_executor = SimpleExecutor(id="simple")
requester = Requester(id="req2")
wf = WorkflowBuilder().set_start_executor(simple_executor).add_edge(simple_executor, requester).build()
wf = WorkflowBuilder(start_executor=simple_executor).add_edge(simple_executor, requester).build()
result: WorkflowRunResult = await wf.run("start")
timeline = result.status_timeline()
assert timeline, "Expected status timeline in non-streaming run() results"
@@ -137,12 +137,6 @@ class DeclarativeWorkflowBuilder:
Raises:
ValueError: If no actions are defined (empty workflow), or validation fails
"""
builder = WorkflowBuilder(name=self._workflow_id)
# Enable checkpointing if storage is provided
if self._checkpoint_storage:
builder.with_checkpointing(self._checkpoint_storage)
actions = self._yaml_def.get("actions", [])
if not actions:
# Empty workflow - raise an error since we need at least one executor
@@ -152,6 +146,13 @@ class DeclarativeWorkflowBuilder:
if self._validate:
self._validate_workflow(actions)
# Use a placeholder for start_executor; it will be overwritten below via _set_start_executor
builder = WorkflowBuilder(
start_executor="_declarative_placeholder",
name=self._workflow_id,
checkpoint_storage=self._checkpoint_storage,
)
# First pass: create all executors
entry_executor = self._create_executors_for_actions(actions, builder)
@@ -164,11 +165,11 @@ class DeclarativeWorkflowBuilder:
# Create an entry passthrough node and wire to the structure's branches
entry_node = JoinExecutor({"kind": "Entry"}, id="_workflow_entry")
self._executors[entry_node.id] = entry_node
builder.set_start_executor(entry_node)
builder._set_start_executor(entry_node)
# Use _add_sequential_edge which knows how to wire to structures
self._add_sequential_edge(builder, entry_node, entry_executor)
else:
builder.set_start_executor(entry_executor)
builder._set_start_executor(entry_executor)
else:
raise ValueError("Failed to create any executors from actions.")
@@ -2012,7 +2012,7 @@ class TestBuilderControlFlowCreation:
# Create builder with minimal yaml definition
yaml_def = {"name": "test_workflow", "actions": []}
graph_builder = DeclarativeWorkflowBuilder(yaml_def)
wb = WorkflowBuilder()
wb = WorkflowBuilder(start_executor="dummy")
action_def = {
"kind": "GotoAction",
@@ -2036,7 +2036,7 @@ class TestBuilderControlFlowCreation:
yaml_def = {"name": "test_workflow", "actions": []}
graph_builder = DeclarativeWorkflowBuilder(yaml_def)
wb = WorkflowBuilder()
wb = WorkflowBuilder(start_executor="dummy")
action_def = {
"kind": "GotoAction",
@@ -2056,7 +2056,7 @@ class TestBuilderControlFlowCreation:
yaml_def = {"name": "test_workflow", "actions": []}
graph_builder = DeclarativeWorkflowBuilder(yaml_def)
wb = WorkflowBuilder()
wb = WorkflowBuilder(start_executor="dummy")
action_def = {
"kind": "GotoAction",
@@ -2094,7 +2094,7 @@ class TestBuilderControlFlowCreation:
yaml_def = {"name": "test_workflow", "actions": []}
graph_builder = DeclarativeWorkflowBuilder(yaml_def)
wb = WorkflowBuilder()
wb = WorkflowBuilder(start_executor="dummy")
# Create a mock loop_next executor
loop_next = ForeachNextExecutor(
@@ -2124,7 +2124,7 @@ class TestBuilderControlFlowCreation:
yaml_def = {"name": "test_workflow", "actions": []}
graph_builder = DeclarativeWorkflowBuilder(yaml_def)
wb = WorkflowBuilder()
wb = WorkflowBuilder(start_executor="dummy")
action_def = {
"kind": "BreakLoop",
@@ -2149,7 +2149,7 @@ class TestBuilderControlFlowCreation:
yaml_def = {"name": "test_workflow", "actions": []}
graph_builder = DeclarativeWorkflowBuilder(yaml_def)
wb = WorkflowBuilder()
wb = WorkflowBuilder(start_executor="dummy")
# Create a mock loop_next executor
loop_next = ForeachNextExecutor(
@@ -2179,7 +2179,7 @@ class TestBuilderControlFlowCreation:
yaml_def = {"name": "test_workflow", "actions": []}
graph_builder = DeclarativeWorkflowBuilder(yaml_def)
wb = WorkflowBuilder()
wb = WorkflowBuilder(start_executor="dummy")
action_def = {
"kind": "ContinueLoop",
@@ -2203,7 +2203,7 @@ class TestBuilderEdgeWiring:
yaml_def = {"name": "test_workflow", "actions": []}
graph_builder = DeclarativeWorkflowBuilder(yaml_def)
wb = WorkflowBuilder()
wb = WorkflowBuilder(start_executor="dummy")
# Create a mock source executor
source = SendActivityExecutor({"kind": "SendActivity", "activity": {"text": "test"}}, id="source")
@@ -2236,7 +2236,7 @@ class TestBuilderEdgeWiring:
yaml_def = {"name": "test_workflow", "actions": []}
graph_builder = DeclarativeWorkflowBuilder(yaml_def)
wb = WorkflowBuilder()
wb = WorkflowBuilder(start_executor="dummy")
source = SendActivityExecutor({"kind": "SendActivity", "activity": {"text": "source"}}, id="source")
target = SendActivityExecutor({"kind": "SendActivity", "activity": {"text": "target"}}, id="target")
+1 -1
View File
@@ -96,7 +96,7 @@ agents/
│ ├── agent.py
│ └── .env # Optional: API keys, config vars
├── my_workflow/
│ ├── __init__.py # Must export: workflow = WorkflowBuilder()...
│ ├── __init__.py # Must export: workflow = WorkflowBuilder(start_executor=...)...
│ ├── workflow.py
│ └── .env # Optional: environment variables
└── .env # Optional: shared environment variables
@@ -540,7 +540,7 @@ class EntityDiscovery:
This safely checks for module-level assignments like:
- agent = ChatAgent(...)
- workflow = WorkflowBuilder()...
- workflow = WorkflowBuilder(start_executor=...)...
Args:
file_path: Python file to check
@@ -441,7 +441,7 @@ class AgentFrameworkExecutor:
if not checkpoint_id:
error_msg = (
"Cannot process HIL responses without a checkpoint. "
"Workflows using HIL must be configured with .with_checkpointing() "
"Workflows using HIL must be configured with checkpoint_storage in constructor"
"and a checkpoint must exist before sending responses."
)
logger.error(error_msg)
@@ -488,7 +488,7 @@ async def sequential_workflow() -> tuple[AgentFrameworkExecutor, str, MockBaseCh
system_message="You are a reviewer. Provide constructive feedback.",
)
workflow = SequentialBuilder().participants([writer, reviewer]).build()
workflow = SequentialBuilder(participants=[writer, reviewer]).build()
discovery = EntityDiscovery(None)
mapper = MessageMapper()
@@ -540,7 +540,7 @@ async def concurrent_workflow() -> tuple[AgentFrameworkExecutor, str, MockBaseCh
system_message="You are a summarizer. Provide concise summaries.",
)
workflow = ConcurrentBuilder().participants([researcher, analyst, summarizer]).build()
workflow = ConcurrentBuilder(participants=[researcher, analyst, summarizer]).build()
discovery = EntityDiscovery(None)
mapper = MessageMapper()
@@ -76,12 +76,12 @@ def test_workflow():
executor = WorkflowTestExecutor(id="test_executor")
checkpoint_storage = InMemoryCheckpointStorage()
return (
WorkflowBuilder(name="Test Workflow", description="Test checkpoint behavior")
.set_start_executor(executor)
.with_checkpointing(checkpoint_storage)
.build()
)
return WorkflowBuilder(
name="Test Workflow",
description="Test checkpoint behavior",
start_executor=executor,
checkpoint_storage=checkpoint_storage,
).build()
class TestCheckpointConversationManager:
@@ -335,7 +335,7 @@ class TestIntegration:
# Get checkpoint storage for this session
checkpoint_storage = checkpoint_manager.get_checkpoint_storage(conversation_id)
# Set build-time storage (equivalent to .with_checkpointing() at build time)
# Set build-time storage (equivalent to checkpoint_storage= at build time)
# Note: In production, DevUI uses runtime injection via run(stream=True) parameter
if hasattr(test_workflow, "_runner") and hasattr(test_workflow._runner, "context"):
test_workflow._runner.context._checkpoint_storage = checkpoint_storage
@@ -399,7 +399,7 @@ class TestIntegration:
"""Test that workflows automatically save checkpoints to our conversation-backed storage.
This is the critical end-to-end test that verifies the entire checkpoint flow:
1. Storage is set as build-time storage (simulates .with_checkpointing())
1. Storage is set as build-time storage (simulates checkpoint_storage=...)
2. Workflow runs and pauses at HIL point (IDLE_WITH_PENDING_REQUESTS status)
3. Framework automatically saves checkpoint to our storage
4. Checkpoint is accessible via manager for UI to list/resume
@@ -135,10 +135,8 @@ from agent_framework import WorkflowBuilder, FunctionExecutor
def test_func(input: str) -> str:
return f"Processed: {input}"
builder = WorkflowBuilder()
executor = FunctionExecutor(id="test_executor", func=test_func)
builder.set_start_executor(executor)
workflow = builder.build()
workflow = WorkflowBuilder(start_executor=executor).build()
""")
discovery = EntityDiscovery(str(temp_path))
@@ -182,10 +180,8 @@ from agent_framework import WorkflowBuilder, FunctionExecutor
def test_func(input: str) -> str:
return f"Processed: {input}"
builder = WorkflowBuilder()
executor = FunctionExecutor(id="test_executor", func=test_func)
builder.set_start_executor(executor)
workflow = builder.build()
workflow = WorkflowBuilder(start_executor=executor).build()
""")
# Create agent with agent.py
@@ -243,10 +239,8 @@ from agent_framework import WorkflowBuilder, FunctionExecutor
def test_func(input: str) -> str:
return "v1"
builder = WorkflowBuilder()
executor = FunctionExecutor(id="test_executor", func=test_func)
builder.set_start_executor(executor)
workflow = builder.build()
workflow = WorkflowBuilder(start_executor=executor).build()
""")
discovery = EntityDiscovery(str(temp_path))
@@ -266,12 +260,9 @@ def test_func(input: str) -> str:
def test_func2(input: str) -> str:
return "v2_extra"
builder = WorkflowBuilder()
executor1 = FunctionExecutor(id="test_executor", func=test_func)
executor2 = FunctionExecutor(id="test_executor2", func=test_func2)
builder.set_start_executor(executor1)
builder.add_edge(executor1, executor2)
workflow = builder.build()
workflow = WorkflowBuilder(start_executor=executor1).add_edge(executor1, executor2).build()
""")
# Without invalidation, gets cached version
@@ -298,10 +289,8 @@ async def test_in_memory_entities_bypass_lazy_loading():
def test_func(input: str) -> str:
return f"Processed: {input}"
builder = WorkflowBuilder()
executor = FunctionExecutor(id="test_executor", func=test_func)
builder.set_start_executor(executor)
workflow = builder.build()
workflow = WorkflowBuilder(start_executor=executor).build()
discovery = EntityDiscovery()
@@ -175,10 +175,12 @@ async def test_workflow_streaming_execution():
def process_input(input_data: str) -> str:
return f"Processed: {input_data}"
builder = WorkflowBuilder(name="Test Workflow", description="Test workflow for execution")
start_executor = FunctionExecutor(id="process", func=process_input)
builder.set_start_executor(start_executor)
workflow = builder.build()
workflow = WorkflowBuilder(
name="Test Workflow",
description="Test workflow for execution",
start_executor=start_executor,
).build()
# Create executor and register workflow
discovery = EntityDiscovery(None)
@@ -213,10 +215,12 @@ async def test_workflow_sync_execution():
def echo(text: str) -> str:
return f"Echo: {text}"
builder = WorkflowBuilder(name="Echo Workflow", description="Simple echo workflow")
start_executor = FunctionExecutor(id="echo", func=echo)
builder.set_start_executor(start_executor)
workflow = builder.build()
workflow = WorkflowBuilder(
name="Echo Workflow",
description="Simple echo workflow",
start_executor=start_executor,
).build()
# Create executor and register workflow
discovery = EntityDiscovery(None)
@@ -308,10 +312,12 @@ async def test_full_pipeline_workflow_events_are_json_serializable():
system_message="You are a test assistant.",
)
builder = WorkflowBuilder(name="Serialization Test Workflow", description="Test workflow")
agent_executor = AgentExecutor(id="agent_node", agent=agent)
builder.set_start_executor(agent_executor)
workflow = builder.build()
workflow = WorkflowBuilder(
name="Serialization Test Workflow",
description="Test workflow",
start_executor=agent_executor,
).build()
# Create executor and register
discovery = EntityDiscovery(None)
@@ -420,11 +426,11 @@ async def test_executor_parse_structured_extracts_input_for_string_workflow():
async def process(self, text: str, ctx: WorkflowContext[Any, Any]) -> None:
await ctx.yield_output(f"Got: {text}")
workflow = (
WorkflowBuilder(name="String Workflow", description="Accepts string")
.set_start_executor(StringInputExecutor(id="str_exec"))
.build()
)
workflow = WorkflowBuilder(
name="String Workflow",
description="Accepts string",
start_executor=StringInputExecutor(id="str_exec"),
).build()
executor = AgentFrameworkExecutor(EntityDiscovery(None), MessageMapper())
@@ -445,11 +451,11 @@ async def test_executor_parse_raw_string_for_string_workflow():
async def process(self, text: str, ctx: WorkflowContext[Any, Any]) -> None:
await ctx.yield_output(f"Got: {text}")
workflow = (
WorkflowBuilder(name="String Workflow", description="Accepts string")
.set_start_executor(StringInputExecutor(id="str_exec"))
.build()
)
workflow = WorkflowBuilder(
name="String Workflow",
description="Accepts string",
start_executor=StringInputExecutor(id="str_exec"),
).build()
executor = AgentFrameworkExecutor(EntityDiscovery(None), MessageMapper())
@@ -490,11 +496,11 @@ async def test_executor_parse_stringified_json_workflow_input():
await ctx.yield_output(f"Got: {data.input}")
# Build workflow with Pydantic input type
workflow = (
WorkflowBuilder(name="Pydantic Workflow", description="Accepts Pydantic input")
.set_start_executor(PydanticInputExecutor(id="pydantic_exec"))
.build()
)
workflow = WorkflowBuilder(
name="Pydantic Workflow",
description="Accepts Pydantic input",
start_executor=PydanticInputExecutor(id="pydantic_exec"),
).build()
executor = AgentFrameworkExecutor(EntityDiscovery(None), MessageMapper())
@@ -689,11 +695,11 @@ async def test_full_pipeline_workflow_output_event_serialization():
await ctx.yield_output({"final": "result", "data": [1, 2, 3]})
# Build workflow
workflow = (
WorkflowBuilder(name="Output Workflow", description="Tests yield_output")
.set_start_executor(OutputtingExecutor(id="outputter"))
.build()
)
workflow = WorkflowBuilder(
name="Output Workflow",
description="Tests yield_output",
start_executor=OutputtingExecutor(id="outputter"),
).build()
# Create DevUI executor and register workflow
discovery = EntityDiscovery(None)
@@ -97,10 +97,7 @@ def workflow_two_agents():
# Build workflow: analyzer -> advisor
workflow = (
WorkflowBuilder()
.set_start_executor(analyzer_executor)
.add_edge(analyzer_executor, advisor_executor)
.build()
WorkflowBuilder(start_executor=analyzer_executor).add_edge(analyzer_executor, advisor_executor).build()
)
yield workflow
+2 -5
View File
@@ -165,15 +165,12 @@ from agent_framework.lab.tau2 import TaskRunner
class WorkflowTaskRunner(TaskRunner):
def build_conversation_workflow(self, assistant_agent, user_simulator_agent):
# Build a custom workflow
builder = WorkflowBuilder()
# Create agent executors
assistant_executor = AgentExecutor(assistant_agent, id="assistant_agent")
user_executor = AgentExecutor(user_simulator_agent, id="user_simulator")
# Add workflow edges and conditions
builder.set_start_executor(assistant_executor)
# Build a custom workflow with start executor
builder = WorkflowBuilder(start_executor=assistant_executor)
builder.add_edge(assistant_executor, user_executor)
builder.add_edge(user_executor, assistant_executor, condition=self.should_not_stop)
@@ -288,8 +288,8 @@ class TaskRunner:
# Creates a cyclic workflow: Orchestrator -> Assistant -> Orchestrator -> User -> Orchestrator...
# The orchestrator acts as a message router that flips roles and routes to appropriate agent
return (
WorkflowBuilder(max_iterations=10000) # Unlimited - we control termination via should_not_stop
.set_start_executor(orchestrator) # Orchestrator manages the conversation flow
# Orchestrator manages the conversation flow
WorkflowBuilder(max_iterations=10000, start_executor=orchestrator)
.add_edge(orchestrator, self._assistant_executor) # Route messages to assistant
.add_edge(
self._assistant_executor, orchestrator, condition=self.should_not_stop
+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()
@@ -77,7 +77,7 @@ async def run_agent_framework() -> None:
)
# Create sequential workflow
workflow = SequentialBuilder().participants([researcher, writer, editor]).build()
workflow = SequentialBuilder(participants=[researcher, writer, editor]).build()
# Run the workflow
print("[Agent Framework] Sequential conversation:")
@@ -137,7 +137,7 @@ async def run_agent_framework_with_cycle() -> None:
await context.send_message(AgentExecutorRequest(messages=response.full_conversation, should_respond=True))
workflow = (
WorkflowBuilder()
WorkflowBuilder(start_executor=researcher)
.add_edge(researcher, writer)
.add_edge(writer, editor)
.add_edge(
@@ -145,7 +145,6 @@ async def run_agent_framework_with_cycle() -> None:
check_approval,
)
.add_edge(check_approval, researcher)
.set_start_executor(researcher)
.build()
)
@@ -85,18 +85,14 @@ async def run_agent_framework() -> None:
description="Expert in databases and SQL",
)
workflow = (
GroupChatBuilder()
.participants([python_expert, javascript_expert, database_expert])
.with_orchestrator(
agent=client.as_agent(
name="selector_manager",
instructions="Based on the conversation, select the most appropriate expert to respond next.",
),
)
.with_max_rounds(1)
.build()
)
workflow = GroupChatBuilder(
participants=[python_expert, javascript_expert, database_expert],
max_rounds=1,
orchestrator_agent=client.as_agent(
name="selector_manager",
instructions="Based on the conversation, select the most appropriate expert to respond next.",
),
).build()
# Run with a question that requires expert selection
print("[Agent Framework] Group chat conversation:")
@@ -138,10 +138,10 @@ async def run_agent_framework() -> None:
HandoffBuilder(
name="support_handoff",
participants=[triage_agent, billing_agent, tech_support],
termination_condition=lambda conv: sum(1 for msg in conv if msg.role == "user") > 3,
)
.with_start_agent(triage_agent)
.add_handoff(triage_agent, [billing_agent, tech_support])
.with_termination_condition(lambda conv: sum(1 for msg in conv if msg.role == "user") > 3)
.build()
)
@@ -91,21 +91,17 @@ async def run_agent_framework() -> None:
)
# Create Magentic workflow
workflow = (
MagenticBuilder()
.participants([researcher, coder, reviewer])
.with_manager(
agent=client.as_agent(
name="magentic_manager",
instructions="You coordinate a team to complete complex tasks efficiently.",
description="Orchestrator for team coordination",
),
max_round_count=20,
max_stall_count=3,
max_reset_count=1,
)
.build()
)
workflow = MagenticBuilder(
participants=[researcher, coder, reviewer],
manager_agent=client.as_agent(
name="magentic_manager",
instructions="You coordinate a team to complete complex tasks efficiently.",
description="Orchestrator for team coordination",
),
max_round_count=20,
max_stall_count=3,
max_reset_count=1,
).build()
# Run complex task
last_message_id: str | None = None
@@ -31,7 +31,7 @@ def main():
)
# Build a concurrent workflow
workflow = ConcurrentBuilder().participants([researcher, marketer, legal]).build()
workflow = ConcurrentBuilder(participants=[researcher, marketer, legal]).build()
# Convert the workflow to an agent
workflow_agent = workflow.as_agent()
@@ -319,8 +319,7 @@ async def _create_workflow(project_client, credential):
# 7. booking_info_aggregation, booking_payment, activity_search → final_coordinator (final aggregation, fan-in)
workflow = (
WorkflowBuilder(name="Travel Planning Workflow")
.set_start_executor(start_executor)
WorkflowBuilder(name="Travel Planning Workflow", start_executor=start_executor)
.add_edge(start_executor, travel_request_handler)
.add_fan_out_edges(travel_request_handler, [hotel_search_agent, flight_search_agent, activity_search_agent])
.add_edge(hotel_search_agent, booking_info_aggregation_agent)
@@ -8,7 +8,6 @@ from agent_framework import (
AgentResponseUpdate,
ChatAgent,
CitationAnnotation,
Content,
HostedCodeInterpreterTool,
HostedFileContent,
TextContent,
@@ -4,7 +4,7 @@ import asyncio
import base64
import anyio
from agent_framework import Content, HostedImageGenerationTool
from agent_framework import HostedImageGenerationTool
from agent_framework.openai import OpenAIResponsesClient
"""OpenAI Responses Client Streaming Image Generation Example
@@ -662,8 +662,8 @@ def create_complex_workflow():
WorkflowBuilder(
name="Data Processing Pipeline",
description="Complex workflow with parallel validation, transformation, and quality assurance stages",
start_executor=data_ingestion,
)
.set_start_executor(data_ingestion)
# Fan-out to validation stage
.add_fan_out_edges(data_ingestion, [schema_validator, quality_validator, security_validator])
# Fan-in from validation to aggregator
@@ -102,8 +102,8 @@ def main():
WorkflowBuilder(
name="Text Transformer",
description="Simple 2-step workflow that converts text to uppercase and adds exclamation",
start_executor=upper_executor,
)
.set_start_executor(upper_executor)
.add_edge(upper_executor, exclaim_executor)
.build()
)
@@ -392,13 +392,13 @@ legitimate_message_handler = LegitimateMessageHandler(id="legitimate_message_han
final_processor = FinalProcessor(id="final_processor")
# Build the comprehensive 4-step workflow with branching logic and HIL support
# Note: No .with_checkpointing() call - DevUI will pass checkpoint_storage at runtime
# Note: No checkpoint_storage in constructor - DevUI will pass checkpoint_storage at runtime
workflow = (
WorkflowBuilder(
name="Email Spam Detector",
description="4-step email classification workflow with human-in-the-loop spam approval",
start_executor=email_preprocessor,
)
.set_start_executor(email_preprocessor)
.add_edge(email_preprocessor, spam_detector)
# HIL handled within spam_detector via @response_handler
# Continue with branching logic after human approval
@@ -132,8 +132,8 @@ workflow = (
WorkflowBuilder(
name="Content Review Workflow",
description="Multi-agent content creation workflow with quality-based routing (Writer → Reviewer → Editor/Publisher)",
start_executor=writer,
)
.set_start_executor(writer)
.add_edge(writer, reviewer)
# Branch 1: High quality (>= 80) goes directly to publisher
.add_edge(reviewer, publisher, condition=is_approved)
@@ -81,9 +81,8 @@ async def run_sequential_workflow() -> None:
# Step 2: Build the workflow with the defined edges.
workflow = (
WorkflowBuilder()
WorkflowBuilder(start_executor=upper_case_executor)
.add_edge(upper_case_executor, reverse_text_executor)
.set_start_executor(upper_case_executor)
.build()
)
@@ -17,7 +17,7 @@ The default aggregator fans in their results and yields output containing
a list[ChatMessage] representing the concatenated conversations from all agents.
Demonstrates:
- Minimal wiring with ConcurrentBuilder().participants([...]).build()
- Minimal wiring with ConcurrentBuilder(participants=[...]).build()
- Fan-out to multiple agents, fan-in aggregation of final ChatMessages
- Workflow completion when idle with no pending work
@@ -57,7 +57,7 @@ async def main() -> None:
# 2) Build a concurrent workflow
# Participants are either Agents (type of SupportsAgentRun) or Executors
workflow = ConcurrentBuilder().participants([researcher, marketer, legal]).build()
workflow = ConcurrentBuilder(participants=[researcher, marketer, legal]).build()
# 3) Run with a single prompt and pretty-print the final combined messages
events = await workflow.run("We are launching a new budget-friendly electric bike for urban commuters.")
@@ -27,7 +27,7 @@ ConcurrentBuilder API and the default aggregator.
Demonstrates:
- Executors that create their ChatAgent in __init__ (via AzureOpenAIChatClient)
- A @handler that converts AgentExecutorRequest -> AgentExecutorResponse
- ConcurrentBuilder().participants([...]) to build fan-out/fan-in
- ConcurrentBuilder(participants=[...]) to build fan-out/fan-in
- Default aggregator returning list[ChatMessage] (one user + one assistant per agent)
- Workflow completion when all participants become idle
@@ -103,7 +103,7 @@ async def main() -> None:
marketer = MarketerExec(chat_client)
legal = LegalExec(chat_client)
workflow = ConcurrentBuilder().participants([researcher, marketer, legal]).build()
workflow = ConcurrentBuilder(participants=[researcher, marketer, legal]).build()
events = await workflow.run("We are launching a new budget-friendly electric bike for urban commuters.")
outputs = events.get_outputs()
@@ -18,7 +18,7 @@ to synthesize a concise, consolidated summary from the experts' outputs.
The workflow completes when all participants become idle.
Demonstrates:
- ConcurrentBuilder().participants([...]).with_aggregator(callback)
- ConcurrentBuilder(participants=[...]).with_aggregator(callback)
- Fan-out to agents and fan-in at an aggregator
- Aggregation implemented via an LLM call (chat_client.get_response)
- Workflow output yielded with the synthesized summary string
@@ -87,7 +87,7 @@ async def main() -> None:
# • Custom callback -> return value becomes workflow output (string here)
# The callback can be sync or async; it receives list[AgentExecutorResponse].
workflow = (
ConcurrentBuilder().participants([researcher, marketer, legal]).with_aggregator(summarize_results).build()
ConcurrentBuilder(participants=[researcher, marketer, legal]).with_aggregator(summarize_results).build()
)
events = await workflow.run("We are launching a new budget-friendly electric bike for urban commuters.")
@@ -33,7 +33,7 @@ instances created by the same builder. This is particularly useful when you need
requests or tasks in parallel with stateful participants.
Demonstrates:
- ConcurrentBuilder().register_participants([...]).with_aggregator(callback)
- ConcurrentBuilder(participant_factories=[...]).with_aggregator(callback)
- Fan-out to agents and fan-in at an aggregator
- Aggregation implemented via an LLM call (chat_client.get_response)
- Workflow output yielded with the synthesized summary string
@@ -125,8 +125,7 @@ async def main() -> None:
# SupportsAgentRun (agents) or Executor instances.
# - register_aggregator(...) takes a factory function that returns an Executor instance.
concurrent_builder = (
ConcurrentBuilder()
.register_participants([create_researcher, create_marketer, create_legal])
ConcurrentBuilder(participant_factories=[create_researcher, create_marketer, create_legal])
.register_aggregator(SummarizationExecutor)
)
@@ -65,16 +65,20 @@ async def main() -> None:
)
# Build the group chat workflow
# termination_condition: stop after 4 assistant messages
# (The agent orchestrator will intelligently decide when to end before this limit but just in case)
# intermediate_outputs=True: Enable intermediate outputs to observe the conversation as it unfolds
# (Intermediate outputs will be emitted as WorkflowOutputEvent events)
workflow = (
GroupChatBuilder()
.with_orchestrator(agent=orchestrator_agent)
.participants([researcher, writer])
GroupChatBuilder(
participants=[researcher, writer],
termination_condition=lambda messages: sum(1 for msg in messages if msg.role == "assistant") >= 4,
intermediate_outputs=True,
orchestrator_agent=orchestrator_agent,
)
# Set a hard termination condition: stop after 4 assistant messages
# The agent orchestrator will intelligently decide when to end before this limit but just in case
.with_termination_condition(lambda messages: sum(1 for msg in messages if msg.role == "assistant") >= 4)
# Enable intermediate outputs to observe the conversation as it unfolds
# Intermediate outputs will be emitted as WorkflowEvent with type "output" events
.with_intermediate_outputs()
.build()
)
@@ -207,14 +207,17 @@ Share your perspective authentically. Feel free to:
chat_client=_get_chat_client(),
)
# termination_condition: stop after 10 assistant messages
# intermediate_outputs=True: Enable intermediate outputs to observe the conversation as it unfolds
# (Intermediate outputs will be emitted as WorkflowOutputEvent events)
workflow = (
GroupChatBuilder()
.with_orchestrator(agent=moderator)
.participants([farmer, developer, teacher, activist, spiritual_leader, artist, immigrant, doctor])
GroupChatBuilder(
participants=[farmer, developer, teacher, activist, spiritual_leader, artist, immigrant, doctor],
termination_condition=lambda messages: sum(1 for msg in messages if msg.role == "assistant") >= 10,
intermediate_outputs=True,
orchestrator_agent=moderator,
)
.with_termination_condition(lambda messages: sum(1 for msg in messages if msg.role == "assistant") >= 10)
# Enable intermediate outputs to observe the conversation as it unfolds
# Intermediate outputs will be emitted as WorkflowEvent with type "output" events
.with_intermediate_outputs()
.build()
)
@@ -16,7 +16,7 @@ from azure.identity import AzureCliCredential
Sample: Group Chat with a round-robin speaker selector
What it does:
- Demonstrates the with_orchestrator() API for GroupChat orchestration
- Demonstrates the selection_func parameter for GroupChat orchestration
- Uses a pure Python function to control speaker selection based on conversation state
Prerequisites:
@@ -80,19 +80,26 @@ async def main() -> None:
)
# Build the group chat workflow
# termination_condition: stop after 6 messages (user task + one full rounds + 1)
# One round is expert -> verifier -> clarifier -> skeptic, after which the expert gets to respond again.
# This will end the conversation after the expert has spoken 2 times (one iteration loop)
# Note: it's possible that the expert gets it right the first time and the other participants
# have nothing to add, but for demo purposes we want to see at least one full round of interaction.
# intermediate_outputs=True: Enable intermediate outputs to observe the conversation as it unfolds
# (Intermediate outputs will be emitted as WorkflowOutputEvent events)
workflow = (
GroupChatBuilder()
.participants([expert, verifier, clarifier, skeptic])
.with_orchestrator(selection_func=round_robin_selector)
GroupChatBuilder(
participants=[expert, verifier, clarifier, skeptic],
termination_condition=lambda conversation: len(conversation) >= 6,
intermediate_outputs=True,
selection_func=round_robin_selector,
)
# Set a hard termination condition: stop after 6 messages (user task + one full rounds + 1)
# One round is expert -> verifier -> clarifier -> skeptic, after which the expert gets to respond again.
# This will end the conversation after the expert has spoken 2 times (one iteration loop)
# Note: it's possible that the expert gets it right the first time and the other participants
# have nothing to add, but for demo purposes we want to see at least one full round of interaction.
.with_termination_condition(lambda conversation: len(conversation) >= 6)
# Enable intermediate outputs to observe the conversation as it unfolds
# Intermediate outputs will be emitted as WorkflowEvent with type "output" events
.with_intermediate_outputs()
.build()
)
@@ -78,10 +78,15 @@ async def main() -> None:
# Build the workflow with autonomous mode
# In autonomous mode, agents continue iterating until they invoke a handoff tool
# termination_condition: Terminate after coordinator provides 5 assistant responses
workflow = (
HandoffBuilder(
name="autonomous_iteration_handoff",
participants=[coordinator, research_agent, summary_agent],
termination_condition=lambda conv: sum(
1 for msg in conv if msg.author_name == "coordinator" and msg.role == "assistant"
)
>= 5,
)
.with_start_agent(coordinator)
.add_handoff(coordinator, [research_agent, summary_agent])
@@ -98,10 +103,6 @@ async def main() -> None:
resolve_agent_id(summary_agent): 5,
}
)
.with_termination_condition(
# Terminate after coordinator provides 5 assistant responses
lambda conv: sum(1 for msg in conv if msg.author_name == "coordinator" and msg.role == "assistant") >= 5
)
.build()
)
@@ -217,6 +217,9 @@ async def _run_workflow(workflow: Workflow, user_inputs: list[str]) -> None:
async def main() -> None:
"""Run the autonomous handoff workflow with participant factories."""
# Build the handoff workflow using participant factories
# termination_condition: Custom termination that checks if the triage agent has provided a closing message.
# This looks for the last message being from triage_agent and containing "welcome",
# which indicates the conversation has concluded naturally.
workflow_builder = (
HandoffBuilder(
name="Autonomous Handoff with Participant Factories",
@@ -226,18 +229,13 @@ async def main() -> None:
"order_status": create_order_status_agent,
"return": create_return_agent,
},
)
.with_start_agent("triage")
.with_termination_condition(
# Custom termination: Check if the triage agent has provided a closing message.
# This looks for the last message being from triage_agent and containing "welcome",
# which indicates the conversation has concluded naturally.
lambda conversation: (
termination_condition=lambda conversation: (
len(conversation) > 0
and conversation[-1].author_name == "triage_agent"
and "welcome" in conversation[-1].text.lower()
)
),
)
.with_start_agent("triage")
)
# Scripted user responses for reproducible demo
@@ -198,7 +198,7 @@ async def main() -> None:
# - participants: All agents that can participate in the workflow
# - with_start_agent: The triage agent is designated as the start agent, which means
# it receives all user input first and orchestrates handoffs to specialists
# - with_termination_condition: Custom logic to stop the request/response loop.
# - termination_condition: Custom logic to stop the request/response loop.
# Without this, the default behavior continues requesting user input until max_turns
# is reached. Here we use a custom condition that checks if the conversation has ended
# naturally (when one of the agents says something like "you're welcome").
@@ -206,14 +206,14 @@ async def main() -> None:
HandoffBuilder(
name="customer_support_handoff",
participants=[triage, refund, order, support],
)
.with_start_agent(triage)
.with_termination_condition(
# Custom termination: Check if one of the agents has provided a closing message.
# This looks for the last message containing "welcome", which indicates the
# conversation has concluded naturally.
lambda conversation: len(conversation) > 0 and "welcome" in conversation[-1].text.lower()
termination_condition=lambda conversation: (
len(conversation) > 0 and "welcome" in conversation[-1].text.lower()
),
)
.with_start_agent(triage)
.build()
)
@@ -163,10 +163,11 @@ async def main() -> None:
async with create_agents(credential) as (triage, code_specialist):
workflow = (
HandoffBuilder()
HandoffBuilder(
termination_condition=lambda conv: sum(1 for msg in conv if msg.role == "user") >= 2,
)
.participants([triage, code_specialist])
.with_start_agent(triage)
.with_termination_condition(lambda conv: sum(1 for msg in conv if msg.role == "user") >= 2)
.build()
)
@@ -72,20 +72,16 @@ async def main() -> None:
print("\nBuilding Magentic Workflow...")
workflow = (
MagenticBuilder()
.participants([researcher_agent, coder_agent])
.with_manager(
agent=manager_agent,
max_round_count=10,
max_stall_count=3,
max_reset_count=2,
)
# Enable intermediate outputs to observe the conversation as it unfolds
# Intermediate outputs will be emitted as WorkflowEvent events
.with_intermediate_outputs()
.build()
)
# intermediate_outputs=True: Enable intermediate outputs to observe the conversation as it unfolds
# (Intermediate outputs will be emitted as WorkflowOutputEvent events)
workflow = MagenticBuilder(
participants=[researcher_agent, coder_agent],
intermediate_outputs=True,
manager_agent=manager_agent,
max_round_count=10,
max_stall_count=3,
max_reset_count=2,
).build()
task = (
"I am preparing a report on the energy efficiency of different machine learning model architectures. "
@@ -76,18 +76,14 @@ def build_workflow(checkpoint_storage: FileCheckpointStorage):
# The builder wires in the Magentic orchestrator, sets the plan review path, and
# stores the checkpoint backend so the runtime knows where to persist snapshots.
return (
MagenticBuilder()
.participants([researcher, writer])
.with_plan_review()
.with_manager(
agent=manager_agent,
max_round_count=10,
max_stall_count=3,
)
.with_checkpointing(checkpoint_storage)
.build()
)
return MagenticBuilder(
participants=[researcher, writer],
enable_plan_review=True,
checkpoint_storage=checkpoint_storage,
manager_agent=manager_agent,
max_round_count=10,
max_stall_count=3,
).build()
async def main() -> None:
@@ -115,22 +115,18 @@ async def main() -> None:
print("\nBuilding Magentic Workflow with Human Plan Review...")
workflow = (
MagenticBuilder()
.participants([researcher_agent, analyst_agent])
.with_manager(
agent=manager_agent,
max_round_count=10,
max_stall_count=1,
max_reset_count=2,
)
# Request human input for plan review
.with_plan_review()
# Enable intermediate outputs to observe the conversation as it unfolds
# Intermediate outputs will be emitted as WorkflowEvent with type "output"
.with_intermediate_outputs()
.build()
)
# enable_plan_review=True: Request human input for plan review
# intermediate_outputs=True: Enable intermediate outputs to observe the conversation as it unfolds
# (Intermediate outputs will be emitted as WorkflowOutputEvent events)
workflow = MagenticBuilder(
participants=[researcher_agent, analyst_agent],
enable_plan_review=True,
intermediate_outputs=True,
manager_agent=manager_agent,
max_round_count=10,
max_stall_count=1,
max_reset_count=2,
).build()
task = "Research sustainable aviation fuel technology and summarize the findings."
@@ -43,7 +43,7 @@ async def main() -> None:
)
# 2) Build sequential workflow: writer -> reviewer
workflow = SequentialBuilder().participants([writer, reviewer]).build()
workflow = SequentialBuilder(participants=[writer, reviewer]).build()
# 3) Run and collect outputs
outputs: list[list[ChatMessage]] = []
@@ -66,7 +66,7 @@ async def main() -> None:
# 2) Build sequential workflow: content -> summarizer
summarizer = Summarizer(id="summarizer")
workflow = SequentialBuilder().participants([content, summarizer]).build()
workflow = SequentialBuilder(participants=[content, summarizer]).build()
# 3) Run workflow and extract final conversation
events = await workflow.run("Explain the benefits of budget eBikes for commuters.")
@@ -70,7 +70,7 @@ async def run_workflow(workflow: Workflow, query: str) -> None:
async def main() -> None:
# 1) Create a builder with participant factories
builder = SequentialBuilder().register_participants([
builder = SequentialBuilder(participant_factories=[
lambda: Accumulate("accumulator"),
create_agent,
])
@@ -160,10 +160,10 @@ async def main():
upper_case = UpperCase(id="upper_case_executor")
# Build the workflow using a fluent pattern:
# 1) add_edge(from_node, to_node) defines a directed edge upper_case -> reverse_text
# 2) set_start_executor(node) declares the entry point
# 1) start_executor=... in constructor declares the entry point
# 2) add_edge(from_node, to_node) defines a directed edge upper_case -> reverse_text
# 3) build() finalizes and returns an immutable Workflow object
workflow1 = WorkflowBuilder().add_edge(upper_case, reverse_text).set_start_executor(upper_case).build()
workflow1 = WorkflowBuilder(start_executor=upper_case).add_edge(upper_case, reverse_text).build()
# Run the workflow by sending the initial message to the start node.
# The run(...) call returns an event collection; its get_outputs() method
@@ -181,10 +181,9 @@ async def main():
# exclamation_adder uses @handler(input=str, output=str) to
# explicitly declare types instead of relying on introspection.
workflow2 = (
WorkflowBuilder()
WorkflowBuilder(start_executor=upper_case)
.add_edge(upper_case, exclamation_adder)
.add_edge(exclamation_adder, reverse_text)
.set_start_executor(upper_case)
.build()
)
@@ -45,8 +45,8 @@ async def main():
)
# Build the workflow using the fluent builder.
# Set the start node and connect an edge from writer to reviewer.
workflow = WorkflowBuilder().set_start_executor(writer_agent).add_edge(writer_agent, reviewer_agent).build()
# Set the start node via constructor and connect an edge from writer to reviewer.
workflow = WorkflowBuilder(start_executor=writer_agent).add_edge(writer_agent, reviewer_agent).build()
# Run the workflow with the user's initial message.
# For foundational clarity, use run (non streaming) and print the terminal event.
@@ -44,8 +44,8 @@ async def main():
)
# Build the workflow using the fluent builder.
# Set the start node and connect an edge from writer to reviewer.
workflow = WorkflowBuilder().set_start_executor(writer_agent).add_edge(writer_agent, reviewer_agent).build()
# Set the start node via constructor and connect an edge from writer to reviewer.
workflow = WorkflowBuilder(start_executor=writer_agent).add_edge(writer_agent, reviewer_agent).build()
# Track the last author to format streaming output.
last_author: str | None = None
@@ -73,12 +73,11 @@ async def main():
# 4) set_start_executor(node) declares the entry point
# 5) build() finalizes and returns an immutable Workflow object
workflow = (
WorkflowBuilder()
WorkflowBuilder(start_executor="UpperCase")
.register_executor(lambda: UpperCase(id="upper_case_executor"), name="UpperCase")
.register_executor(lambda: reverse_text, name="ReverseText")
.register_agent(create_agent, name="DecoderAgent")
.add_chain(["UpperCase", "ReverseText", "DecoderAgent"])
.set_start_executor("UpperCase")
.build()
)
@@ -38,8 +38,8 @@ async def main() -> None:
)
# Build the workflow by adding agents directly as edges.
# Agents adapt to workflow mode: run(stream=True) for complete responses, run() for incremental updates.
workflow = WorkflowBuilder().set_start_executor(writer_agent).add_edge(writer_agent, reviewer_agent).build()
# Agents adapt to workflow mode: run(stream=True) for incremental updates, run() for complete responses.
workflow = WorkflowBuilder(start_executor=writer_agent).add_edge(writer_agent, reviewer_agent).build()
# Track the last author to format streaming output.
last_author: str | None = None
@@ -71,7 +71,7 @@ async def main() -> None:
shared_thread.message_store = ChatMessageStore()
workflow = (
WorkflowBuilder()
WorkflowBuilder(start_executor="writer")
.register_agent(factory_func=lambda: writer, name="writer", agent_thread=shared_thread)
.register_agent(factory_func=lambda: reviewer, name="reviewer", agent_thread=shared_thread)
.register_executor(
@@ -79,7 +79,6 @@ async def main() -> None:
name="intercept_agent_response",
)
.add_chain(["writer", "intercept_agent_response", "reviewer"])
.set_start_executor("writer")
.build()
)
@@ -110,8 +110,7 @@ async def main() -> None:
)
workflow = (
WorkflowBuilder()
.set_start_executor(research_agent)
WorkflowBuilder(start_executor=research_agent)
.add_edge(research_agent, enrich_with_references)
.add_edge(enrich_with_references, final_editor_agent)
.build()
@@ -40,7 +40,7 @@ async def main():
# Build the workflow using the fluent builder.
# Set the start node and connect an edge from writer to reviewer.
# Agents adapt to workflow mode: run(stream=True) for incremental updates, run() for complete responses.
workflow = WorkflowBuilder().set_start_executor(writer_agent).add_edge(writer_agent, reviewer_agent).build()
workflow = WorkflowBuilder(start_executor=writer_agent).add_edge(writer_agent, reviewer_agent).build()
# Track the last author to format streaming output.
last_author: str | None = None
@@ -240,7 +240,7 @@ async def main() -> None:
# Build the workflow.
workflow = (
WorkflowBuilder()
WorkflowBuilder(start_executor="writer_agent")
.register_agent(create_writer_agent, name="writer_agent")
.register_agent(create_final_editor_agent, name="final_editor_agent")
.register_executor(
@@ -251,7 +251,6 @@ async def main() -> None:
),
name="coordinator",
)
.set_start_executor("writer_agent")
.add_edge("writer_agent", "coordinator")
.add_edge("coordinator", "writer_agent")
.add_edge("final_editor_agent", "coordinator")
@@ -65,7 +65,7 @@ async def main() -> None:
)
# 2) Build a concurrent workflow
workflow = ConcurrentBuilder().participants([researcher, marketer, legal]).build()
workflow = ConcurrentBuilder(participants=[researcher, marketer, legal]).build()
# 3) Expose the concurrent workflow as an agent for easy reuse
agent = workflow.as_agent(name="ConcurrentWorkflowAgent")
@@ -113,7 +113,7 @@ async def main():
# Build the workflow using the fluent builder.
# Set the start node and connect an edge from writer to reviewer.
workflow = WorkflowBuilder().set_start_executor(writer).add_edge(writer, reviewer).build()
workflow = WorkflowBuilder(start_executor=writer).add_edge(writer, reviewer).build()
# Run the workflow with the user's initial message.
# For foundational clarity, use run (non streaming) and print the workflow output.
@@ -33,20 +33,16 @@ async def main() -> None:
chat_client=OpenAIResponsesClient(),
)
workflow = (
GroupChatBuilder()
.with_orchestrator(
agent=OpenAIChatClient().as_agent(
name="Orchestrator",
instructions="You coordinate a team conversation to solve the user's task.",
)
)
.participants([researcher, writer])
# Enable intermediate outputs to observe the conversation as it unfolds
# Intermediate outputs will be emitted as WorkflowEvent with type "output" events
.with_intermediate_outputs()
.build()
)
# intermediate_outputs=True: Enable intermediate outputs to observe the conversation as it unfolds
# (Intermediate outputs will be emitted as WorkflowOutputEvent events)
workflow = GroupChatBuilder(
participants=[researcher, writer],
intermediate_outputs=True,
orchestrator_agent=OpenAIChatClient().as_agent(
name="Orchestrator",
instructions="You coordinate a team conversation to solve the user's task.",
),
).build()
task = "Outline the core considerations for planning a community hackathon, and finish with a concise action plan."
@@ -156,7 +156,7 @@ async def main() -> None:
# - participants: All agents that can participate in the workflow
# - with_start_agent: The triage agent is designated as the start agent, which means
# it receives all user input first and orchestrates handoffs to specialists
# - with_termination_condition: Custom logic to stop the request/response loop.
# - termination_condition: Custom logic to stop the request/response loop.
# Without this, the default behavior continues requesting user input until max_turns
# is reached. Here we use a custom condition that checks if the conversation has ended
# naturally (when one of the agents says something like "you're welcome").
@@ -164,14 +164,14 @@ async def main() -> None:
HandoffBuilder(
name="customer_support_handoff",
participants=[triage, refund, order, support],
)
.with_start_agent(triage)
.with_termination_condition(
# Custom termination: Check if one of the agents has provided a closing message.
# This looks for the last message containing "welcome", which indicates the
# conversation has concluded naturally.
lambda conversation: len(conversation) > 0 and "welcome" in conversation[-1].text.lower()
termination_condition=lambda conversation: (
len(conversation) > 0 and "welcome" in conversation[-1].text.lower()
),
)
.with_start_agent(triage)
.build()
.as_agent() # Convert workflow to agent interface
)
@@ -50,20 +50,16 @@ async def main() -> None:
print("\nBuilding Magentic Workflow...")
workflow = (
MagenticBuilder()
.participants([researcher_agent, coder_agent])
.with_manager(
agent=manager_agent,
max_round_count=10,
max_stall_count=3,
max_reset_count=2,
)
# Enable intermediate outputs to observe the conversation as it unfolds
# Intermediate outputs will be emitted as WorkflowEvent with type "output" events
.with_intermediate_outputs()
.build()
)
# intermediate_outputs=True: Enable intermediate outputs to observe the conversation as it unfolds
# (Intermediate outputs will be emitted as WorkflowOutputEvent events)
workflow = MagenticBuilder(
participants=[researcher_agent, coder_agent],
intermediate_outputs=True,
manager_agent=manager_agent,
max_round_count=10,
max_stall_count=3,
max_reset_count=2,
).build()
task = (
"I am preparing a report on the energy efficiency of different machine learning model architectures. "
@@ -40,7 +40,7 @@ async def main() -> None:
)
# 2) Build sequential workflow: writer -> reviewer
workflow = SequentialBuilder().participants([writer, reviewer]).build()
workflow = SequentialBuilder(participants=[writer, reviewer]).build()
# 3) Treat the workflow itself as an agent for follow-up invocations
agent = workflow.as_agent(name="SequentialWorkflowAgent")
@@ -99,7 +99,7 @@ async def main() -> None:
# Build a workflow with bidirectional communication between Worker and Reviewer,
# and escalation paths for human review.
agent = (
WorkflowBuilder()
WorkflowBuilder(start_executor="worker")
.register_executor(
lambda: Worker(
id="sub-worker",
@@ -113,7 +113,6 @@ async def main() -> None:
)
.add_edge("worker", "reviewer") # Worker sends requests to Reviewer
.add_edge("reviewer", "worker") # Reviewer sends feedback to Worker
.set_start_executor("worker")
.build()
.as_agent() # Convert workflow into an agent interface
)
@@ -94,7 +94,7 @@ async def main() -> None:
)
# Build a sequential workflow
workflow = SequentialBuilder().participants([agent]).build()
workflow = SequentialBuilder(participants=[agent]).build()
# Expose the workflow as an agent using .as_agent()
workflow_agent = workflow.as_agent(name="WorkflowAgent")
@@ -187,7 +187,7 @@ async def main() -> None:
print("Building workflow with Worker ↔ Reviewer cycle...")
agent = (
WorkflowBuilder()
WorkflowBuilder(start_executor="worker")
.register_executor(
lambda: Worker(id="worker", chat_client=OpenAIChatClient(model_id="gpt-4.1-nano")),
name="worker",
@@ -198,7 +198,6 @@ async def main() -> None:
)
.add_edge("worker", "reviewer") # Worker sends responses to Reviewer
.add_edge("reviewer", "worker") # Reviewer provides feedback to Worker
.set_start_executor("worker")
.build()
.as_agent() # Wrap workflow as an agent
)
@@ -59,7 +59,7 @@ async def main() -> None:
)
# Build a sequential workflow: assistant -> summarizer
workflow = SequentialBuilder().register_participants([create_assistant, create_summarizer]).build()
workflow = SequentialBuilder(participant_factories=[create_assistant, create_summarizer]).build()
# Wrap the workflow as an agent
agent = workflow.as_agent(name="ConversationalWorkflowAgent")
@@ -130,7 +130,7 @@ async def demonstrate_thread_serialization() -> None:
instructions="You are a helpful assistant with good memory. Remember details from our conversation.",
)
workflow = SequentialBuilder().register_participants([create_assistant]).build()
workflow = SequentialBuilder(participant_factories=[create_assistant]).build()
agent = workflow.as_agent(name="MemoryWorkflowAgent")
# Create initial thread and have a conversation
@@ -179,7 +179,9 @@ def create_workflow(checkpoint_storage: FileCheckpointStorage) -> Workflow:
# module docstring. Because `WorkflowBuilder` is declarative, reading these
# edges is often the quickest way to understand execution order.
workflow_builder = (
WorkflowBuilder(max_iterations=6)
WorkflowBuilder(
max_iterations=6, start_executor="prepare_brief", checkpoint_storage=checkpoint_storage
)
.register_agent(
lambda: AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
instructions="Write concise, warm release notes that sound human and helpful.",
@@ -190,11 +192,9 @@ def create_workflow(checkpoint_storage: FileCheckpointStorage) -> Workflow:
)
.register_executor(lambda: ReviewGateway(id="review_gateway", writer_id="writer"), name="review_gateway")
.register_executor(lambda: BriefPreparer(id="prepare_brief", agent_id="writer"), name="prepare_brief")
.set_start_executor("prepare_brief")
.add_edge("prepare_brief", "writer")
.add_edge("writer", "review_gateway")
.add_edge("review_gateway", "writer") # revisions loop
.with_checkpointing(checkpoint_storage=checkpoint_storage)
)
return workflow_builder.build()
@@ -104,16 +104,14 @@ class WorkerExecutor(Executor):
async def main():
# Build workflow with checkpointing enabled
checkpoint_storage = InMemoryCheckpointStorage()
workflow_builder = (
WorkflowBuilder()
WorkflowBuilder(start_executor="start", checkpoint_storage=checkpoint_storage)
.register_executor(lambda: StartExecutor(id="start"), name="start")
.register_executor(lambda: WorkerExecutor(id="worker"), name="worker")
.set_start_executor("start")
.add_edge("start", "worker")
.add_edge("worker", "worker") # Self-loop for iterative processing
)
checkpoint_storage = InMemoryCheckpointStorage()
workflow_builder = workflow_builder.with_checkpointing(checkpoint_storage=checkpoint_storage)
# Run workflow with automatic checkpoint recovery
latest_checkpoint: WorkflowCheckpoint | None = None
@@ -97,17 +97,16 @@ def create_workflow(checkpoint_storage: FileCheckpointStorage) -> tuple[Workflow
client = AzureOpenAIChatClient(credential=AzureCliCredential())
triage, refund, order = create_agents(client)
# checkpoint_storage: Enable checkpointing for resume
# termination_condition: Terminate after 5 user messages for this demo
workflow = (
HandoffBuilder(
name="checkpoint_handoff_demo",
participants=[triage, refund, order],
checkpoint_storage=checkpoint_storage,
termination_condition=lambda conv: sum(1 for msg in conv if msg.role == "user") >= 5,
)
.with_start_agent(triage)
.with_checkpointing(checkpoint_storage)
.with_termination_condition(
# Terminate after 5 user messages for this demo
lambda conv: sum(1 for msg in conv if msg.role == "user") >= 5
)
.build()
)
@@ -298,11 +298,10 @@ class LaunchCoordinator(Executor):
def build_sub_workflow() -> WorkflowExecutor:
"""Assemble the sub-workflow used by the parent workflow executor."""
sub_workflow = (
WorkflowBuilder()
WorkflowBuilder(start_executor="writer")
.register_executor(DraftWriter, name="writer")
.register_executor(DraftReviewRouter, name="router")
.register_executor(DraftFinaliser, name="finaliser")
.set_start_executor("writer")
.add_edge("writer", "router")
.add_edge("router", "finaliser")
.add_edge("finaliser", "writer") # permits revision loops
@@ -315,13 +314,11 @@ def build_sub_workflow() -> WorkflowExecutor:
def build_parent_workflow(storage: FileCheckpointStorage) -> Workflow:
"""Assemble the parent workflow that embeds the sub-workflow."""
return (
WorkflowBuilder()
WorkflowBuilder(start_executor="coordinator", checkpoint_storage=storage)
.register_executor(LaunchCoordinator, name="coordinator")
.register_executor(build_sub_workflow, name="sub_executor")
.set_start_executor("coordinator")
.add_edge("coordinator", "sub_executor")
.add_edge("sub_executor", "coordinator")
.with_checkpointing(storage)
.build()
)
@@ -56,7 +56,7 @@ async def basic_checkpointing() -> None:
)
# Build sequential workflow with participant factories
workflow = SequentialBuilder().register_participants([create_assistant, create_reviewer]).build()
workflow = SequentialBuilder(participant_factories=[create_assistant, create_reviewer]).build()
agent = workflow.as_agent(name="CheckpointedAgent")
# Create checkpoint storage
@@ -93,7 +93,7 @@ async def checkpointing_with_thread() -> None:
instructions="You are a helpful assistant with good memory. Reference previous conversation when relevant.",
)
workflow = SequentialBuilder().register_participants([create_assistant]).build()
workflow = SequentialBuilder(participant_factories=[create_assistant]).build()
agent = workflow.as_agent(name="MemoryAgent")
# Create both thread (for conversation) and checkpoint storage (for workflow state)
@@ -137,7 +137,7 @@ async def streaming_with_checkpoints() -> None:
instructions="You are a helpful assistant.",
)
workflow = SequentialBuilder().register_participants([create_assistant]).build()
workflow = SequentialBuilder(participant_factories=[create_assistant]).build()
agent = workflow.as_agent(name="StreamingCheckpointAgent")
checkpoint_storage = InMemoryCheckpointStorage()
@@ -141,9 +141,8 @@ def create_sub_workflow() -> WorkflowExecutor:
print("🚀 Setting up sub-workflow...")
processing_workflow = (
WorkflowBuilder()
WorkflowBuilder(start_executor="text_processor")
.register_executor(TextProcessor, name="text_processor")
.set_start_executor("text_processor")
.build()
)
@@ -155,10 +154,9 @@ async def main():
print("🔧 Setting up parent workflow...")
# Step 1: Create the parent workflow
main_workflow = (
WorkflowBuilder()
WorkflowBuilder(start_executor="text_orchestrator")
.register_executor(TextProcessingOrchestrator, name="text_orchestrator")
.register_executor(create_sub_workflow, name="text_processor_workflow")
.set_start_executor("text_orchestrator")
.add_edge("text_orchestrator", "text_processor_workflow")
.add_edge("text_processor_workflow", "text_orchestrator")
.build()
@@ -88,7 +88,7 @@ async def main() -> None:
)
# Build the inner (sub) workflow with the agent
inner_workflow = SequentialBuilder().participants([inner_agent]).build()
inner_workflow = SequentialBuilder(participants=[inner_agent]).build()
# Wrap the inner workflow in a WorkflowExecutor to use it as a sub-workflow
subworkflow_executor = WorkflowExecutor(
@@ -97,7 +97,7 @@ async def main() -> None:
)
# Build the outer (parent) workflow containing the sub-workflow
outer_workflow = SequentialBuilder().participants([subworkflow_executor]).build()
outer_workflow = SequentialBuilder(participants=[subworkflow_executor]).build()
# Define custom context that will flow through to the sub-workflow's agent
user_token = {

Some files were not shown because too many files have changed in this diff Show More