[Bug] arun() binds a third positional argument to run()'s imgs parameter - #1879
[Bug] arun() binds a third positional argument to run()'s imgs parameter#1879ayaangazali wants to merge 3 commits into
Conversation
|
Hello there, thank you for opening an PR ! 🙏🏻 The team was notified and they will get back to you asap. |
4efbdcb to
7eee04f
Compare
7eee04f to
ee1ab58
Compare
f6ec3d5 to
8e8025a
Compare
|
Rebased onto 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 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.
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 What changed here. That test is replaced with two that use a stub carrying the real parameter names, so a mis-bind is observable:
The second fails against master's I found this by diffing the failure set against a clean master worktree — |
8e8025a to
efa5203
Compare
|
Correction to my previous comment: the push it described also carried two files that have nothing to do with this PR — Re-verified after stripping them: |
`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>
efa5203 to
65df20d
Compare
|
Rebased onto master, which now carries your own fix for this in 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"))The
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 This PR drops That is a deliberate trade:
|
What
Agent.arun()andAgent.__call__()both declare*argsthat they cannot forward torun().run()'s signature is:There are four parameters between
imgandrun()'s own*args. Nothing splatted aftertaskandimgcan get past them.arun()splats anyway:so a third positional lands on
imgs:imgsexpectsList[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*argsbut passestask=/img=as keywords beside it, so it raisesTypeError: run() got multiple values for argument 'task'as soon as*argsis non-empty. Loud rather than silent, but equally unreachable.Why this way
I introduced the
arunhalf of this in #1871 while fixing that sameTypeError. Making the forward positional removed the exception but did not make*argsreachable — 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
TestArunForwardingclass intests/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 mirrorsrun()'s real parameter names, and a second test pins that a third positional is now rejected rather than silently rebound — that one fails onmasterwithDID NOT RAISE TypeError.Both live in
tests/structs/test_agent.py, the file this diff already touches. Verified by swapping onlyswarms/structs/agent.pyfor master's copy:and the whole file is
master=27 branch=27 IDENTICAL FAILURE SETagainst a clean master worktree.