Skip to content

Commit c76130b

Browse files
ayaangazaliclaude
andcommitted
[bugf][multi-agent-router][hybrid-swarm][give each task its own Conversation instead of sharing one across threads (kyegomez#2054)]
MultiAgentRouter and HybridHierarchicalClusterSwarm both built one Conversation in __init__ and wrote to it from run()/route_task(). Both also fan those same methods across a thread pool - hybrid_hiearchical_peer_swarm.py:212 via run_concurrently(self.run, tasks), multi_agent_router.py:485 via executor.submit(self.route_task, task) - so N threads appended to one object and each caller got every task's messages back, not its own. Demonstrated with the same shape in isolation, 8 concurrent callers: shared instance -> [1, 2, 3, 4, 5, 6, 7, 8] per-call -> [1, 1, 1, 1, 1, 1, 1, 1] Resetting the shared Conversation at the top of run() would fix sequential reuse but not this: concurrent callers would still race on the reset. The conversation is per-task state, so it is now constructed in the per-task entry point and threaded explicitly into the helpers that record into it. That also removes the sequential leak, since nothing survives the call. self.conversation is gone from both classes rather than left vestigial; no caller in the repo read it, and leaving it would be a shared object that looks live but is never written. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent a193a8e commit c76130b

2 files changed

Lines changed: 33 additions & 18 deletions

File tree

swarms/structs/hybrid_hiearchical_peer_swarm.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,6 @@ def __init__(
118118
self.max_loops = max_loops
119119
self.output_type = output_type
120120

121-
self.conversation = Conversation()
122-
123121
self.router_agent = Agent(
124122
agent_name="Router Agent",
125123
agent_description="A router agent that routes tasks to the appropriate swarms.",
@@ -146,7 +144,8 @@ def run(self, task: str, *args, **kwargs):
146144
if not task:
147145
raise ValueError("Task cannot be empty.")
148146

149-
self.conversation.add(role="User", content=task)
147+
conversation = Conversation()
148+
conversation.add(role="User", content=task)
150149

151150
response = self.router_agent.run(task=task)
152151

@@ -167,10 +166,10 @@ def run(self, task: str, *args, **kwargs):
167166
f"Please check the response format from the model: {self.router_agent.model_name}."
168167
)
169168

170-
self.route_task(swarm_name, task_description)
169+
self.route_task(swarm_name, task_description, conversation)
171170

172171
return history_output_formatter(
173-
self.conversation, self.output_type
172+
conversation, self.output_type
174173
)
175174

176175
def find_swarm_by_name(self, swarm_name: str):
@@ -188,13 +187,19 @@ def find_swarm_by_name(self, swarm_name: str):
188187
return swarm
189188
return None
190189

191-
def route_task(self, swarm_name: str, task_description: str):
190+
def route_task(
191+
self,
192+
swarm_name: str,
193+
task_description: str,
194+
conversation: Conversation,
195+
):
192196
"""
193197
Routes the task to the specified swarm.
194198
195199
Args:
196200
swarm_name (str): The name of the swarm to route the task to.
197201
task_description (str): The description of the task to be executed.
202+
conversation (Conversation): The conversation to record the output in.
198203
199204
Raises:
200205
ValueError: If the swarm is not found.
@@ -203,7 +208,7 @@ def route_task(self, swarm_name: str, task_description: str):
203208

204209
if swarm:
205210
output = swarm.run(task_description)
206-
self.conversation.add(role=swarm.name, content=output)
211+
conversation.add(role=swarm.name, content=output)
207212
else:
208213
raise ValueError(f"Swarm '{swarm_name}' not found.")
209214

swarms/structs/multi_agent_router.py

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,6 @@ def __init__(
182182
self.system_prompt = system_prompt
183183
self.skip_null_tasks = skip_null_tasks
184184
self.agents = list(agents) if agents else []
185-
self.conversation = Conversation()
186185

187186
router_system_prompt = ""
188187

@@ -249,15 +248,18 @@ def _create_boss_system_prompt(self) -> str:
249248
return agent_boss_router_prompt(agent_descriptions)
250249

251250
def handle_single_handoff(
252-
self, boss_response_str: dict, task: str
251+
self,
252+
boss_response_str: dict,
253+
task: str,
254+
conversation: Conversation,
253255
) -> dict:
254256
"""
255257
Execute the single agent selected by the boss and record its response.
256258
257259
Looks up the agent named in the first (and only) handoff, runs it on
258260
the modified task (falling back to the original task if the boss did
259261
not rewrite it), and appends the agent's response to
260-
``self.conversation``. When ``skip_null_tasks`` is True and the
262+
``conversation``. When ``skip_null_tasks`` is True and the
261263
resolved task is empty or ``None``, execution is skipped.
262264
263265
Args:
@@ -300,14 +302,17 @@ def handle_single_handoff(
300302
# Use the agent's run method directly
301303
agent_response = selected_agent.run(final_task)
302304

303-
self.conversation.add(
305+
conversation.add(
304306
role=selected_agent.agent_name, content=agent_response
305307
)
306308

307309
# return agent_response
308310

309311
def handle_multiple_handoffs(
310-
self, boss_response_str: dict, task: str
312+
self,
313+
boss_response_str: dict,
314+
task: str,
315+
conversation: Conversation,
311316
) -> dict:
312317
"""
313318
Execute every agent selected by the boss and record the first response.
@@ -317,7 +322,7 @@ def handle_multiple_handoffs(
317322
task (or the original ``task`` when the boss did not rewrite it). When
318323
``skip_null_tasks`` is True, agents whose resolved task is empty or
319324
``None`` are skipped. After execution, the first selected agent's
320-
response is appended to ``self.conversation``.
325+
response is appended to ``conversation``.
321326
322327
Args:
323328
boss_response_str (dict): Parsed boss decision containing a
@@ -380,7 +385,7 @@ def handle_multiple_handoffs(
380385
)
381386
]
382387

383-
self.conversation.add(
388+
conversation.add(
384389
role=selected_agents[0].agent_name,
385390
content=agent_responses[0],
386391
)
@@ -398,7 +403,8 @@ def route_task(self, task: str) -> dict:
398403
dict: A dictionary containing the routing result, including the selected agent, reasoning, and response.
399404
"""
400405
try:
401-
self.conversation.add(role="user", content=task)
406+
conversation = Conversation()
407+
conversation.add(role="user", content=task)
402408

403409
# Get boss decision using function calling
404410
boss_response_str = self.function_caller.run(task)
@@ -412,12 +418,16 @@ def route_task(self, task: str) -> dict:
412418
)
413419

414420
if len(boss_response_str["handoffs"]) > 1:
415-
self.handle_multiple_handoffs(boss_response_str, task)
421+
self.handle_multiple_handoffs(
422+
boss_response_str, task, conversation
423+
)
416424
else:
417-
self.handle_single_handoff(boss_response_str, task)
425+
self.handle_single_handoff(
426+
boss_response_str, task, conversation
427+
)
418428

419429
return history_output_formatter(
420-
conversation=self.conversation, type=self.output_type
430+
conversation=conversation, type=self.output_type
421431
)
422432

423433
except Exception as e:

0 commit comments

Comments
 (0)