Skip to content

[Bug] arun() binds a third positional argument to run()'s imgs parameter - #1879

Open
ayaangazali wants to merge 3 commits into
kyegomez:masterfrom
ayaangazali:fix/arun-cannot-forward-positionals
Open

[Bug] arun() binds a third positional argument to run()'s imgs parameter#1879
ayaangazali wants to merge 3 commits into
kyegomez:masterfrom
ayaangazali:fix/arun-cannot-forward-positionals

Conversation

@ayaangazali

@ayaangazali ayaangazali commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

What

Agent.arun() and Agent.__call__() both declare *args that they cannot forward to run().

run()'s signature is:

def run(self, task=None, img=None, imgs=None, correct_answer=None,
        streaming_callback=None, n=1, *args, **kwargs)

There are four parameters between img and run()'s own *args. Nothing splatted after task and img can get past them.

arun() splats anyway:

return await asyncio.to_thread(self.run, task, img, *args, **kwargs)

so a third positional lands on imgs:

arun("Summarize", "chart.png", "EXTRA")
  -> run(task="Summarize", img="chart.png", imgs="EXTRA")

imgs expects List[str] and is iterated as one, so "EXTRA" is consumed a character at a time. The call does not fail — it runs with the wrong argument in the wrong slot.

__call__() has the same dead *args but passes task=/img= as keywords beside it, so it raises TypeError: run() got multiple values for argument 'task' as soon as *args is non-empty. Loud rather than silent, but equally unreachable.

Why this way

I introduced the arun half of this in #1871 while fixing that same TypeError. Making the forward positional removed the exception but did not make *args reachable — it only moved the extras onto the four named parameters in between. Deleting the parameter is the honest fix: it never carried a value under either version.

Nothing that worked stops working. Extra positionals could not reach run() before this change either, and every parameter they would have targeted (imgs, correct_answer, streaming_callback, n) is still reachable by name through **kwargs.

Tests

Appended to the existing TestArunForwarding class in tests/structs/test_agent.py.

The previous test used a fake_run(*args, **kwargs) stub, which accepts any binding and so could not observe the misbind. The replacement mirrors run()'s real parameter names, and a second test pins that a third positional is now rejected rather than silently rebound — that one fails on master with DID NOT RAISE TypeError.

Both live in tests/structs/test_agent.py, the file this diff already touches. Verified by swapping only swarms/structs/agent.py for master's copy:

FAILED TestArunForwarding::test_a_third_positional_is_refused_not_bound_to_imgs
1 failed, 2 passed

and the whole file is master=27 branch=27 IDENTICAL FAILURE SET against a clean master worktree.

An earlier revision of this PR dropped the test file and this section said so. That is no longer true — the tests are back, because the merged test from #1871 asserted the mis-bind was correct and had to be replaced rather than deleted.

@ayaangazali
ayaangazali requested a review from kyegomez as a code owner August 12, 2026 09:18
Copilot AI lite review requested due to automatic review settings August 12, 2026 09:18

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions

Copy link
Copy Markdown

Hello there, thank you for opening an PR ! 🙏🏻 The team was notified and they will get back to you asap.

@ayaangazali

Copy link
Copy Markdown
Contributor Author

Rebased onto 07f3bd39 (was 25 behind), and pushed a test change that this PR needed and did not have.

The short version: #1871 is mine, it made this worse, and it shipped a test that locks the defect in.

#1871 (merged 2026-08-11) fixed a real TypeError: run() got multiple values for argument 'task' by switching arun to forward positionally:

            return await asyncio.to_thread(
                self.run, task, img, *args, **kwargs,
            )

That removed the exception and replaced it with a silent mis-bind, which is worse. run()'s positionals after img are imgs, correct_answer, streaming_callback, n — so a third positional never reaches run()'s own *args:

>>> inspect.signature(Agent.run).bind_partial(None, "T", "I", "EXTRA").arguments
{'task': 'T', 'img': 'I', 'imgs': 'EXTRA'}

imgs is a List[str] of image paths. arun(task, img, extra) now quietly hands extra to the image pipeline instead of raising.

Why nothing caught it. #1871's test asserts the mis-bind is correct:

        def fake_run(*args, **kwargs):
            ...
        result = asyncio.run(Agent.arun(agent, "T", "I", "EXTRA"))
        assert seen["args"] == ("T", "I", "EXTRA")

A stub declared (*args, **kwargs) accepts anything positionally, so it cannot see where the argument would really land. The assertion is true of the stub and meaningless against Agent.run.

What changed here. That test is replaced with two that use a stub carrying the real parameter names, so a mis-bind is observable:

  • test_task_and_img_reach_run_with_kwargs — the forwarding arun can actually do
  • test_a_third_positional_is_refused_not_bound_to_imgs — a third positional raises TypeError at the arun boundary

The second fails against master's agent.py and passes here; I checked by swapping just that file rather than assuming.

