fix(voice): collapse duplicate same-target handoff tool calls#6280
fix(voice): collapse duplicate same-target handoff tool calls#6280georgeglarson wants to merge 1 commit into
Conversation
When a tool call returns an Agent, AgentActivity treats a second AgentTask in the same turn as an ambiguous multi-handoff and aborts the switch (ignore_task_switch=True). Real LLMs routinely emit the same handoff tool call more than once in a single turn (parallel / duplicate tool calls); each returns the same target agent, so the guard discarded the handoff entirely -- the target agent never started and the follow-up tool turn was stranded. Only refuse the switch when the duplicate AgentTask targets a *different* agent (genuinely ambiguous). Identical same-target calls collapse to a single handoff. The same fix is applied to both the pipeline and realtime tool-output loops. Surfaced while investigating livekit#5990; this is an independent correctness fix, not the primary cause of that issue.
2407f81 to
0488fc4
Compare
| # turn (parallel/duplicate tool calls, common with real LLMs). Collapse to | ||
| # a single handoff instead of discarding it (livekit/agents#5990). | ||
|
|
||
| new_agent_task = sanitized_out.agent_task |
There was a problem hiding this comment.
π© Unconditional new_agent_task reassignment can lose a handoff if a later tool returns None
At livekit-agents/livekit/agents/voice/agent_activity.py:3150, new_agent_task = sanitized_out.agent_task runs unconditionally β even when sanitized_out.agent_task is None. If a handoff tool and a regular (non-handoff) tool execute in the same turn, the final value of new_agent_task depends on iteration order: a non-handoff tool processed after the handoff tool silently overwrites it to None, dropping the handoff. This is a pre-existing issue (the line is unchanged by this PR) and would only matter when mixing handoff and non-handoff tools in one parallel turn, but it's a latent footgun.
Was this helpful? React with π or π to provide feedback.
There was a problem hiding this comment.
Good catch, and agreed it's real: if a non-handoff tool is processed after the handoff tool in the same turn, the unconditional reassignment nulls out the captured new_agent_task and drops the handoff. As you noted, that line is pre-existing (unchanged by this PR), and it's a separate failure mode from the duplicate same-target case this change targets, so I've kept it out to leave this reviewable on its own. Can send a separate PR that guards the reassignment (if sanitized_out.agent_task is not None) if maintainers want the pre-existing issue fixed alongside.
What
When a
@function_toolreturns anAgent(a handoff),AgentActivitytreats a secondAgentTaskproduced in the same turn as an ambiguous multi-handoff and aborts the switch:Real LLMs can emit the same handoff tool call more than once in a single turn (parallel or duplicate tool calls). Each duplicate returns the same target agent, so this guard fires,
ignore_task_switch = True, and the handoff is discarded:update_agentnever runs, the target agent never starts, and the follow-up tool turn is stranded.Fix
Refuse the switch only when the duplicate
AgentTasktargets a different agent (genuinely ambiguous). Identical same-target calls collapse to one handoff. This guard exists in both the pipeline and realtime tool-output loops, so the same change is applied to both. Each duplicate tool call still gets its ownfunction_call_output, so the one-output-per-tool-call requirement holds.Test
tests/test_handoff_duplicate_transfer_5990.pyis a hermetic, no-keys unit test: a scriptedFakeLLMemits two identicaltransfercalls in one turn, and the test asserts the handoff completes and the specialist's follow-up tool runs. It fails onmainand passes with this change. It covers the pipeline path; the realtime loop takes the identical change, and there is no hermetic realtime harness in the suite to unit-test it separately without a live model.make checkis clean (ruff + mypy strict) and the full--unitsuite is green.Scope
This surfaced while investigating #5990. It is an independent correctness fix and does not by itself close that issue. Keeping it small and self-contained so it can be reviewed on its own.