Skip to content

Commit 4088b32

Browse files
authored
Merge pull request #1479 from Steve-Dusty/fix/repeated-agent-flow-awareness
fix: correct sequential awareness for repeated agents in flow
2 parents 0ebeea5 + af38ee0 commit 4088b32

2 files changed

Lines changed: 252 additions & 16 deletions

File tree

swarms/structs/agent_rearrange.py

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -327,35 +327,41 @@ def validate_flow(self):
327327
)
328328
agents_in_flow.append(agent_name)
329329

330-
# # If the length of the agents does not equal the length of the agents in flow
331-
# if len(set(agents_in_flow)) != len(agents_in_flow):
332-
# raise ValueError(
333-
# "Duplicate agent names in the flow are not allowed."
334-
# )
335-
336330
logger.info(f"Flow: {self.flow} is valid.")
337331
return True
338332

339333
def _get_sequential_awareness(
340-
self, agent_name: str, tasks: List[str]
334+
self,
335+
agent_name: str,
336+
tasks: List[str],
337+
task_idx: int = None,
341338
) -> str:
342339
"""
343340
Determines the sequential awareness information for an agent in a sequential flow.
344341
345342
Args:
346343
agent_name (str): The name of the current agent.
347344
tasks (List[str]): The list of tasks in the flow.
345+
task_idx (int, optional): The exact position index of this agent invocation
346+
in the flow. When provided, uses this directly instead of searching by name.
347+
This is essential for repeated agents (e.g., "writer -> reviewer -> writer")
348+
so each occurrence gets correct positional awareness.
348349
349350
Returns:
350351
str: A string describing the agents ahead and behind in the sequence.
351352
"""
352-
# Find the position of the current agent in the flow
353-
agent_position = None
354-
for i, task in enumerate(tasks):
355-
agent_names = [name.strip() for name in task.split(",")]
356-
if agent_name in agent_names:
357-
agent_position = i
358-
break
353+
# Use provided position index if available, otherwise search by name
354+
if task_idx is not None:
355+
agent_position = task_idx
356+
else:
357+
agent_position = None
358+
for i, task in enumerate(tasks):
359+
agent_names = [
360+
name.strip() for name in task.split(",")
361+
]
362+
if agent_name in agent_names:
363+
agent_position = i
364+
break
359365

360366
if agent_position is None:
361367
return ""
@@ -526,6 +532,7 @@ def _run_sequential_workflow(
526532
self,
527533
agent_name: str,
528534
tasks: List[str],
535+
task_idx: int = None,
529536
img: str = None,
530537
*args,
531538
**kwargs,
@@ -541,6 +548,8 @@ def _run_sequential_workflow(
541548
agent_name (str): Name of the agent to run sequentially.
542549
tasks (List[str]): List of all tasks in the flow for awareness context.
543550
Used to determine the agent's position and provide awareness info.
551+
task_idx (int, optional): The position index of this agent in the flow.
552+
Essential for repeated agents so each occurrence gets correct awareness.
544553
img (str, optional): Image input for agents that support it.
545554
Defaults to None.
546555
*args: Additional positional arguments passed to agent execution.
@@ -560,7 +569,7 @@ def _run_sequential_workflow(
560569

561570
# Add sequential awareness information for the agent
562571
awareness_info = self._get_sequential_awareness(
563-
agent_name, tasks
572+
agent_name, tasks, task_idx=task_idx
564573
)
565574
if awareness_info:
566575
self.conversation.add("system", awareness_info)
@@ -676,11 +685,20 @@ def _run(
676685
result = self._run_sequential_workflow(
677686
agent_name=agent_name,
678687
tasks=tasks,
688+
task_idx=task_idx,
679689
img=img,
680690
*args,
681691
**kwargs,
682692
)
683-
response_dict[agent_name] = result
693+
694+
# Use indexed key to preserve all outputs
695+
# from repeated agents (e.g., "Writer_0", "Writer_2")
696+
if agent_name in response_dict:
697+
response_dict[
698+
f"{agent_name}_{task_idx}"
699+
] = result
700+
else:
701+
response_dict[agent_name] = result
684702

685703
loop_count += 1
686704

tests/structs/test_agent_rearrange.py

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -597,6 +597,216 @@ def test_successful_callable_returns_result():
597597
assert result is not None
598598

599599

600+
# ============================================================================
601+
# Repeated Agent Flow Tests
602+
# ============================================================================
603+
604+
605+
def create_repeated_flow_agents():
606+
"""Create agents for repeated flow testing."""
607+
return [
608+
Agent(
609+
agent_name="Writer",
610+
agent_description="Expert in writing content",
611+
system_prompt="You are a writer. Write one concise sentence.",
612+
model_name="gpt-4o-mini",
613+
max_loops=1,
614+
verbose=False,
615+
),
616+
Agent(
617+
agent_name="Reviewer",
618+
agent_description="Expert in reviewing content",
619+
system_prompt="You are a reviewer. Give one critique.",
620+
model_name="gpt-4o-mini",
621+
max_loops=1,
622+
verbose=False,
623+
),
624+
]
625+
626+
627+
def test_repeated_agent_flow_valid():
628+
"""Test that flows with repeated agents pass validation."""
629+
agents = create_repeated_flow_agents()
630+
631+
agent_rearrange = AgentRearrange(
632+
agents=agents,
633+
flow="Writer -> Reviewer -> Writer",
634+
)
635+
636+
assert agent_rearrange.validate_flow() is True
637+
print("✓ test_repeated_agent_flow_valid passed")
638+
639+
640+
def test_repeated_agent_awareness_position_0():
641+
"""Test that the first occurrence of a repeated agent gets correct awareness."""
642+
agents = create_repeated_flow_agents()
643+
644+
agent_rearrange = AgentRearrange(
645+
agents=agents,
646+
flow="Writer -> Reviewer -> Writer",
647+
)
648+
649+
tasks = agent_rearrange.flow.split("->")
650+
awareness = agent_rearrange._get_sequential_awareness(
651+
"Writer", tasks, task_idx=0
652+
)
653+
654+
# Writer at position 0: no agent ahead, Reviewer behind
655+
assert "Agent behind" in awareness
656+
assert "Reviewer" in awareness
657+
assert "Agent ahead" not in awareness
658+
print("✓ test_repeated_agent_awareness_position_0 passed")
659+
660+
661+
def test_repeated_agent_awareness_position_2():
662+
"""Test that the second occurrence of a repeated agent gets correct awareness."""
663+
agents = create_repeated_flow_agents()
664+
665+
agent_rearrange = AgentRearrange(
666+
agents=agents,
667+
flow="Writer -> Reviewer -> Writer",
668+
)
669+
670+
tasks = agent_rearrange.flow.split("->")
671+
awareness = agent_rearrange._get_sequential_awareness(
672+
"Writer", tasks, task_idx=2
673+
)
674+
675+
# Writer at position 2: Reviewer ahead, no agent behind
676+
assert "Agent ahead" in awareness
677+
assert "Reviewer" in awareness
678+
assert "Agent behind" not in awareness
679+
print("✓ test_repeated_agent_awareness_position_2 passed")
680+
681+
682+
def test_repeated_agent_awareness_differs_per_position():
683+
"""Test that each occurrence of a repeated agent gets different awareness."""
684+
agents = create_repeated_flow_agents()
685+
686+
agent_rearrange = AgentRearrange(
687+
agents=agents,
688+
flow="Writer -> Reviewer -> Writer",
689+
)
690+
691+
tasks = agent_rearrange.flow.split("->")
692+
awareness_0 = agent_rearrange._get_sequential_awareness(
693+
"Writer", tasks, task_idx=0
694+
)
695+
awareness_2 = agent_rearrange._get_sequential_awareness(
696+
"Writer", tasks, task_idx=2
697+
)
698+
699+
assert awareness_0 != awareness_2
700+
print("✓ test_repeated_agent_awareness_differs_per_position passed")
701+
702+
703+
def test_repeated_agent_awareness_fallback_without_idx():
704+
"""Test that awareness still works when task_idx is not provided (backward compat)."""
705+
agents = create_repeated_flow_agents()
706+
707+
agent_rearrange = AgentRearrange(
708+
agents=agents,
709+
flow="Writer -> Reviewer -> Writer",
710+
)
711+
712+
tasks = agent_rearrange.flow.split("->")
713+
# Without task_idx, falls back to finding first occurrence
714+
awareness = agent_rearrange._get_sequential_awareness(
715+
"Writer", tasks
716+
)
717+
718+
assert awareness is not None
719+
assert isinstance(awareness, str)
720+
assert "Sequential awareness" in awareness
721+
print("✓ test_repeated_agent_awareness_fallback_without_idx passed")
722+
723+
724+
def test_repeated_agent_three_occurrences():
725+
"""Test awareness correctness with three occurrences of the same agent."""
726+
agents = create_repeated_flow_agents()
727+
728+
agent_rearrange = AgentRearrange(
729+
agents=agents,
730+
flow="Writer -> Reviewer -> Writer -> Reviewer -> Writer",
731+
)
732+
733+
tasks = agent_rearrange.flow.split("->")
734+
735+
# Writer at pos 0: no ahead, Reviewer behind
736+
a0 = agent_rearrange._get_sequential_awareness("Writer", tasks, task_idx=0)
737+
assert "Agent behind" in a0
738+
assert "Agent ahead" not in a0
739+
740+
# Writer at pos 2: Reviewer ahead, Reviewer behind
741+
a2 = agent_rearrange._get_sequential_awareness("Writer", tasks, task_idx=2)
742+
assert "Agent ahead" in a2
743+
assert "Agent behind" in a2
744+
745+
# Writer at pos 4: Reviewer ahead, no behind
746+
a4 = agent_rearrange._get_sequential_awareness("Writer", tasks, task_idx=4)
747+
assert "Agent ahead" in a4
748+
assert "Agent behind" not in a4
749+
750+
print("✓ test_repeated_agent_three_occurrences passed")
751+
752+
753+
def test_repeated_agent_run():
754+
"""Test that a repeated agent flow runs end-to-end."""
755+
agents = create_repeated_flow_agents()
756+
757+
agent_rearrange = AgentRearrange(
758+
name="repeated-flow-test",
759+
agents=agents,
760+
flow="Writer -> Reviewer -> Writer",
761+
max_loops=1,
762+
)
763+
764+
result = agent_rearrange.run("Write about the moon.")
765+
assert result is not None
766+
assert len(str(result)) > 0
767+
768+
# Verify Writer appears twice in conversation
769+
messages = agent_rearrange.conversation.to_dict()
770+
writer_msgs = [m for m in messages if m.get("role") == "Writer"]
771+
assert len(writer_msgs) == 2, (
772+
f"Expected 2 Writer messages, got {len(writer_msgs)}"
773+
)
774+
775+
print("✓ test_repeated_agent_run passed")
776+
777+
778+
def test_repeated_agent_awareness_in_conversation():
779+
"""Test that different awareness messages are injected for each occurrence."""
780+
agents = create_repeated_flow_agents()
781+
782+
agent_rearrange = AgentRearrange(
783+
name="awareness-conv-test",
784+
agents=agents,
785+
flow="Writer -> Reviewer -> Writer",
786+
max_loops=1,
787+
)
788+
789+
agent_rearrange.run("Write about rain.")
790+
791+
messages = agent_rearrange.conversation.to_dict()
792+
793+
# Find awareness messages that precede Writer messages
794+
writer_awareness = []
795+
for idx, msg in enumerate(messages):
796+
if "Sequential awareness" in str(msg.get("content", "")):
797+
if idx + 1 < len(messages) and messages[idx + 1].get("role") == "Writer":
798+
writer_awareness.append(msg.get("content", ""))
799+
800+
assert len(writer_awareness) == 2, (
801+
f"Expected 2 awareness messages before Writer, got {len(writer_awareness)}"
802+
)
803+
assert writer_awareness[0] != writer_awareness[1], (
804+
"Both Writer invocations got identical awareness"
805+
)
806+
807+
print("✓ test_repeated_agent_awareness_in_conversation passed")
808+
809+
600810
def main():
601811
"""Run all tests."""
602812
tests = [
@@ -629,6 +839,14 @@ def main():
629839
test_error_logged_once,
630840
test_successful_run_returns_result,
631841
test_successful_callable_returns_result,
842+
test_repeated_agent_flow_valid,
843+
test_repeated_agent_awareness_position_0,
844+
test_repeated_agent_awareness_position_2,
845+
test_repeated_agent_awareness_differs_per_position,
846+
test_repeated_agent_awareness_fallback_without_idx,
847+
test_repeated_agent_three_occurrences,
848+
test_repeated_agent_run,
849+
test_repeated_agent_awareness_in_conversation,
632850
]
633851

634852
print("=" * 60)

0 commit comments

Comments
 (0)