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

* Move single-config fluent methods to constructor parameters

* Updates

* Adjust magentic and group chat
This commit is contained in:
Evan Mattson
2026-02-07 15:01:52 +09:00
committed by GitHub
Unverified
parent 5d355ac507
commit 74ac470a56
132 changed files with 1341 additions and 2386 deletions
@@ -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"