Skip to content

fix(multi-agent-router): record every selected agent's response (#2044) - #2076

Open
Steve-Dusty wants to merge 1 commit into
kyegomez:masterfrom
Steve-Dusty:fix/multi-agent-router-record-all-2044
Open

fix(multi-agent-router): record every selected agent's response (#2044)#2076
Steve-Dusty wants to merge 1 commit into
kyegomez:masterfrom
Steve-Dusty:fix/multi-agent-router-record-all-2044

Conversation

@Steve-Dusty

Copy link
Copy Markdown
Contributor

Closes #2044.

MultiAgentRouter.handle_multiple_handoffs ran every agent the boss selected and then recorded only the first one's answer. Whenever the boss selected more than one agent, the caller paid for N completions and received one, with nothing logged or warned about the rest.

Problem

swarms/structs/multi_agent_router.py:371-386:

        # Execute agents only if there are valid tasks
        if selected_agents:
            # Use the agents' run method directly
            agent_responses = [
                agent.run(final_task)
                for agent, final_task in zip(
                    selected_agents, final_tasks
                )
            ]

            self.conversation.add(
                role=selected_agents[0].agent_name,
                content=agent_responses[0],
            )

        # return agent_responses

agent_responses[1:] is never read. Two consequences:

  • Spend is silently discarded. Each dropped agent has already made a completion call by the time the list comprehension finishes.
  • It is invisible. Nothing distinguishes "the boss picked one agent" from "the boss picked four and three answers were thrown away" — the conversation looks identical.

The dangling # return agent_responses on the following line is the remnant of the refactor that left it this way; the method's own docstring said "record the first response", so the code and its documentation agreed on the wrong behavior.

Fix

            for agent, agent_response in zip(
                selected_agents, agent_responses
            ):
                self.conversation.add(
                    role=agent.agent_name, content=agent_response
                )

Every response is recorded under its own agent's name, in the order the agents were run. The docstring is corrected to match, and the commented-out return — which described exactly the discarded values — is removed.

Files

File Change
swarms/structs/multi_agent_router.py record all responses; docstring corrected; dead commented-out return removed
tests/structs/test_multi_agent_router.py 7 tests appended to the existing file

Design decisions

  • Record all, rather than run one. The issue notes the alternative: if returning a single answer is intended, only one agent should be run. That would be a behavior change to the boss contract — handoffs is a list and the boss prompt actively invites multiple entries — so the conservative reading is that running all of them was intended and the recording was the half left unfinished.
  • Attribute each message to its own agent. Recording all four answers under selected_agents[0].agent_name would have been a smaller diff and is wrong: a downstream reader filtering the conversation by agent would attribute three agents' work to the first.
  • Order follows execution order. zip(selected_agents, agent_responses) reuses the list built alongside final_tasks, so the recorded order matches the order the agents actually ran, not the boss's original list — which can differ once skip_null_tasks drops entries.

Deliberately not in this PR

The issue lists two neighbours in the same file. Both are left alone, each its own concern:

  • Raw transcripts recorded rather than answers (:301-305 has the same shape) — Agent.run honours output_type, defaulting to "str-all-except-first". Fixing that means routing through context_utils.agent_answer at both sites and changes what every existing caller reads out of the conversation.
  • concurrent_batch_run races on one shared Conversation (:485) — that is the per-task conversation isolation work, not this.

Behavior-change note

Callers reading self.conversation after a multi-agent handoff now see one message per agent instead of one message total. Anything that indexed conversation_history[-1] expecting the sole handoff result will now get the last agent's answer rather than the first's. Single-agent handoffs, skip_null_tasks skipping, and the up-front unknown-agent validation are all unchanged — each is covered by a test below.