tests/structs/test_agent.py -k ArunForwarding    3 passed
full file: master=27  branch=27                  IDENTICAL FAILURE SET
black==24.2.0 --check .                          clean
ruff==0.2.1 check .                              clean

I found this by diffing the failure set against a clean master worktree — test is red on 07f3bd39 itself, so CI would not have shown it either.

@ayaangazali
ayaangazali force-pushed the fix/arun-cannot-forward-positionals branch from 8e8025a to efa5203 Compare August 23, 2026 06:37
@ayaangazali

Copy link
Copy Markdown
Contributor Author

Correction to my previous comment: the push it described also carried two files that have nothing to do with this PR — swarms/structs/conversation.py and a stray conversation_conversation-test.json. My mistake, from a bad git stash on my side. Both are stripped and force-pushed; the branch is now exactly:

swarms/structs/agent.py
tests/structs/test_agent.py

Re-verified after stripping them: black==24.2.0 --check . and ruff==0.2.1 check . clean, and tests/structs/test_agent.py is master=27 branch=27 IDENTICAL FAILURE SET against a clean master worktree. Everything in the previous comment about the actual fix still stands.

ayaangazali and others added 3 commits August 26, 2026 18:14
`run()`'s parameters after `img` are `imgs`, `correct_answer`,
`streaming_callback` and `n`, followed by its own `*args`. There is no
way to splat a caller's extra positionals past those four and reach
`*args`, so the `*args` that `arun()` and `__call__()` declare could
never be forwarded.

`arun()` splatted them anyway, which meant `arun(task, img, extra)`
bound `extra` to `imgs` — a parameter that expects a list of image
paths — and ran with the wrong value in the wrong slot instead of
failing. `__call__()` passes `task=`/`img=` as keywords alongside the
same splat, so it raises `TypeError: run() got multiple values for
argument 'task'` the moment `*args` is non-empty.

Drop the dead `*args` from both signatures and forward by keyword. No
caller loses anything: extra positionals could not reach `run()` before
this either, and every parameter they were reaching for is available by
name through `**kwargs`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
test_extra_positional_args_reach_run came in with kyegomez#1871 and asserts that
arun(task, img, "EXTRA") forwards "EXTRA" positionally to run(). That is
the behaviour this PR removes, so the test locked the defect in.

It only ever passed because its stub is declared
`fake_run(*args, **kwargs)`, which accepts anything positionally and so
cannot observe where the argument actually lands. Against the real
signature it lands on `imgs`:

    >>> inspect.signature(Agent.run).bind_partial(None, "T", "I", "EXTRA")
    {'task': 'T', 'img': 'I', 'imgs': 'EXTRA'}

`imgs` is a List[str] of image paths, not run()'s own *args.

Replaced with two tests that use a stub with the real parameter names,
so a mis-bind is visible:

  - task/img/kwargs reach run() as themselves
  - a third positional raises TypeError at the arun boundary instead of
    being silently bound to imgs

The second fails on master's agent.py and passes here, which is the
whole point of the change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ayaangazali
ayaangazali force-pushed the fix/arun-cannot-forward-positionals branch from efa5203 to 65df20d Compare August 27, 2026 01:15
@ayaangazali

Copy link
Copy Markdown
Contributor Author

Rebased onto master, which now carries your own fix for this in arun/__call__. Flagging why this PR is still open rather than closing it as superseded, because the two fixes disagree and the difference is testable.

Master forwards positionally. On master today:

def real_shaped_run(task=None, img=None, imgs=None, correct_answer=None,
                    streaming_callback=None, n=None, *args, **kwargs): ...
agent.run = real_shaped_run
asyncio.run(Agent.arun(agent, "T", "I", "EXTRA"))
EXTRA landed on: [imgs]
run()'s own *args: ()

The TypeError: got multiple values for argument 'task' is gone, but the third positional is now silently bound to imgs instead of reaching run()'s *args. run() has four parameters between img and its *argsimgs, correct_answer, streaming_callback, n — so positional forwarding lands extras on those before it can ever reach *args.

test_extra_positional_args_reach_run passes anyway because its stub is def fake_run(*args, **kwargs), a catch-all that absorbs every binding. seen["args"] == ("T", "I", "EXTRA") is true against that stub and false against run()'s real signature.

So I replaced that test rather than keeping both: it asserts the binding this PR removes, and it cannot fail on the misbind it was written to catch. The replacement stubs run()'s real parameter list so a misbind shows up as a wrong parameter name.

This PR drops *args from arun/__call__ and forwards task/img by keyword plus **kwargs. An extra positional then raises immediately:

TypeError: Agent.arun() takes from 1 to 3 positional arguments but 4 were given

That is a deliberate trade: *args on these two forwarders could never carry a value to run()'s own *args under either scheme, so the signature was promising something it could not keep. Same reasoning as #1891 and #1893.

tests/structs/test_agent.py: 89 passed. Happy to close this instead if you would rather keep the positional path and accept the imgs binding.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants