Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 87 additions & 66 deletions swarms/structs/round_robin.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,19 @@ class RoundRobinSwarm(SerializableMixin):
"""
A swarm implementation that executes tasks in a true round-robin fashion.

Agents are visited in their declared insertion order, cycling through the
full roster once per loop. Over K loops with N agents the schedule is:
Agents are visited in roster order, cycling through the full list once
per loop. Over K loops with N agents the within-run schedule is:

turn t -> agents[t % N] for t in range(K * N)
turn t -> agents[(start + t) % N] for t in range(K * N)

The order is deterministic and identical on every loop, so each agent
receives exactly `max_loops` turns and every agent reads the full
conversation history accumulated by the agents that spoke before it.
``start`` is ``self.index`` when ``persist_rotation`` is True, otherwise
0. By default ``persist_rotation`` is False so each ``run()`` replays
the same visit order turn-for-turn; set it to True to resume rotation
across ``run()`` / ``run_batch()`` calls.

The order is deterministic within a run: each agent receives exactly
`max_loops` turns and every agent reads the full conversation history
accumulated by the agents that spoke before it.

Args:
name (str): Name of the swarm. Defaults to "RoundRobinSwarm".
Expand All @@ -89,14 +94,20 @@ class RoundRobinSwarm(SerializableMixin):
verbose (bool, optional): Flag to enable verbose mode. Defaults to False.
max_loops (int, optional): Maximum number of loops to run. Defaults to 1.
output_type (OutputType, optional): Type of output format. Defaults to "final".
persist_rotation (bool, optional): When True, resume the round-robin
pointer across ``run()`` calls so ``run_batch`` gives each agent
the opening turn equally often. Defaults to False to preserve
current turn-for-turn reproducibility.

Attributes:
name (str): Name of the swarm.
description (str): Description of the swarm's purpose.
agents (List[Agent]): List of agents in the swarm.
verbose (bool): Flag to enable verbose mode.
max_loops (int): Maximum number of loops to run.
index (int): Current index of the agent being executed.
index (int): Roster offset for the next run's opening turn when
``persist_rotation`` is True.
persist_rotation (bool): Whether rotation carries across ``run()`` calls.
output_type (OutputType): Type of output format.
conversation (Conversation): Conversation history for the swarm.

Expand All @@ -119,6 +130,7 @@ def __init__(
verbose: bool = False,
max_loops: int = 1,
output_type: OutputType = "final",
persist_rotation: bool = False,
):

self.name = name
Expand All @@ -128,6 +140,7 @@ def __init__(
self.max_loops = max_loops
self.index = 0
self.output_type = output_type
self.persist_rotation = persist_rotation

# Initialize conversation for tracking agent interactions
self.conversation = Conversation(
Expand Down Expand Up @@ -192,11 +205,13 @@ def run(
"""
Execute the task across the agents in true round-robin order.

The schedule is deterministic: for N agents and `max_loops` loops the
visit order is `agents[t % N]` for `t` in `range(max_loops * N)`. The
order is identical on every loop, every agent reads the full
conversation transcript accumulated so far, and every agent receives
exactly `max_loops` turns.
The schedule is deterministic within a run: for N agents and
``max_loops`` loops the visit order is
``agents[(start + t) % N]`` for ``t`` in ``range(max_loops * N)``,
where ``start`` is ``self.index`` when ``persist_rotation`` is True.
Every agent receives exactly ``max_loops`` turns. When rotation
persists, the opening turn advances by one after each run so
``run_batch`` does not always seat ``agents[0]`` first.

Args:
task (str): The task to be executed.
Expand All @@ -220,67 +235,73 @@ def run(
f"Starting round-robin execution with task on {n} agents: {agent_names}",
)

for loop in range(self.max_loops):
self._log(
"debug",
f"Starting loop {loop + 1}/{self.max_loops}",
)
start = self.index % n if self.persist_rotation else 0
total_turns = self.max_loops * n

for i, current_agent in enumerate(self.agents):
self.index = (loop * n) + i

prev_name = (
self.agents[i - 1].agent_name
if i > 0
else (
self.agents[-1].agent_name
if loop > 0
else None
)
)
next_name = (
self.agents[i + 1].agent_name
if i + 1 < n
else (
self.agents[0].agent_name
if loop + 1 < self.max_loops
else None
)
)
for turn in range(total_turns):
loop = turn // n + 1
roster_i = (start + turn) % n
current_agent = self.agents[roster_i]