Verification

  • Unit: 7 tests appended to the existing tests/structs/test_multi_agent_router.py. No new test file.

    $ .venv/bin/python -m pytest tests/structs/test_multi_agent_router.py -q \
        -k "every_selected or run_and_then_discarded or own_agent_name or \
            single_handoff_is_unchanged or skipped_agents or falls_back or \
            unknown_agent_is_rejected"
    7 passed, 24 deselected in 2.39s
    

    Covered: all three answers kept; one message per agent run; each recorded under its own name; a single handoff unchanged; skipped null-task agents contribute nothing; a handoff with no rewritten task falls back to the original; an unknown agent raises before anything runs and leaves the conversation empty.

  • The tests fail on the unfixed base. Same tests against upstream/master's multi_agent_router.py: 4 failed, 3 passed. The four that fail are exactly the multi-agent ones; the three that pass are the single-agent and validation guards, which is the point — they prove the fix did not change those paths.

  • The test file, before and after:

    passed failed skipped
    clean upstream/master @ ff8a60e0 (git worktree) 1 22 1
    this branch 8 22 1
  • Full tests/structs suite, both runs on this machine, same interpreter:

    passed failed skipped xfailed errors
    clean upstream/master (git worktree) 866 112 23 3 5
    this branch 873 112 23 3 5

    Identical failure and error sets; the +7 is exactly this PR's tests. The 112 pre-existing failures — including the 22 in this very file — are live-LLM tests that construct real Agents and need an API key; they fail identically on the clean base.

  • Lint: black --check clean on both files; ruff check with the CI rule set (ruff==0.2.1 defaults, per .github/workflows/lint.yml) reports no findings on either.

  • Not verified here: no live-LLM path was exercised. The new tests build the router with MultiAgentRouter.__new__ and agents via Agent.__new__, setting only the attributes handle_multiple_handoffs reads (agents, skip_null_tasks, print_on, conversation), so no model is contacted and no API key is needed. The boss call itself is not exercised — these tests drive handle_multiple_handoffs with a boss response directly, which is the method the defect lives in.

@kyegomez

Copy link
Copy Markdown
Owner

Verified this independently rather than taking the write-up on faith — it all holds. Approving.

Two things your description understates, both of which strengthen the case:

The discarded answers never reached the caller at all. route_task ignores this method's return value and returns history_output_formatter(conversation=self.conversation, ...) (:419). The conversation is the output, so agent_responses[1:] wasn't just unlogged — it was deleted before anything downstream could read it.

It fired on every invocation. :414 only routes here when len(handoffs) > 1; single handoffs go to handle_single_handoff. So there was no input for which this method was correct.

Details you got right that a smaller patch would have missed: attributing each message to its own agent rather than recording all N under selected_agents[0], and iterating selected_agents rather than the boss's original list, so the recorded order matches what actually ran once skip_null_tasks has dropped entries.

Verification on my side, from detached worktrees, run offline:

new tests against unpatched master:  4 failed, 3 passed
master baseline:                     22 failed, 1 passed, 1 skipped
this branch:                         22 failed, 8 passed, 1 skipped

Identical failure sets — zero introduced. The 4/3 split on the unpatched base is the useful part: the four multi-agent tests fail and the single-handoff and unknown-agent guards pass, which is what demonstrates those paths are untouched. black --line-length 70 and ruff clean on both files.

Two non-blocking nits, either here or as a follow-up:

  1. The -> dict annotation is still wrong. The method returns None implicitly, and the docstring now says so explicitly ("Currently returns None implicitly... Kept as dict in the signature for parity"). A docstring that documents an incorrect annotation is worse than either alone — since you're already in this docstring, -> None would settle it.
  2. handle_single_handoff still has the sibling dangling comment# return agent_response at :307. You removed the one in handle_multiple_handoffs as a remnant of the same refactor; leaving its twin in place makes the next reader wonder whether one of them meant something.

Scoping out the raw-transcript issue at :301-305 and the concurrent_batch_run conversation race at :485 was the right call — each has its own blast radius.

…omez#2044)

handle_multiple_handoffs runs every agent the boss selected, then writes
only the first one's answer into the conversation:

    agent_responses = list(
        executor.map(
            lambda pair: pair[0].run(pair[1]),
            zip(selected_agents, final_tasks),
        )
    )

    self.conversation.add(
        role=selected_agents[0].agent_name,
        content=agent_responses[0],
    )

agent_responses[1:] is never read. Whenever the boss selects more than
one agent, the caller pays for N completions and receives one, and
nothing logs or warns that the rest were dropped -- it looks like the
router simply chose a single agent. The dangling
`# return agent_responses` on the next line was the remnant of the
refactor that left it this way.

Every response is now recorded under its own agent's name, in the order
the agents were dispatched. The docstring said "record the first
response" and is corrected to match.

Removed the commented-out return, which described the discarded values.

Behavior note: callers reading self.conversation after a multi-agent
handoff now see one message per agent instead of one message total.
Single-agent handoffs, skipped null tasks, and the up-front
unknown-agent validation are unchanged.

Closes kyegomez#2044
@Steve-Dusty
Steve-Dusty force-pushed the fix/multi-agent-router-record-all-2044 branch from 5783d86 to ddf9306 Compare August 29, 2026 02:57
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.

[BUG][MultiAgentRouter][Only the first selected agent response is recorded; the rest are run, paid for, and discarded]

2 participants