[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
@@ -39,14 +39,14 @@ class _FakeAgentExec(Executor):
def test_concurrent_builder_rejects_empty_participants() -> None:
with pytest.raises(ValueError):
ConcurrentBuilder().participants([])
ConcurrentBuilder(participants=[])
def test_concurrent_builder_rejects_duplicate_executors() -> None:
a = _FakeAgentExec("dup", "A")
b = _FakeAgentExec("dup", "B") # same executor id
with pytest.raises(ValueError):
ConcurrentBuilder().participants([a, b])
ConcurrentBuilder(participants=[a, b])
def test_concurrent_builder_rejects_duplicate_executors_from_factories() -> None:
@@ -58,43 +58,35 @@ def test_concurrent_builder_rejects_duplicate_executors_from_factories() -> None
def create_dup2() -> Executor:
return _FakeAgentExec("dup", "B") # same executor id
builder = ConcurrentBuilder().register_participants([create_dup1, create_dup2])
builder = ConcurrentBuilder(participant_factories=[create_dup1, create_dup2])
with pytest.raises(ValueError, match="Duplicate executor ID 'dup' detected in workflow."):
builder.build()
def test_concurrent_builder_rejects_mixed_participants_and_factories() -> None:
"""Test that mixing .participants() and .register_participants() raises an error."""
# Case 1: participants first, then register_participants
with pytest.raises(ValueError, match="Cannot mix .participants"):
(
ConcurrentBuilder()
.participants([_FakeAgentExec("a", "A")])
.register_participants([lambda: _FakeAgentExec("b", "B")])
)
# Case 2: register_participants first, then participants
with pytest.raises(ValueError, match="Cannot mix .participants"):
(
ConcurrentBuilder()
.register_participants([lambda: _FakeAgentExec("a", "A")])
.participants([_FakeAgentExec("b", "B")])
"""Test that passing both participants and participant_factories to the constructor raises an error."""
with pytest.raises(ValueError, match="Cannot provide both participants and participant_factories"):
ConcurrentBuilder(
participants=[_FakeAgentExec("a", "A")],
participant_factories=[lambda: _FakeAgentExec("b", "B")],
)
def test_concurrent_builder_rejects_multiple_calls_to_participants() -> None:
"""Test that multiple calls to .participants() raises an error."""
with pytest.raises(ValueError, match=r"participants\(\) has already been called"):
(ConcurrentBuilder().participants([_FakeAgentExec("a", "A")]).participants([_FakeAgentExec("b", "B")]))
def test_concurrent_builder_rejects_both_participants_and_factories() -> None:
"""Test that passing both participants and participant_factories raises an error."""
with pytest.raises(ValueError, match="Cannot provide both participants and participant_factories"):
ConcurrentBuilder(
participants=[_FakeAgentExec("a", "A")],
participant_factories=[lambda: _FakeAgentExec("b", "B")],
)
def test_concurrent_builder_rejects_multiple_calls_to_register_participants() -> None:
"""Test that multiple calls to .register_participants() raises an error."""
with pytest.raises(ValueError, match=r"register_participants\(\) has already been called"):
(
ConcurrentBuilder()
.register_participants([lambda: _FakeAgentExec("a", "A")])
.register_participants([lambda: _FakeAgentExec("b", "B")])
def test_concurrent_builder_rejects_both_factories_and_participants() -> None:
"""Test that passing both participant_factories and participants raises an error."""
with pytest.raises(ValueError, match="Cannot provide both participants and participant_factories"):
ConcurrentBuilder(
participant_factories=[lambda: _FakeAgentExec("a", "A")],
participants=[_FakeAgentExec("b", "B")],
)
@@ -104,7 +96,7 @@ async def test_concurrent_default_aggregator_emits_single_user_and_assistants()
e2 = _FakeAgentExec("agentB", "Beta")
e3 = _FakeAgentExec("agentC", "Gamma")
wf = ConcurrentBuilder().participants([e1, e2, e3]).build()
wf = ConcurrentBuilder(participants=[e1, e2, e3]).build()
completed = False
output: list[ChatMessage] | None = None
@@ -142,7 +134,7 @@ async def test_concurrent_custom_aggregator_callback_is_used() -> None:
texts.append(msgs[-1].text if msgs else "")
return " | ".join(sorted(texts))
wf = ConcurrentBuilder().participants([e1, e2]).with_aggregator(summarize).build()
wf = ConcurrentBuilder(participants=[e1, e2]).with_aggregator(summarize).build()
completed = False
output: str | None = None
@@ -173,7 +165,7 @@ async def test_concurrent_custom_aggregator_sync_callback_is_used() -> None:
texts.append(msgs[-1].text if msgs else "")
return " | ".join(sorted(texts))
wf = ConcurrentBuilder().participants([e1, e2]).with_aggregator(summarize_sync).build()
wf = ConcurrentBuilder(participants=[e1, e2]).with_aggregator(summarize_sync).build()
completed = False
output: str | None = None
@@ -198,7 +190,7 @@ def test_concurrent_custom_aggregator_uses_callback_name_for_id() -> None:
def summarize(results: list[AgentExecutorResponse]) -> str: # type: ignore[override]
return str(len(results))
wf = ConcurrentBuilder().participants([e1, e2]).with_aggregator(summarize).build()
wf = ConcurrentBuilder(participants=[e1, e2]).with_aggregator(summarize).build()
assert "summarize" in wf.executors
aggregator = wf.executors["summarize"]
@@ -221,7 +213,7 @@ async def test_concurrent_with_aggregator_executor_instance() -> None:
e2 = _FakeAgentExec("agentB", "Two")
aggregator_instance = CustomAggregator(id="instance_aggregator")
wf = ConcurrentBuilder().participants([e1, e2]).with_aggregator(aggregator_instance).build()
wf = ConcurrentBuilder(participants=[e1, e2]).with_aggregator(aggregator_instance).build()
completed = False
output: str | None = None
@@ -255,8 +247,7 @@ async def test_concurrent_with_aggregator_executor_factory() -> None:
e2 = _FakeAgentExec("agentB", "Two")
wf = (
ConcurrentBuilder()
.participants([e1, e2])
ConcurrentBuilder(participants=[e1, e2])
.register_aggregator(lambda: CustomAggregator(id="custom_aggregator"))
.build()
)
@@ -295,7 +286,7 @@ async def test_concurrent_with_aggregator_executor_factory_with_default_id() ->
e1 = _FakeAgentExec("agentA", "One")
e2 = _FakeAgentExec("agentB", "Two")
wf = ConcurrentBuilder().participants([e1, e2]).register_aggregator(CustomAggregator).build()
wf = ConcurrentBuilder(participants=[e1, e2]).register_aggregator(CustomAggregator).build()
completed = False
output: str | None = None
@@ -320,7 +311,11 @@ def test_concurrent_builder_rejects_multiple_calls_to_with_aggregator() -> None:
return str(len(results))
with pytest.raises(ValueError, match=r"with_aggregator\(\) has already been called"):
(ConcurrentBuilder().with_aggregator(summarize).with_aggregator(summarize))
(
ConcurrentBuilder(participants=[_FakeAgentExec("a", "A")])
.with_aggregator(summarize)
.with_aggregator(summarize)
)
def test_concurrent_builder_rejects_multiple_calls_to_register_aggregator() -> None:
@@ -331,7 +326,7 @@ def test_concurrent_builder_rejects_multiple_calls_to_register_aggregator() -> N
with pytest.raises(ValueError, match=r"register_aggregator\(\) has already been called"):
(
ConcurrentBuilder()
ConcurrentBuilder(participants=[_FakeAgentExec("a", "A")])
.register_aggregator(lambda: CustomAggregator(id="agg1"))
.register_aggregator(lambda: CustomAggregator(id="agg2"))
)
@@ -346,7 +341,7 @@ async def test_concurrent_checkpoint_resume_round_trip() -> None:
_FakeAgentExec("agentC", "Gamma"),
)
wf = ConcurrentBuilder().participants(list(participants)).with_checkpointing(storage).build()
wf = ConcurrentBuilder(participants=list(participants), checkpoint_storage=storage).build()
baseline_output: list[ChatMessage] | None = None
async for ev in wf.run("checkpoint concurrent", stream=True):
@@ -370,7 +365,7 @@ async def test_concurrent_checkpoint_resume_round_trip() -> None:
_FakeAgentExec("agentB", "Beta"),
_FakeAgentExec("agentC", "Gamma"),
)
wf_resume = ConcurrentBuilder().participants(list(resumed_participants)).with_checkpointing(storage).build()
wf_resume = ConcurrentBuilder(participants=list(resumed_participants), checkpoint_storage=storage).build()
resumed_output: list[ChatMessage] | None = None
async for ev in wf_resume.run(checkpoint_id=resume_checkpoint.checkpoint_id, stream=True):
@@ -392,7 +387,7 @@ async def test_concurrent_checkpoint_runtime_only() -> None:
storage = InMemoryCheckpointStorage()
agents = [_FakeAgentExec(id="agent1", reply_text="A1"), _FakeAgentExec(id="agent2", reply_text="A2")]
wf = ConcurrentBuilder().participants(agents).build()
wf = ConcurrentBuilder(participants=agents).build()
baseline_output: list[ChatMessage] | None = None
async for ev in wf.run("runtime checkpoint test", checkpoint_storage=storage, stream=True):
@@ -413,7 +408,7 @@ async def test_concurrent_checkpoint_runtime_only() -> None:
)
resumed_agents = [_FakeAgentExec(id="agent1", reply_text="A1"), _FakeAgentExec(id="agent2", reply_text="A2")]
wf_resume = ConcurrentBuilder().participants(resumed_agents).build()
wf_resume = ConcurrentBuilder(participants=resumed_agents).build()
resumed_output: list[ChatMessage] | None = None
async for ev in wf_resume.run(
@@ -442,7 +437,7 @@ async def test_concurrent_checkpoint_runtime_overrides_buildtime() -> None:
runtime_storage = FileCheckpointStorage(temp_dir2)
agents = [_FakeAgentExec(id="agent1", reply_text="A1"), _FakeAgentExec(id="agent2", reply_text="A2")]
wf = ConcurrentBuilder().participants(agents).with_checkpointing(buildtime_storage).build()
wf = ConcurrentBuilder(participants=agents, checkpoint_storage=buildtime_storage).build()
baseline_output: list[ChatMessage] | None = None
async for ev in wf.run("override test", checkpoint_storage=runtime_storage, stream=True):
@@ -462,7 +457,7 @@ async def test_concurrent_checkpoint_runtime_overrides_buildtime() -> None:
def test_concurrent_builder_rejects_empty_participant_factories() -> None:
with pytest.raises(ValueError):
ConcurrentBuilder().register_participants([])
ConcurrentBuilder(participant_factories=[])
async def test_concurrent_builder_reusable_after_build_with_participants() -> None:
@@ -470,7 +465,7 @@ async def test_concurrent_builder_reusable_after_build_with_participants() -> No
e1 = _FakeAgentExec("agentA", "One")
e2 = _FakeAgentExec("agentB", "Two")
builder = ConcurrentBuilder().participants([e1, e2])
builder = ConcurrentBuilder(participants=[e1, e2])
builder.build()
@@ -493,7 +488,7 @@ async def test_concurrent_builder_reusable_after_build_with_factories() -> None:
call_count += 1
return _FakeAgentExec("agentB", "Two")
builder = ConcurrentBuilder().register_participants([create_agent_executor_a, create_agent_executor_b])
builder = ConcurrentBuilder(participant_factories=[create_agent_executor_a, create_agent_executor_b])
# Build the first workflow
wf1 = builder.build()
@@ -523,7 +518,7 @@ async def test_concurrent_with_register_participants() -> None:
def create_agent3() -> Executor:
return _FakeAgentExec("agentC", "Gamma")
wf = ConcurrentBuilder().register_participants([create_agent1, create_agent2, create_agent3]).build()
wf = ConcurrentBuilder(participant_factories=[create_agent1, create_agent2, create_agent3]).build()
completed = False
output: list[ChatMessage] | None = None
@@ -178,13 +178,12 @@ async def test_group_chat_builder_basic_flow() -> None:
alpha = StubAgent("alpha", "ack from alpha")
beta = StubAgent("beta", "ack from beta")
workflow = (
GroupChatBuilder()
.with_orchestrator(selection_func=selector, orchestrator_name="manager")
.participants([alpha, beta])
.with_max_rounds(2) # Limit rounds to prevent infinite loop
.build()
)
workflow = GroupChatBuilder(
participants=[alpha, beta],
max_rounds=2, # Limit rounds to prevent infinite loop
selection_func=selector,
orchestrator_name="manager",
).build()
outputs: list[list[ChatMessage]] = []
async for event in workflow.run("coordinate task", stream=True):
@@ -205,13 +204,12 @@ async def test_group_chat_as_agent_accepts_conversation() -> None:
alpha = StubAgent("alpha", "ack from alpha")
beta = StubAgent("beta", "ack from beta")
workflow = (
GroupChatBuilder()
.with_orchestrator(selection_func=selector, orchestrator_name="manager")
.participants([alpha, beta])
.with_max_rounds(2) # Limit rounds to prevent infinite loop
.build()
)
workflow = GroupChatBuilder(
participants=[alpha, beta],
max_rounds=2, # Limit rounds to prevent infinite loop
selection_func=selector,
orchestrator_name="manager",
).build()
agent = workflow.as_agent(name="group-chat-agent")
conversation = [
@@ -233,64 +231,47 @@ class TestGroupChatBuilder:
"""Test that building without a manager raises ValueError."""
agent = StubAgent("test", "response")
builder = GroupChatBuilder().participants([agent])
builder = GroupChatBuilder(participants=[agent])
with pytest.raises(
ValueError, match=r"No orchestrator has been configured\. Call with_orchestrator\(\) to set one\."
ValueError,
match=r"No orchestrator has been configured\.",
):
builder.build()
def test_build_without_participants_raises_error(self) -> None:
"""Test that building without participants raises ValueError."""
def selector(state: GroupChatState) -> str:
return "agent"
builder = GroupChatBuilder().with_orchestrator(selection_func=selector)
"""Test that constructing without participants raises ValueError."""
with pytest.raises(
ValueError,
match=r"No participants provided\. Call \.participants\(\) or \.register_participants\(\) first\.",
match=r"Either participants or participant_factories must be provided\.",
):
builder.build()
GroupChatBuilder()
def test_duplicate_manager_configuration_raises_error(self) -> None:
"""Test that configuring multiple managers raises ValueError."""
"""Test that configuring multiple orchestrator options raises ValueError."""
agent = StubAgent("test", "response")
def selector(state: GroupChatState) -> str:
return "agent"
builder = GroupChatBuilder().with_orchestrator(selection_func=selector)
with pytest.raises(
ValueError,
match=r"A selection function has already been configured\. Call with_orchestrator\(\.\.\.\) once only\.",
match=r"Exactly one of",
):
builder.with_orchestrator(selection_func=selector)
GroupChatBuilder(participants=[agent], selection_func=selector, orchestrator_agent=StubManagerAgent())
def test_empty_participants_raises_error(self) -> None:
"""Test that empty participants list raises ValueError."""
def selector(state: GroupChatState) -> str:
return "agent"
builder = GroupChatBuilder().with_orchestrator(selection_func=selector)
with pytest.raises(ValueError, match="participants cannot be empty"):
builder.participants([])
GroupChatBuilder(participants=[])
def test_duplicate_participant_names_raises_error(self) -> None:
"""Test that duplicate participant names raise ValueError."""
agent1 = StubAgent("test", "response1")
agent2 = StubAgent("test", "response2")
def selector(state: GroupChatState) -> str:
return "agent"
builder = GroupChatBuilder().with_orchestrator(selection_func=selector)
with pytest.raises(ValueError, match="Duplicate participant name 'test'"):
builder.participants([agent1, agent2])
GroupChatBuilder(participants=[agent1, agent2])
def test_agent_without_name_raises_error(self) -> None:
"""Test that agent without name attribute raises ValueError."""
@@ -315,25 +296,15 @@ class TestGroupChatBuilder:
agent = AgentWithoutName()
def selector(state: GroupChatState) -> str:
return "agent"
builder = GroupChatBuilder().with_orchestrator(selection_func=selector)
with pytest.raises(ValueError, match="SupportsAgentRun participants must have a non-empty name"):
builder.participants([agent])
GroupChatBuilder(participants=[agent])
def test_empty_participant_name_raises_error(self) -> None:
"""Test that empty participant name raises ValueError."""
agent = StubAgent("", "response") # Agent with empty name
def selector(state: GroupChatState) -> str:
return "agent"
builder = GroupChatBuilder().with_orchestrator(selection_func=selector)
with pytest.raises(ValueError, match="SupportsAgentRun participants must have a non-empty name"):
builder.participants([agent])
GroupChatBuilder(participants=[agent])
class TestGroupChatWorkflow:
@@ -350,13 +321,11 @@ class TestGroupChatWorkflow:
agent = StubAgent("agent", "response")
workflow = (
GroupChatBuilder()
.with_orchestrator(selection_func=selector)
.participants([agent])
.with_max_rounds(2) # Limit to 2 rounds
.build()
)
workflow = GroupChatBuilder(
participants=[agent],
max_rounds=2, # Limit to 2 rounds
selection_func=selector,
).build()
outputs: list[list[ChatMessage]] = []
async for event in workflow.run("test task", stream=True):
@@ -385,13 +354,11 @@ class TestGroupChatWorkflow:
agent = StubAgent("agent", "response")
workflow = (
GroupChatBuilder()
.with_orchestrator(selection_func=selector)
.participants([agent])
.with_termination_condition(termination_condition)
.build()
)
workflow = GroupChatBuilder(
participants=[agent],
termination_condition=termination_condition,
selection_func=selector,
).build()
outputs: list[list[ChatMessage]] = []
async for event in workflow.run("test task", stream=True):
@@ -413,13 +380,11 @@ class TestGroupChatWorkflow:
manager = StubManagerAgent()
worker = StubAgent("agent", "response")
workflow = (
GroupChatBuilder()
.with_orchestrator(agent=manager)
.participants([worker])
.with_termination_condition(lambda conv: any(msg.author_name == "agent" for msg in conv))
.build()
)
workflow = GroupChatBuilder(
participants=[worker],
termination_condition=lambda conv: any(msg.author_name == "agent" for msg in conv),
orchestrator_agent=manager,
).build()
outputs: list[list[ChatMessage]] = []
async for event in workflow.run("test task", stream=True):
@@ -441,7 +406,7 @@ class TestGroupChatWorkflow:
agent = StubAgent("agent", "response")
workflow = GroupChatBuilder().with_orchestrator(selection_func=selector).participants([agent]).build()
workflow = GroupChatBuilder(participants=[agent], selection_func=selector).build()
with pytest.raises(RuntimeError, match="Selection function returned unknown participant 'unknown_agent'"):
async for _ in workflow.run("test task", stream=True):
@@ -460,14 +425,12 @@ class TestCheckpointing:
agent = StubAgent("agent", "response")
storage = InMemoryCheckpointStorage()
workflow = (
GroupChatBuilder()
.with_orchestrator(selection_func=selector)
.participants([agent])
.with_max_rounds(1)
.with_checkpointing(storage)
.build()
)
workflow = GroupChatBuilder(
participants=[agent],
max_rounds=1,
checkpoint_storage=storage,
selection_func=selector,
).build()
outputs: list[list[ChatMessage]] = []
async for event in workflow.run("test task", stream=True):
@@ -490,13 +453,7 @@ class TestConversationHandling:
agent = StubAgent("agent", "response")
workflow = (
GroupChatBuilder()
.with_orchestrator(selection_func=selector)
.participants([agent])
.with_max_rounds(1)
.build()
)
workflow = GroupChatBuilder(participants=[agent], max_rounds=1, selection_func=selector).build()
with pytest.raises(ValueError, match="At least one ChatMessage is required to start the group chat workflow."):
async for _ in workflow.run([], stream=True):
@@ -514,13 +471,7 @@ class TestConversationHandling:
agent = StubAgent("agent", "response")
workflow = (
GroupChatBuilder()
.with_orchestrator(selection_func=selector)
.participants([agent])
.with_max_rounds(1)
.build()
)
workflow = GroupChatBuilder(participants=[agent], max_rounds=1, selection_func=selector).build()
outputs: list[list[ChatMessage]] = []
async for event in workflow.run("test string", stream=True):
@@ -543,13 +494,7 @@ class TestConversationHandling:
agent = StubAgent("agent", "response")
workflow = (
GroupChatBuilder()
.with_orchestrator(selection_func=selector)
.participants([agent])
.with_max_rounds(1)
.build()
)
workflow = GroupChatBuilder(participants=[agent], max_rounds=1, selection_func=selector).build()
outputs: list[list[ChatMessage]] = []
async for event in workflow.run(task_message, stream=True):
@@ -575,13 +520,7 @@ class TestConversationHandling:
agent = StubAgent("agent", "response")
workflow = (
GroupChatBuilder()
.with_orchestrator(selection_func=selector)
.participants([agent])
.with_max_rounds(1)
.build()
)
workflow = GroupChatBuilder(participants=[agent], max_rounds=1, selection_func=selector).build()
outputs: list[list[ChatMessage]] = []
async for event in workflow.run(conversation, stream=True):
@@ -607,13 +546,11 @@ class TestRoundLimitEnforcement:
agent = StubAgent("agent", "response")
workflow = (
GroupChatBuilder()
.with_orchestrator(selection_func=selector)
.participants([agent])
.with_max_rounds(1) # Very low limit
.build()
)
workflow = GroupChatBuilder(
participants=[agent],
max_rounds=1, # Very low limit
selection_func=selector,
).build()
outputs: list[list[ChatMessage]] = []
async for event in workflow.run("test", stream=True):
@@ -642,13 +579,11 @@ class TestRoundLimitEnforcement:
agent = StubAgent("agent", "response from agent")
workflow = (
GroupChatBuilder()
.with_orchestrator(selection_func=selector)
.participants([agent])
.with_max_rounds(1) # Hit limit after first response
.build()
)
workflow = GroupChatBuilder(
participants=[agent],
max_rounds=1, # Hit limit after first response
selection_func=selector,
).build()
outputs: list[list[ChatMessage]] = []
async for event in workflow.run("test", stream=True):
@@ -674,13 +609,7 @@ async def test_group_chat_checkpoint_runtime_only() -> None:
agent_b = StubAgent("agentB", "Reply from B")
selector = make_sequence_selector()
wf = (
GroupChatBuilder()
.participants([agent_a, agent_b])
.with_orchestrator(selection_func=selector)
.with_max_rounds(2)
.build()
)
wf = GroupChatBuilder(participants=[agent_a, agent_b], max_rounds=2, selection_func=selector).build()
baseline_output: list[ChatMessage] | None = None
async for ev in wf.run("runtime checkpoint test", checkpoint_storage=storage, stream=True):
@@ -712,14 +641,12 @@ async def test_group_chat_checkpoint_runtime_overrides_buildtime() -> None:
agent_b = StubAgent("agentB", "Reply from B")
selector = make_sequence_selector()
wf = (
GroupChatBuilder()
.participants([agent_a, agent_b])
.with_orchestrator(selection_func=selector)
.with_max_rounds(2)
.with_checkpointing(buildtime_storage)
.build()
)
wf = GroupChatBuilder(
participants=[agent_a, agent_b],
max_rounds=2,
checkpoint_storage=buildtime_storage,
selection_func=selector,
).build()
baseline_output: list[ChatMessage] | None = None
async for ev in wf.run("override test", checkpoint_storage=runtime_storage, stream=True):
if ev.type == "output":
@@ -759,10 +686,12 @@ async def test_group_chat_with_request_info_filtering():
return "alpha"
workflow = (
GroupChatBuilder()
.with_orchestrator(selection_func=selector, orchestrator_name="manager")
.participants([alpha, beta])
.with_max_rounds(2)
GroupChatBuilder(
participants=[alpha, beta],
max_rounds=2,
selection_func=selector,
orchestrator_name="manager",
)
.with_request_info(agents=["beta"]) # Only pause before beta runs
.build()
)
@@ -811,10 +740,12 @@ async def test_group_chat_with_request_info_no_filter_pauses_all():
return "alpha"
workflow = (
GroupChatBuilder()
.with_orchestrator(selection_func=selector, orchestrator_name="manager")
.participants([alpha])
.with_max_rounds(1)
GroupChatBuilder(
participants=[alpha],
max_rounds=1,
selection_func=selector,
orchestrator_name="manager",
)
.with_request_info() # No filter - pause for all
.build()
)
@@ -833,12 +764,13 @@ async def test_group_chat_with_request_info_no_filter_pauses_all():
def test_group_chat_builder_with_request_info_returns_self():
"""Test that with_request_info() returns self for method chaining."""
builder = GroupChatBuilder()
agent = StubAgent("test", "response")
builder = GroupChatBuilder(participants=[agent])
result = builder.with_request_info()
assert result is builder
# Also test with agents parameter
builder2 = GroupChatBuilder()
builder2 = GroupChatBuilder(participants=[agent])
result2 = builder2.with_request_info(agents=["test"])
assert result2 is builder2
@@ -853,47 +785,41 @@ def test_group_chat_builder_rejects_empty_participant_factories():
return list(state.participants.keys())[0]
with pytest.raises(ValueError, match=r"participant_factories cannot be empty"):
GroupChatBuilder().register_participants([])
GroupChatBuilder(participant_factories=[])
with pytest.raises(
ValueError,
match=r"No participants provided\. Call \.participants\(\) or \.register_participants\(\) first\.",
match=r"Either participants or participant_factories must be provided\.",
):
GroupChatBuilder().with_orchestrator(selection_func=selector).build()
GroupChatBuilder()
def test_group_chat_builder_rejects_mixing_participants_and_factories():
"""Test that mixing .participants() and .register_participants() raises an error."""
"""Test that passing both participants and participant_factories to the constructor raises an error."""
alpha = StubAgent("alpha", "reply from alpha")
# Case 1: participants first, then register_participants
with pytest.raises(ValueError, match="Cannot mix .participants"):
GroupChatBuilder().participants([alpha]).register_participants([lambda: StubAgent("beta", "reply from beta")])
# Case 2: register_participants first, then participants
with pytest.raises(ValueError, match="Cannot mix .participants"):
GroupChatBuilder().register_participants([lambda: alpha]).participants([StubAgent("beta", "reply from beta")])
def test_group_chat_builder_rejects_multiple_calls_to_register_participants():
"""Test that multiple calls to .register_participants() raises an error."""
with pytest.raises(
ValueError, match=r"register_participants\(\) has already been called on this builder instance."
):
(
GroupChatBuilder()
.register_participants([lambda: StubAgent("alpha", "reply from alpha")])
.register_participants([lambda: StubAgent("beta", "reply from beta")])
with pytest.raises(ValueError, match="Cannot provide both participants and participant_factories"):
GroupChatBuilder(
participants=[alpha],
participant_factories=[lambda: StubAgent("beta", "reply from beta")],
)
def test_group_chat_builder_rejects_multiple_calls_to_participants():
"""Test that multiple calls to .participants() raises an error."""
with pytest.raises(ValueError, match="participants have already been set"):
(
GroupChatBuilder()
.participants([StubAgent("alpha", "reply from alpha")])
.participants([StubAgent("beta", "reply from beta")])
def test_group_chat_builder_rejects_both_factories_and_participants():
"""Test that passing both participant_factories and participants raises an error."""
with pytest.raises(ValueError, match="Cannot provide both participants and participant_factories"):
GroupChatBuilder(
participant_factories=[lambda: StubAgent("alpha", "reply from alpha")],
participants=[StubAgent("beta", "reply from beta")],
)
def test_group_chat_builder_rejects_both_participants_and_factories():
"""Test that passing both participants and participant_factories raises an error."""
with pytest.raises(ValueError, match="Cannot provide both participants and participant_factories"):
GroupChatBuilder(
participants=[StubAgent("alpha", "reply from alpha")],
participant_factories=[lambda: StubAgent("beta", "reply from beta")],
)
@@ -913,13 +839,11 @@ async def test_group_chat_with_participant_factories():
selector = make_sequence_selector()
workflow = (
GroupChatBuilder()
.register_participants([create_alpha, create_beta])
.with_orchestrator(selection_func=selector)
.with_max_rounds(2)
.build()
)
workflow = GroupChatBuilder(
participant_factories=[create_alpha, create_beta],
max_rounds=2,
selection_func=selector,
).build()
# Factories should be called during build
assert call_count == 2
@@ -948,12 +872,7 @@ async def test_group_chat_participant_factories_reusable_builder():
selector = make_sequence_selector()
builder = (
GroupChatBuilder()
.register_participants([create_alpha, create_beta])
.with_orchestrator(selection_func=selector)
.with_max_rounds(2)
)
builder = GroupChatBuilder(participant_factories=[create_alpha, create_beta], max_rounds=2, selection_func=selector)
# Build first workflow
wf1 = builder.build()
@@ -980,14 +899,12 @@ async def test_group_chat_participant_factories_with_checkpointing():
selector = make_sequence_selector()
workflow = (
GroupChatBuilder()
.register_participants([create_alpha, create_beta])
.with_orchestrator(selection_func=selector)
.with_checkpointing(storage)
.with_max_rounds(2)
.build()
)
workflow = GroupChatBuilder(
participant_factories=[create_alpha, create_beta],
checkpoint_storage=storage,
max_rounds=2,
selection_func=selector,
).build()
outputs: list[WorkflowEvent] = []
async for event in workflow.run("checkpoint test", stream=True):
@@ -1014,16 +931,15 @@ def test_group_chat_builder_rejects_multiple_orchestrator_configurations():
def agent_factory() -> ChatAgent:
return cast(ChatAgent, StubManagerAgent())
builder = GroupChatBuilder().with_orchestrator(selection_func=selector)
agent = StubAgent("test", "response")
# Already has a selection_func, should fail on second call
with pytest.raises(ValueError, match=r"A selection function has already been configured"):
builder.with_orchestrator(selection_func=selector)
# Both selection_func and orchestrator_agent provided simultaneously - should fail
with pytest.raises(ValueError, match=r"Exactly one of"):
GroupChatBuilder(participants=[agent], selection_func=selector, orchestrator_agent=StubManagerAgent())
# Test with agent_factory
builder2 = GroupChatBuilder().with_orchestrator(agent=agent_factory)
with pytest.raises(ValueError, match=r"A factory has already been configured"):
builder2.with_orchestrator(agent=agent_factory)
# Test with agent_factory - already has factory, should fail with second config
with pytest.raises(ValueError, match=r"Exactly one of"):
GroupChatBuilder(participants=[agent], orchestrator_agent=agent_factory, selection_func=selector)
def test_group_chat_builder_requires_exactly_one_orchestrator_option():
@@ -1035,13 +951,15 @@ def test_group_chat_builder_requires_exactly_one_orchestrator_option():
def agent_factory() -> ChatAgent:
return cast(ChatAgent, StubManagerAgent())
# No options provided
with pytest.raises(ValueError, match="Exactly one of"):
GroupChatBuilder().with_orchestrator() # type: ignore
agent = StubAgent("test", "response")
# No orchestrator options provided - only fails at build() time
with pytest.raises(ValueError, match="No orchestrator has been configured"):
GroupChatBuilder(participants=[agent]).build()
# Multiple options provided
with pytest.raises(ValueError, match="Exactly one of"):
GroupChatBuilder().with_orchestrator(selection_func=selector, agent=agent_factory) # type: ignore
GroupChatBuilder(participants=[agent], selection_func=selector, orchestrator_agent=agent_factory)
async def test_group_chat_with_orchestrator_factory_returning_chat_agent():
@@ -1112,7 +1030,7 @@ async def test_group_chat_with_orchestrator_factory_returning_chat_agent():
alpha = StubAgent("alpha", "reply from alpha")
beta = StubAgent("beta", "reply from beta")
workflow = GroupChatBuilder().participants([alpha, beta]).with_orchestrator(agent=agent_factory).build()
workflow = GroupChatBuilder(participants=[alpha, beta], orchestrator_agent=agent_factory).build()
# Factory should be called during build
assert factory_call_count == 1
@@ -1156,7 +1074,7 @@ def test_group_chat_with_orchestrator_factory_returning_base_orchestrator():
alpha = StubAgent("alpha", "reply from alpha")
workflow = GroupChatBuilder().participants([alpha]).with_orchestrator(orchestrator=orchestrator_factory).build()
workflow = GroupChatBuilder(participants=[alpha], orchestrator=orchestrator_factory).build()
# Factory should be called during build
assert factory_call_count == 1
@@ -1176,7 +1094,7 @@ async def test_group_chat_orchestrator_factory_reusable_builder():
alpha = StubAgent("alpha", "reply from alpha")
beta = StubAgent("beta", "reply from beta")
builder = GroupChatBuilder().participants([alpha, beta]).with_orchestrator(agent=agent_factory)
builder = GroupChatBuilder(participants=[alpha, beta], orchestrator_agent=agent_factory)
# Build first workflow
wf1 = builder.build()
@@ -1202,13 +1120,13 @@ def test_group_chat_orchestrator_factory_invalid_return_type():
TypeError,
match=r"Orchestrator factory must return ChatAgent or BaseGroupChatOrchestrator instance",
):
(GroupChatBuilder().participants([alpha]).with_orchestrator(orchestrator=invalid_factory).build())
GroupChatBuilder(participants=[alpha], orchestrator=invalid_factory).build()
with pytest.raises(
TypeError,
match=r"Orchestrator factory must return ChatAgent or BaseGroupChatOrchestrator instance",
):
(GroupChatBuilder().participants([alpha]).with_orchestrator(agent=invalid_factory).build())
GroupChatBuilder(participants=[alpha], orchestrator_agent=invalid_factory).build()
def test_group_chat_with_both_participant_and_orchestrator_factories():
@@ -1231,12 +1149,10 @@ def test_group_chat_with_both_participant_and_orchestrator_factories():
agent_factory_call_count += 1
return cast(ChatAgent, StubManagerAgent())
workflow = (
GroupChatBuilder()
.register_participants([create_alpha, create_beta])
.with_orchestrator(agent=agent_factory)
.build()
)
workflow = GroupChatBuilder(
participant_factories=[create_alpha, create_beta],
orchestrator_agent=agent_factory,
).build()
# All factories should be called during build
assert participant_factory_call_count == 2
@@ -1268,9 +1184,7 @@ async def test_group_chat_factories_reusable_for_multiple_workflows():
agent_factory_call_count += 1
return cast(ChatAgent, StubManagerAgent())
builder = (
GroupChatBuilder().register_participants([create_alpha, create_beta]).with_orchestrator(agent=agent_factory)
)
builder = GroupChatBuilder(participant_factories=[create_alpha, create_beta], orchestrator_agent=agent_factory)
# Build first workflow
wf1 = builder.build()
@@ -140,9 +140,11 @@ async def test_handoff():
# Without explicitly defining handoffs, the builder will create connections
# between all agents.
workflow = (
HandoffBuilder(participants=[triage, specialist, escalation])
HandoffBuilder(
participants=[triage, specialist, escalation],
termination_condition=lambda conv: sum(1 for m in conv if m.role == "user") >= 2,
)
.with_start_agent(triage)
.with_termination_condition(lambda conv: sum(1 for m in conv if m.role == "user") >= 2)
.build()
)
@@ -166,7 +168,15 @@ async def test_autonomous_mode_yields_output_without_user_request():
specialist = MockHandoffAgent(name="specialist")
workflow = (
HandoffBuilder(participants=[triage, specialist])
HandoffBuilder(
participants=[triage, specialist],
# This termination condition ensures the workflow runs through both agents.
# First message is the user message to triage, second is triage's response, which
# is a handoff to specialist, third is specialist's response that should not request
# user input due to autonomous mode. Fourth message will come from the specialist
# again and will trigger termination.
termination_condition=lambda conv: len(conv) >= 4,
)
.with_start_agent(triage)
# Since specialist has no handoff, the specialist will be generating normal responses.
# With autonomous mode, this should continue until the termination condition is met.
@@ -174,12 +184,6 @@ async def test_autonomous_mode_yields_output_without_user_request():
agents=[specialist],
turn_limits={resolve_agent_id(specialist): 1},
)
# This termination condition ensures the workflow runs through both agents.
# First message is the user message to triage, second is triage's response, which
# is a handoff to specialist, third is specialist's response that should not request
# user input due to autonomous mode. Fourth message will come from the specialist
# again and will trigger termination.
.with_termination_condition(lambda conv: len(conv) >= 4)
.build()
)
@@ -202,10 +206,9 @@ async def test_autonomous_mode_resumes_user_input_on_turn_limit():
worker = MockHandoffAgent(name="worker")
workflow = (
HandoffBuilder(participants=[triage, worker])
HandoffBuilder(participants=[triage, worker], termination_condition=lambda conv: False)
.with_start_agent(triage)
.with_autonomous_mode(agents=[worker], turn_limits={resolve_agent_id(worker): 2})
.with_termination_condition(lambda conv: False)
.build()
)
@@ -246,9 +249,8 @@ async def test_handoff_async_termination_condition() -> None:
worker = MockHandoffAgent(name="worker")
workflow = (
HandoffBuilder(participants=[coordinator, worker])
HandoffBuilder(participants=[coordinator, worker], termination_condition=async_termination)
.with_start_agent(coordinator)
.with_termination_condition(async_termination)
.build()
)
@@ -537,9 +539,11 @@ async def test_handoff_with_participant_factories():
return MockHandoffAgent(name="specialist")
workflow = (
HandoffBuilder(participant_factories={"triage": create_triage, "specialist": create_specialist})
HandoffBuilder(
participant_factories={"triage": create_triage, "specialist": create_specialist},
termination_condition=lambda conv: sum(1 for m in conv if m.role == "user") >= 2,
)
.with_start_agent("triage")
.with_termination_condition(lambda conv: sum(1 for m in conv if m.role == "user") >= 2)
.build()
)
@@ -607,12 +611,12 @@ async def test_handoff_with_participant_factories_and_add_handoff():
"triage": create_triage,
"specialist_a": create_specialist_a,
"specialist_b": create_specialist_b,
}
},
termination_condition=lambda conv: sum(1 for m in conv if m.role == "user") >= 3,
)
.with_start_agent("triage")
.add_handoff("triage", ["specialist_a", "specialist_b"])
.add_handoff("specialist_a", ["specialist_b"])
.with_termination_condition(lambda conv: sum(1 for m in conv if m.role == "user") >= 3)
.build()
)
@@ -650,10 +654,12 @@ async def test_handoff_participant_factories_with_checkpointing():
return MockHandoffAgent(name="specialist")
workflow = (
HandoffBuilder(participant_factories={"triage": create_triage, "specialist": create_specialist})
HandoffBuilder(
participant_factories={"triage": create_triage, "specialist": create_specialist},
checkpoint_storage=storage,
termination_condition=lambda conv: sum(1 for m in conv if m.role == "user") >= 2,
)
.with_start_agent("triage")
.with_checkpointing(storage)
.with_termination_condition(lambda conv: sum(1 for m in conv if m.role == "user") >= 2)
.build()
)
@@ -186,7 +186,7 @@ async def test_magentic_builder_returns_workflow_and_runs() -> None:
manager = FakeManager()
agent = StubAgent(manager.next_speaker_name, "first draft")
workflow = MagenticBuilder().participants([agent]).with_manager(manager=manager).build()
workflow = MagenticBuilder(participants=[agent], manager=manager).build()
assert isinstance(workflow, Workflow)
@@ -212,7 +212,7 @@ async def test_magentic_as_agent_does_not_accept_conversation() -> None:
manager = FakeManager()
writer = StubAgent(manager.next_speaker_name, "summary response")
workflow = MagenticBuilder().participants([writer]).with_manager(manager=manager).build()
workflow = MagenticBuilder(participants=[writer], manager=manager).build()
agent = workflow.as_agent(name="magentic-agent")
conversation = [
@@ -240,7 +240,7 @@ async def test_standard_manager_plan_and_replan_combined_ledger():
async def test_magentic_workflow_plan_review_approval_to_completion():
manager = FakeManager()
wf = MagenticBuilder().participants([DummyExec("agentA")]).with_manager(manager=manager).with_plan_review().build()
wf = MagenticBuilder(participants=[DummyExec("agentA")], enable_plan_review=True, manager=manager).build()
req_event: WorkflowEvent | None = None
async for ev in wf.run("do work", stream=True):
@@ -278,13 +278,11 @@ async def test_magentic_plan_review_with_revise():
return await super().replan(magentic_context)
manager = CountingManager()
wf = (
MagenticBuilder()
.participants([DummyExec(name=manager.next_speaker_name)])
.with_manager(manager=manager)
.with_plan_review()
.build()
)
wf = MagenticBuilder(
participants=[DummyExec(name=manager.next_speaker_name)],
enable_plan_review=True,
manager=manager,
).build()
# Wait for the initial plan review request
req_event: WorkflowEvent | None = None
@@ -324,12 +322,7 @@ async def test_magentic_plan_review_with_revise():
async def test_magentic_orchestrator_round_limit_produces_partial_result():
manager = FakeManager(max_round_count=1)
wf = (
MagenticBuilder()
.participants([DummyExec(name=manager.next_speaker_name)])
.with_manager(manager=manager)
.build()
)
wf = MagenticBuilder(participants=[DummyExec(name=manager.next_speaker_name)], manager=manager).build()
events: list[WorkflowEvent] = []
async for ev in wf.run("round limit test", stream=True):
@@ -354,14 +347,12 @@ async def test_magentic_checkpoint_resume_round_trip():
storage = InMemoryCheckpointStorage()
manager1 = FakeManager()
wf = (
MagenticBuilder()
.participants([DummyExec(name=manager1.next_speaker_name)])
.with_manager(manager=manager1)
.with_plan_review()
.with_checkpointing(storage)
.build()
)
wf = MagenticBuilder(
participants=[DummyExec(name=manager1.next_speaker_name)],
enable_plan_review=True,
checkpoint_storage=storage,
manager=manager1,
).build()
task_text = "checkpoint task"
req_event: WorkflowEvent | None = None
@@ -377,14 +368,12 @@ async def test_magentic_checkpoint_resume_round_trip():
resume_checkpoint = checkpoints[-1]
manager2 = FakeManager()
wf_resume = (
MagenticBuilder()
.participants([DummyExec(name=manager2.next_speaker_name)])
.with_manager(manager=manager2)
.with_plan_review()
.with_checkpointing(storage)
.build()
)
wf_resume = MagenticBuilder(
participants=[DummyExec(name=manager2.next_speaker_name)],
enable_plan_review=True,
checkpoint_storage=storage,
manager=manager2,
).build()
completed: WorkflowEvent | None = None
req_event = None
@@ -580,13 +569,7 @@ class StubAssistantsAgent(BaseAgent):
async def _collect_agent_responses_setup(participant: SupportsAgentRun) -> list[ChatMessage]:
captured: list[ChatMessage] = []
wf = (
MagenticBuilder()
.participants([participant])
.with_manager(manager=InvokeOnceManager())
.with_intermediate_outputs()
.build()
)
wf = MagenticBuilder(participants=[participant], intermediate_outputs=True, manager=InvokeOnceManager()).build()
# Run a bounded stream to allow one invoke and then completion
events: list[WorkflowEvent] = []
@@ -632,13 +615,9 @@ async def _collect_checkpoints(
async def test_magentic_checkpoint_resume_inner_loop_superstep():
storage = InMemoryCheckpointStorage()
workflow = (
MagenticBuilder()
.participants([StubThreadAgent()])
.with_manager(manager=InvokeOnceManager())
.with_checkpointing(storage)
.build()
)
workflow = MagenticBuilder(
participants=[StubThreadAgent()], checkpoint_storage=storage, manager=InvokeOnceManager()
).build()
async for event in workflow.run("inner-loop task", stream=True):
if event.type == "output":
@@ -647,13 +626,9 @@ async def test_magentic_checkpoint_resume_inner_loop_superstep():
checkpoints = await _collect_checkpoints(storage)
inner_loop_checkpoint = next(cp for cp in checkpoints if cp.metadata.get("superstep") == 1) # type: ignore[reportUnknownMemberType]
resumed = (
MagenticBuilder()
.participants([StubThreadAgent()])
.with_manager(manager=InvokeOnceManager())
.with_checkpointing(storage)
.build()
)
resumed = MagenticBuilder(
participants=[StubThreadAgent()], checkpoint_storage=storage, manager=InvokeOnceManager()
).build()
completed: WorkflowEvent | None = None
async for event in resumed.run(checkpoint_id=inner_loop_checkpoint.checkpoint_id, stream=True): # type: ignore[reportUnknownMemberType]
@@ -670,13 +645,7 @@ async def test_magentic_checkpoint_resume_from_saved_state():
# Use the working InvokeOnceManager first to get a completed workflow
manager = InvokeOnceManager()
workflow = (
MagenticBuilder()
.participants([StubThreadAgent()])
.with_manager(manager=manager)
.with_checkpointing(storage)
.build()
)
workflow = MagenticBuilder(participants=[StubThreadAgent()], checkpoint_storage=storage, manager=manager).build()
async for event in workflow.run("checkpoint resume task", stream=True):
if event.type == "output":
@@ -687,13 +656,9 @@ async def test_magentic_checkpoint_resume_from_saved_state():
# Verify we can resume from the last saved checkpoint
resumed_state = checkpoints[-1] # Use the last checkpoint
resumed_workflow = (
MagenticBuilder()
.participants([StubThreadAgent()])
.with_manager(manager=InvokeOnceManager())
.with_checkpointing(storage)
.build()
)
resumed_workflow = MagenticBuilder(
participants=[StubThreadAgent()], checkpoint_storage=storage, manager=InvokeOnceManager()
).build()
completed: WorkflowEvent | None = None
async for event in resumed_workflow.run(checkpoint_id=resumed_state.checkpoint_id, stream=True):
@@ -708,14 +673,12 @@ async def test_magentic_checkpoint_resume_rejects_participant_renames():
manager = InvokeOnceManager()
workflow = (
MagenticBuilder()
.participants([StubThreadAgent()])
.with_manager(manager=manager)
.with_plan_review()
.with_checkpointing(storage)
.build()
)
workflow = MagenticBuilder(
participants=[StubThreadAgent()],
enable_plan_review=True,
checkpoint_storage=storage,
manager=manager,
).build()
req_event: WorkflowEvent | None = None
async for event in workflow.run("task", stream=True):
@@ -728,14 +691,12 @@ async def test_magentic_checkpoint_resume_rejects_participant_renames():
checkpoints = await _collect_checkpoints(storage)
target_checkpoint = checkpoints[-1]
renamed_workflow = (
MagenticBuilder()
.participants([StubThreadAgent(name="renamedAgent")])
.with_manager(manager=InvokeOnceManager())
.with_plan_review()
.with_checkpointing(storage)
.build()
)
renamed_workflow = MagenticBuilder(
participants=[StubThreadAgent(name="renamedAgent")],
enable_plan_review=True,
checkpoint_storage=storage,
manager=InvokeOnceManager(),
).build()
with pytest.raises(WorkflowCheckpointException, match="Workflow graph has changed"):
async for _ in renamed_workflow.run(
@@ -772,7 +733,7 @@ class NotProgressingManager(MagenticManagerBase):
async def test_magentic_stall_and_reset_reach_limits():
manager = NotProgressingManager(max_round_count=10, max_stall_count=0, max_reset_count=1)
wf = MagenticBuilder().participants([DummyExec("agentA")]).with_manager(manager=manager).build()
wf = MagenticBuilder(participants=[DummyExec("agentA")], manager=manager).build()
events: list[WorkflowEvent] = []
async for ev in wf.run("test limits", stream=True):
@@ -797,7 +758,7 @@ async def test_magentic_checkpoint_runtime_only() -> None:
storage = InMemoryCheckpointStorage()
manager = FakeManager(max_round_count=10)
wf = MagenticBuilder().participants([DummyExec("agentA")]).with_manager(manager=manager).build()
wf = MagenticBuilder(participants=[DummyExec("agentA")], manager=manager).build()
baseline_output: ChatMessage | None = None
async for ev in wf.run("runtime checkpoint test", checkpoint_storage=storage, stream=True):
@@ -829,13 +790,9 @@ async def test_magentic_checkpoint_runtime_overrides_buildtime() -> None:
runtime_storage = FileCheckpointStorage(temp_dir2)
manager = FakeManager(max_round_count=10)
wf = (
MagenticBuilder()
.participants([DummyExec("agentA")])
.with_manager(manager=manager)
.with_checkpointing(buildtime_storage)
.build()
)
wf = MagenticBuilder(
participants=[DummyExec("agentA")], checkpoint_storage=buildtime_storage, manager=manager
).build()
baseline_output: ChatMessage | None = None
async for ev in wf.run("override test", checkpoint_storage=runtime_storage, stream=True):
@@ -884,13 +841,7 @@ async def test_magentic_checkpoint_restore_no_duplicate_history():
manager = FakeManager(max_round_count=10)
storage = InMemoryCheckpointStorage()
wf = (
MagenticBuilder()
.participants([DummyExec("agentA")])
.with_manager(manager=manager)
.with_checkpointing(storage)
.build()
)
wf = MagenticBuilder(participants=[DummyExec("agentA")], checkpoint_storage=storage, manager=manager).build()
# Run with conversation history to create initial checkpoint
conversation: list[ChatMessage] = [
@@ -947,47 +898,41 @@ async def test_magentic_checkpoint_restore_no_duplicate_history():
def test_magentic_builder_rejects_empty_participant_factories():
"""Test that MagenticBuilder rejects empty participant_factories list."""
with pytest.raises(ValueError, match=r"participant_factories cannot be empty"):
MagenticBuilder().register_participants([])
MagenticBuilder(participant_factories=[])
with pytest.raises(
ValueError,
match=r"No participants provided\. Call \.participants\(\) or \.register_participants\(\) first\.",
match=r"Either participants or participant_factories must be provided\.",
):
MagenticBuilder().with_manager(manager=FakeManager()).build()
MagenticBuilder()
def test_magentic_builder_rejects_mixing_participants_and_factories():
"""Test that mixing .participants() and .register_participants() raises an error."""
"""Test that passing both participants and participant_factories to the constructor raises an error."""
agent = StubAgent("agentA", "reply from agentA")
# Case 1: participants first, then register_participants
with pytest.raises(ValueError, match="Cannot mix .participants"):
MagenticBuilder().participants([agent]).register_participants([lambda: StubAgent("agentB", "reply")])
# Case 2: register_participants first, then participants
with pytest.raises(ValueError, match="Cannot mix .participants"):
MagenticBuilder().register_participants([lambda: agent]).participants([StubAgent("agentB", "reply")])
def test_magentic_builder_rejects_multiple_calls_to_register_participants():
"""Test that multiple calls to .register_participants() raises an error."""
with pytest.raises(
ValueError, match=r"register_participants\(\) has already been called on this builder instance."
):
(
MagenticBuilder()
.register_participants([lambda: StubAgent("agentA", "reply from agentA")])
.register_participants([lambda: StubAgent("agentB", "reply from agentB")])
with pytest.raises(ValueError, match="Cannot provide both participants and participant_factories"):
MagenticBuilder(
participants=[agent],
participant_factories=[lambda: StubAgent("agentB", "reply")],
)
def test_magentic_builder_rejects_multiple_calls_to_participants():
"""Test that multiple calls to .participants() raises an error."""
with pytest.raises(ValueError, match="participants have already been set"):
(
MagenticBuilder()
.participants([StubAgent("agentA", "reply from agentA")])
.participants([StubAgent("agentB", "reply from agentB")])
def test_magentic_builder_rejects_both_factories_and_participants():
"""Test that passing both participant_factories and participants raises an error."""
with pytest.raises(ValueError, match="Cannot provide both participants and participant_factories"):
MagenticBuilder(
participant_factories=[lambda: StubAgent("agentA", "reply from agentA")],
participants=[StubAgent("agentB", "reply from agentB")],
)
def test_magentic_builder_rejects_both_participants_and_factories():
"""Test that passing both participants and participant_factories raises an error."""
with pytest.raises(ValueError, match="Cannot provide both participants and participant_factories"):
MagenticBuilder(
participants=[StubAgent("agentA", "reply from agentA")],
participant_factories=[lambda: StubAgent("agentB", "reply from agentB")],
)
@@ -1001,7 +946,7 @@ async def test_magentic_with_participant_factories():
return StubAgent("agentA", "reply from agentA")
manager = FakeManager()
workflow = MagenticBuilder().register_participants([create_agent]).with_manager(manager=manager).build()
workflow = MagenticBuilder(participant_factories=[create_agent], manager=manager).build()
# Factory should be called during build
assert call_count == 1
@@ -1023,7 +968,7 @@ async def test_magentic_participant_factories_reusable_builder():
call_count += 1
return StubAgent("agentA", "reply from agentA")
builder = MagenticBuilder().register_participants([create_agent]).with_manager(manager=FakeManager())
builder = MagenticBuilder(participant_factories=[create_agent], manager=FakeManager())
# Build first workflow
wf1 = builder.build()
@@ -1045,13 +990,9 @@ async def test_magentic_participant_factories_with_checkpointing():
return StubAgent("agentA", "reply from agentA")
manager = FakeManager()
workflow = (
MagenticBuilder()
.register_participants([create_agent])
.with_manager(manager=manager)
.with_checkpointing(storage)
.build()
)
workflow = MagenticBuilder(
participant_factories=[create_agent], checkpoint_storage=storage, manager=manager
).build()
outputs: list[WorkflowEvent] = []
async for event in workflow.run("checkpoint test", stream=True):
@@ -1072,27 +1013,27 @@ async def test_magentic_participant_factories_with_checkpointing():
def test_magentic_builder_rejects_multiple_manager_configurations():
"""Test that configuring multiple managers raises ValueError."""
manager = FakeManager()
agent = StubAgent("agentA", "reply")
builder = MagenticBuilder().with_manager(manager=manager)
with pytest.raises(ValueError, match=r"with_manager\(\) has already been called"):
builder.with_manager(manager=manager)
with pytest.raises(ValueError, match=r"Exactly one of"):
MagenticBuilder(participants=[agent], manager=manager, manager_agent=StubManagerAgent())
def test_magentic_builder_requires_exactly_one_manager_option():
"""Test that exactly one manager option must be provided."""
manager = FakeManager()
agent = StubAgent("agentA", "reply")
def manager_factory() -> MagenticManagerBase:
return FakeManager()
# No options provided
with pytest.raises(ValueError, match="Exactly one of"):
MagenticBuilder().with_manager() # type: ignore
# No options provided - only fails at build() time
with pytest.raises(ValueError, match="No manager configured"):
MagenticBuilder(participants=[agent]).build()
# Multiple options provided
with pytest.raises(ValueError, match="Exactly one of"):
MagenticBuilder().with_manager(manager=manager, manager_factory=manager_factory) # type: ignore
MagenticBuilder(participants=[agent], manager=manager, manager_factory=manager_factory)
async def test_magentic_with_manager_factory():
@@ -1105,7 +1046,7 @@ async def test_magentic_with_manager_factory():
return FakeManager()
agent = StubAgent("agentA", "reply from agentA")
workflow = MagenticBuilder().participants([agent]).with_manager(manager_factory=manager_factory).build()
workflow = MagenticBuilder(participants=[agent], manager_factory=manager_factory).build()
# Factory should be called during build
assert factory_call_count == 1
@@ -1128,12 +1069,9 @@ async def test_magentic_with_agent_factory():
return cast(SupportsAgentRun, StubManagerAgent())
participant = StubAgent("agentA", "reply from agentA")
workflow = (
MagenticBuilder()
.participants([participant])
.with_manager(agent_factory=agent_factory, max_round_count=1)
.build()
)
workflow = MagenticBuilder(
participants=[participant], manager_agent_factory=agent_factory, max_round_count=1
).build()
# Factory should be called during build
assert factory_call_count == 1
@@ -1158,7 +1096,7 @@ async def test_magentic_manager_factory_reusable_builder():
return FakeManager()
agent = StubAgent("agentA", "reply from agentA")
builder = MagenticBuilder().participants([agent]).with_manager(manager_factory=manager_factory)
builder = MagenticBuilder(participants=[agent], manager_factory=manager_factory)
# Build first workflow
wf1 = builder.build()
@@ -1189,9 +1127,7 @@ def test_magentic_with_both_participant_and_manager_factories():
manager_factory_call_count += 1
return FakeManager()
workflow = (
MagenticBuilder().register_participants([create_agent]).with_manager(manager_factory=manager_factory).build()
)
workflow = MagenticBuilder(participant_factories=[create_agent], manager_factory=manager_factory).build()
# All factories should be called during build
assert participant_factory_call_count == 1
@@ -1216,7 +1152,7 @@ async def test_magentic_factories_reusable_for_multiple_workflows():
manager_factory_call_count += 1
return FakeManager()
builder = MagenticBuilder().register_participants([create_agent]).with_manager(manager_factory=manager_factory)
builder = MagenticBuilder(participant_factories=[create_agent], manager_factory=manager_factory)
# Build first workflow
wf1 = builder.build()
@@ -1266,25 +1202,21 @@ def test_magentic_agent_factory_with_standard_manager_options():
)
participant = StubAgent("agentA", "reply from agentA")
workflow = (
MagenticBuilder()
.participants([participant])
.with_manager(
agent_factory=agent_factory,
task_ledger=custom_task_ledger,
max_stall_count=custom_max_stall_count,
max_reset_count=custom_max_reset_count,
max_round_count=custom_max_round_count,
task_ledger_facts_prompt=custom_facts_prompt,
task_ledger_plan_prompt=custom_plan_prompt,
task_ledger_full_prompt=custom_full_prompt,
task_ledger_facts_update_prompt=custom_facts_update_prompt,
task_ledger_plan_update_prompt=custom_plan_update_prompt,
progress_ledger_prompt=custom_progress_prompt,
final_answer_prompt=custom_final_prompt,
)
.build()
)
workflow = MagenticBuilder(
participants=[participant],
manager_agent_factory=agent_factory,
task_ledger=custom_task_ledger,
max_stall_count=custom_max_stall_count,
max_reset_count=custom_max_reset_count,
max_round_count=custom_max_round_count,
task_ledger_facts_prompt=custom_facts_prompt,
task_ledger_plan_prompt=custom_plan_prompt,
task_ledger_full_prompt=custom_full_prompt,
task_ledger_facts_update_prompt=custom_facts_update_prompt,
task_ledger_plan_update_prompt=custom_plan_update_prompt,
progress_ledger_prompt=custom_progress_prompt,
final_answer_prompt=custom_final_prompt,
).build()
# Factory should be called during build
assert factory_call_count == 1
@@ -68,38 +68,36 @@ class _InvalidExecutor(Executor):
def test_sequential_builder_rejects_empty_participants() -> None:
with pytest.raises(ValueError):
SequentialBuilder().participants([])
SequentialBuilder(participants=[])
def test_sequential_builder_rejects_empty_participant_factories() -> None:
with pytest.raises(ValueError):
SequentialBuilder().register_participants([])
SequentialBuilder(participant_factories=[])
def test_sequential_builder_rejects_mixing_participants_and_factories() -> None:
"""Test that mixing .participants() and .register_participants() raises an error."""
"""Test that passing both participants and participant_factories to the constructor raises an error."""
a1 = _EchoAgent(id="agent1", name="A1")
# Try .participants() then .register_participants()
with pytest.raises(ValueError, match="Cannot mix"):
SequentialBuilder().participants([a1]).register_participants([lambda: _EchoAgent(id="agent2", name="A2")])
# Try .register_participants() then .participants()
with pytest.raises(ValueError, match="Cannot mix"):
SequentialBuilder().register_participants([lambda: _EchoAgent(id="agent1", name="A1")]).participants([a1])
with pytest.raises(ValueError, match="Cannot provide both participants and participant_factories"):
SequentialBuilder(
participants=[a1],
participant_factories=[lambda: _EchoAgent(id="agent2", name="A2")],
)
def test_sequential_builder_validation_rejects_invalid_executor() -> None:
"""Test that adding an invalid executor to the builder raises an error."""
with pytest.raises(TypeCompatibilityError):
SequentialBuilder().participants([_EchoAgent(id="agent1", name="A1"), _InvalidExecutor(id="invalid")]).build()
SequentialBuilder(participants=[_EchoAgent(id="agent1", name="A1"), _InvalidExecutor(id="invalid")]).build()
async def test_sequential_agents_append_to_context() -> None:
a1 = _EchoAgent(id="agent1", name="A1")
a2 = _EchoAgent(id="agent2", name="A2")
wf = SequentialBuilder().participants([a1, a2]).build()
wf = SequentialBuilder(participants=[a1, a2]).build()
completed = False
output: list[ChatMessage] | None = None
@@ -132,7 +130,7 @@ async def test_sequential_register_participants_with_agent_factories() -> None:
def create_agent2() -> _EchoAgent:
return _EchoAgent(id="agent2", name="A2")
wf = SequentialBuilder().register_participants([create_agent1, create_agent2]).build()
wf = SequentialBuilder(participant_factories=[create_agent1, create_agent2]).build()
completed = False
output: list[ChatMessage] | None = None
@@ -158,7 +156,7 @@ async def test_sequential_with_custom_executor_summary() -> None:
a1 = _EchoAgent(id="agent1", name="A1")
summarizer = _SummarizerExec(id="summarizer")
wf = SequentialBuilder().participants([a1, summarizer]).build()
wf = SequentialBuilder(participants=[a1, summarizer]).build()
completed = False
output: list[ChatMessage] | None = None
@@ -189,7 +187,7 @@ async def test_sequential_register_participants_mixed_agents_and_executors() ->
def create_summarizer() -> _SummarizerExec:
return _SummarizerExec(id="summarizer")
wf = SequentialBuilder().register_participants([create_agent, create_summarizer]).build()
wf = SequentialBuilder(participant_factories=[create_agent, create_summarizer]).build()
completed = False
output: list[ChatMessage] | None = None
@@ -215,7 +213,7 @@ async def test_sequential_checkpoint_resume_round_trip() -> None:
storage = InMemoryCheckpointStorage()
initial_agents = (_EchoAgent(id="agent1", name="A1"), _EchoAgent(id="agent2", name="A2"))
wf = SequentialBuilder().participants(list(initial_agents)).with_checkpointing(storage).build()
wf = SequentialBuilder(participants=list(initial_agents), checkpoint_storage=storage).build()
baseline_output: list[ChatMessage] | None = None
async for ev in wf.run("checkpoint sequential", stream=True):
@@ -236,7 +234,7 @@ async def test_sequential_checkpoint_resume_round_trip() -> None:
)
resumed_agents = (_EchoAgent(id="agent1", name="A1"), _EchoAgent(id="agent2", name="A2"))
wf_resume = SequentialBuilder().participants(list(resumed_agents)).with_checkpointing(storage).build()
wf_resume = SequentialBuilder(participants=list(resumed_agents), checkpoint_storage=storage).build()
resumed_output: list[ChatMessage] | None = None
async for ev in wf_resume.run(checkpoint_id=resume_checkpoint.checkpoint_id, stream=True):
@@ -258,7 +256,7 @@ async def test_sequential_checkpoint_runtime_only() -> None:
storage = InMemoryCheckpointStorage()
agents = (_EchoAgent(id="agent1", name="A1"), _EchoAgent(id="agent2", name="A2"))
wf = SequentialBuilder().participants(list(agents)).build()
wf = SequentialBuilder(participants=list(agents)).build()
baseline_output: list[ChatMessage] | None = None
async for ev in wf.run("runtime checkpoint test", checkpoint_storage=storage, stream=True):
@@ -279,7 +277,7 @@ async def test_sequential_checkpoint_runtime_only() -> None:
)
resumed_agents = (_EchoAgent(id="agent1", name="A1"), _EchoAgent(id="agent2", name="A2"))
wf_resume = SequentialBuilder().participants(list(resumed_agents)).build()
wf_resume = SequentialBuilder(participants=list(resumed_agents)).build()
resumed_output: list[ChatMessage] | None = None
async for ev in wf_resume.run(
@@ -309,7 +307,7 @@ async def test_sequential_checkpoint_runtime_overrides_buildtime() -> None:
runtime_storage = FileCheckpointStorage(temp_dir2)
agents = (_EchoAgent(id="agent1", name="A1"), _EchoAgent(id="agent2", name="A2"))
wf = SequentialBuilder().participants(list(agents)).with_checkpointing(buildtime_storage).build()
wf = SequentialBuilder(participants=list(agents), checkpoint_storage=buildtime_storage).build()
baseline_output: list[ChatMessage] | None = None
async for ev in wf.run("override test", checkpoint_storage=runtime_storage, stream=True):
@@ -337,7 +335,7 @@ async def test_sequential_register_participants_with_checkpointing() -> None:
def create_agent2() -> _EchoAgent:
return _EchoAgent(id="agent2", name="A2")
wf = SequentialBuilder().register_participants([create_agent1, create_agent2]).with_checkpointing(storage).build()
wf = SequentialBuilder(participant_factories=[create_agent1, create_agent2], checkpoint_storage=storage).build()
baseline_output: list[ChatMessage] | None = None
async for ev in wf.run("checkpoint with factories", stream=True):
@@ -357,9 +355,9 @@ async def test_sequential_register_participants_with_checkpointing() -> None:
checkpoints[-1],
)
wf_resume = (
SequentialBuilder().register_participants([create_agent1, create_agent2]).with_checkpointing(storage).build()
)
wf_resume = SequentialBuilder(
participant_factories=[create_agent1, create_agent2], checkpoint_storage=storage
).build()
resumed_output: list[ChatMessage] | None = None
async for ev in wf_resume.run(checkpoint_id=resume_checkpoint.checkpoint_id, stream=True):
@@ -385,7 +383,7 @@ async def test_sequential_register_participants_factories_called_on_build() -> N
call_count += 1
return _EchoAgent(id=f"agent{call_count}", name=f"A{call_count}")
builder = SequentialBuilder().register_participants([create_agent, create_agent])
builder = SequentialBuilder(participant_factories=[create_agent, create_agent])
# Factories should not be called yet
assert call_count == 0
@@ -418,7 +416,7 @@ async def test_sequential_builder_reusable_after_build_with_participants() -> No
a1 = _EchoAgent(id="agent1", name="A1")
a2 = _EchoAgent(id="agent2", name="A2")
builder = SequentialBuilder().participants([a1, a2])
builder = SequentialBuilder(participants=[a1, a2])
# Build first workflow
builder.build()
@@ -442,7 +440,7 @@ async def test_sequential_builder_reusable_after_build_with_factories() -> None:
call_count += 1
return _EchoAgent(id="agent2", name="A2")
builder = SequentialBuilder().register_participants([create_agent1, create_agent2])
builder = SequentialBuilder(participant_factories=[create_agent1, create_agent2])
# Build first workflow - factories should be called
builder.build()