Skip to content

Commit e77e578

Browse files
ayaangazaliclaude
andcommitted
[bugf][agent][run_batched drops every task when imgs is omitted]
`run_batched` zips the tasks against `imgs` using `imgs` as the loop variable: return [ self.run(task=task, imgs=imgs, *args, **kwargs) for task, imgs in zip(tasks, imgs) ] Three problems in four lines: 1. `imgs` defaults to None and is documented as optional, but `zip(tasks, None)` raises `TypeError: 'NoneType' object is not iterable` — so the documented basic call `agent.run_batched(["a", "b"])` never runs a single task. 2. The loop variable rebinds the parameter, so each `self.run` call receives one image string in `imgs`, a field declared `List[str]` and passed straight through to the provider call. The single-image parameter is `img`. 3. Unequal lengths zip to the shorter one, so passing fewer images than tasks silently discards tasks rather than reporting the mismatch. Now: no images runs the tasks plainly, paired images go through `img`, and a length mismatch raises rather than dropping work. The docstring said "concurrently" while the body was always a list comprehension; it now says what it does. Making it actually concurrent is a behaviour change and belongs in its own PR. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 9d8f6ef commit e77e578

2 files changed

Lines changed: 59 additions & 4 deletions

File tree

swarms/structs/agent.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3349,20 +3349,36 @@ def run_batched(
33493349
**kwargs,
33503350
):
33513351
"""
3352-
Run a batch of tasks concurrently.
3352+
Run a batch of tasks, one after another.
33533353
33543354
Args:
33553355
tasks (List[str]): List of tasks to run.
3356-
imgs (List[str], optional): List of images to run. Defaults to None.
3356+
imgs (List[str], optional): One image per task, paired by position.
3357+
Omit to run the tasks without images. Defaults to None.
33573358
*args: Additional positional arguments to be passed to the execution method.
33583359
**kwargs: Additional keyword arguments to be passed to the execution method.
33593360
33603361
Returns:
33613362
List[Any]: List of results from each task execution.
33623363
"""
3364+
# `for task, imgs in zip(...)` rebound the parameter to one image per
3365+
# iteration, so a List[str] field received a bare str -- and with the
3366+
# documented default of imgs=None the zip raised before any task ran.
3367+
if imgs is None:
3368+
return [
3369+
self.run(task=task, *args, **kwargs) for task in tasks
3370+
]
3371+
3372+
if len(imgs) != len(tasks):
3373+
raise ValueError(
3374+
f"run_batched got {len(tasks)} tasks and {len(imgs)} images; "
3375+
"pass one image per task, or omit imgs entirely. Zipping them "
3376+
"would silently drop the extras."
3377+
)
3378+
33633379
return [
3364-
self.run(task=task, imgs=imgs, *args, **kwargs)
3365-
for task, imgs in zip(tasks, imgs)
3380+
self.run(task=task, img=img, *args, **kwargs)
3381+
for task, img in zip(tasks, imgs)
33663382
]
33673383

33683384
def showcase_config(self):

tests/structs/test_agent.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3170,3 +3170,42 @@ def test_pool_is_shut_down_after_each_call(self):
31703170
break
31713171
time.sleep(0.02)
31723172
assert threading.active_count() <= before
3173+
3174+
3175+
class TestRunBatchedImagePairing:
3176+
"""`for task, imgs in zip(tasks, imgs)` rebound the parameter to a single
3177+
image, so the List[str] `imgs` field received a bare str — and with the
3178+
documented default of imgs=None the zip raised before any task ran.
3179+
"""
3180+
3181+
@staticmethod
3182+
def _agent():
3183+
agent = Agent.__new__(Agent)
3184+
agent.agent_name = "batched"
3185+
return agent
3186+
3187+
def test_tasks_without_images_run(self):
3188+
"""The documented default: imgs is optional."""
3189+
agent = self._agent()
3190+
with patch.object(Agent, "run", side_effect=lambda **kw: kw):
3191+
assert Agent.run_batched(agent, ["t1", "t2"]) == [
3192+
{"task": "t1"},
3193+
{"task": "t2"},
3194+
]
3195+
3196+
def test_each_task_gets_its_own_image_as_a_single_image(self):
3197+
agent = self._agent()
3198+
with patch.object(Agent, "run", side_effect=lambda **kw: kw):
3199+
assert Agent.run_batched(
3200+
agent, ["t1", "t2"], imgs=["a.png", "b.png"]
3201+
) == [
3202+
{"task": "t1", "img": "a.png"},
3203+
{"task": "t2", "img": "b.png"},
3204+
]
3205+
3206+
def test_mismatched_lengths_raise_instead_of_dropping_tasks(self):
3207+
"""zip() would have run one task and discarded the rest in silence."""
3208+
agent = self._agent()
3209+
with patch.object(Agent, "run", side_effect=lambda **kw: kw):
3210+
with pytest.raises(ValueError, match="one image per task"):
3211+
Agent.run_batched(agent, ["t1", "t2", "t3"], imgs=["a.png"])

0 commit comments

Comments
 (0)