conversation_context = (
self.conversation.return_history_as_string()
if turn % n == 0:
self._log(
"debug",
f"Starting loop {loop}/{self.max_loops}",
)

turn_header = build_turn_header(
agent_name=current_agent.agent_name,
position=i + 1,
total=n,
loop=loop + 1,
max_loops=self.max_loops,
prev_name=prev_name,
next_name=next_name,
agent_names=agent_names,
)
prev_name = (
None
if turn == 0
else self.agents[
(start + turn - 1) % n
].agent_name
)
next_name = (
None
if turn == total_turns - 1
else self.agents[
(start + turn + 1) % n
].agent_name
)

collaborative_task = build_collaborative_task(
conversation_context=conversation_context,
turn_header=turn_header,
conversation_context = (
self.conversation.return_history_as_string()
)

turn_header = build_turn_header(
agent_name=current_agent.agent_name,
position=turn % n + 1,
total=n,
loop=loop,
max_loops=self.max_loops,
prev_name=prev_name,
next_name=next_name,
agent_names=agent_names,
)

collaborative_task = build_collaborative_task(
conversation_context=conversation_context,
turn_header=turn_header,
)

try:
self._execute_agent(
current_agent,
collaborative_task,
*args,
**kwargs,
)
except Exception as e:
self._log(
"error",
f"Agent {current_agent.agent_name} failed: {str(e)}",
)
raise

try:
self._execute_agent(
current_agent,
collaborative_task,
*args,
**kwargs,
)
except Exception as e:
self._log(
"error",
f"Agent {current_agent.agent_name} failed: {str(e)}",
)
raise
# max_loops * n turns is a whole number of cycles, so it returns
# the offset to where it started — advancing the opener needs +1.
if self.persist_rotation:
self.index = (start + 1) % n

self._log(
"success",
Expand Down
89 changes: 82 additions & 7 deletions tests/structs/test_round_robin_swarm.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,98 @@
from unittest.mock import patch

import pytest
from swarms.structs.round_robin import RoundRobinSwarm

from swarms.structs.agent import Agent
from swarms.structs.round_robin import RoundRobinSwarm


@pytest.fixture
def round_robin_swarm():
agents = [Agent(name=f"Agent{i}") for i in range(3)]
agents = [Agent(agent_name=f"Agent{i}") for i in range(3)]
return RoundRobinSwarm(agents=agents, verbose=True, max_loops=2)


def test_init(round_robin_swarm):
assert isinstance(round_robin_swarm, RoundRobinSwarm)
assert round_robin_swarm.verbose is True
assert round_robin_swarm.max_loops == 2
assert round_robin_swarm.persist_rotation is False
assert len(round_robin_swarm.agents) == 3


def test_run(round_robin_swarm):
task = "test_task"
result = round_robin_swarm.run(task)
assert result == task
assert round_robin_swarm.index == 0
def _visit_order(swarm, task="t"):
"""Agent names in turn order without LLM calls."""
seen = []

def record(agent, *_args, **_kwargs):
seen.append(agent.agent_name)
return f"{agent.agent_name}-ok"

with patch.object(swarm, "_execute_agent", side_effect=record):
swarm.run(task)
return seen


def test_rotation_does_not_persist_by_default():
"""Default stays turn-for-turn reproducible across consecutive run() calls."""
agents = [Agent(agent_name=f"A{i}") for i in range(3)]
swarm = RoundRobinSwarm(agents=agents, max_loops=2)

first = _visit_order(swarm)
assert _visit_order(swarm) == first
assert first[0] == "A0"
assert swarm.index == 0


def test_persist_rotation_run_batch_distributes_opening_turn():
"""N tasks on N agents: each agent opens exactly once when rotation persists."""
agents = [Agent(agent_name=f"A{i}") for i in range(3)]
swarm = RoundRobinSwarm(
agents=agents, max_loops=1, persist_rotation=True
)
n = len(agents)

all_calls = []

def record(agent, *_args, **_kwargs):
all_calls.append(agent.agent_name)
return "ok"

with patch.object(swarm, "_execute_agent", side_effect=record):
swarm.run_batch(["task-1", "task-2", "task-3"])

openers = [all_calls[i * n] for i in range(n)]
assert openers == ["A0", "A1", "A2"]


def test_persist_rotation_false_identical_visit_order():
"""With the flag off, two consecutive run() calls produce the same order."""
agents = [Agent(agent_name=f"A{i}") for i in range(3)]
swarm = RoundRobinSwarm(
agents=agents, max_loops=2, persist_rotation=False
)

first = _visit_order(swarm)
second = _visit_order(swarm)

assert first == second
assert first[0] == "A0"
assert swarm.index == 0


def test_within_run_every_agent_gets_max_loops_turns():
agents = [Agent(agent_name=f"A{i}") for i in range(3)]
swarm = RoundRobinSwarm(
agents=agents, max_loops=2, persist_rotation=True
)

order = _visit_order(swarm)

assert len(order) == 6
assert {
name: order.count(name) for name in ("A0", "A1", "A2")
} == {
"A0": 2,
"A1": 2,
"A2": 2,
}