Skip to content

Commit 4cce457

Browse files
Aditya InamdarAditya Inamdar
authored andcommitted
fix(voice): skip tool reply generation during handoff
1 parent 596f4bd commit 4cce457

2 files changed

Lines changed: 60 additions & 3 deletions

File tree

livekit-agents/livekit/agents/voice/agent_activity.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2467,7 +2467,8 @@ def _tool_execution_completed_cb(out: ToolExecutionOutput) -> None:
24672467
ignore_task_switch = True
24682468
# TODO(long): should we mark the function call as failed to notify the LLM?
24692469

2470-
new_agent_task = sanitized_out.agent_task
2470+
if sanitized_out.agent_task is not None:
2471+
new_agent_task = sanitized_out.agent_task
24712472

24722473
if new_agent_task and not ignore_task_switch:
24732474
fnc_executed_ev._handoff_required = True
@@ -2480,7 +2481,7 @@ def _tool_execution_completed_cb(out: ToolExecutionOutput) -> None:
24802481
draining = True
24812482

24822483
tool_messages = new_calls + new_fnc_outputs
2483-
if fnc_executed_ev._reply_required:
2484+
if fnc_executed_ev._reply_required and not fnc_executed_ev._handoff_required:
24842485
chat_ctx.items.extend(tool_messages)
24852486

24862487
# refresh instructions in chat_ctx so that any update_instructions()
@@ -2992,7 +2993,8 @@ def _create_assistant_message(
29922993
)
29932994
ignore_task_switch = True
29942995

2995-
new_agent_task = sanitized_out.agent_task
2996+
if sanitized_out.agent_task is not None:
2997+
new_agent_task = sanitized_out.agent_task
29962998

29972999
if new_agent_task and not ignore_task_switch:
29983000
fnc_executed_ev._handoff_required = True
@@ -3029,6 +3031,7 @@ def _create_assistant_message(
30293031

30303032
if (
30313033
fnc_executed_ev._reply_required
3034+
and not fnc_executed_ev._handoff_required
30323035
and not self.llm.capabilities.auto_tool_reply_generation
30333036
):
30343037
self._rt_session.interrupt()

tests/test_agent_session.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import asyncio
4+
from unittest import mock
45

56
import pytest
67

@@ -65,6 +66,29 @@ async def on_user_turn_completed(self, turn_ctx: ChatContext, new_message: ChatM
6566
await asyncio.sleep(self.on_user_turn_completed_delay)
6667

6768

69+
class HandoffTargetAgent(Agent):
70+
def __init__(self, entered_event: asyncio.Event) -> None:
71+
super().__init__(instructions=("You are the target handoff agent."))
72+
self._entered_event = entered_event
73+
74+
async def on_enter(self) -> None:
75+
self._entered_event.set()
76+
77+
78+
class HandoffSourceAgent(Agent):
79+
def __init__(self, entered_event: asyncio.Event) -> None:
80+
super().__init__(instructions=("You are a source agent that can hand off."))
81+
self._entered_event = entered_event
82+
83+
@function_tool
84+
async def switch_to_secondary(self) -> Agent:
85+
return HandoffTargetAgent(self._entered_event)
86+
87+
@function_tool
88+
async def save_data(self, value: str) -> str:
89+
return f"saved:{value}"
90+
91+
6892
SESSION_TIMEOUT = 60.0
6993

7094

@@ -215,6 +239,36 @@ async def test_tool_call() -> None:
215239
assert chat_ctx_items[6].text_content == "The weather in Tokyo is sunny today."
216240

217241

242+
async def test_handoff_and_reply_required_no_extra_old_agent_reply() -> None:
243+
speed = 5.0
244+
actions = FakeActions()
245+
actions.add_user_speech(0.5, 2.0, "switch")
246+
actions.add_llm(
247+
content="",
248+
tool_calls=[
249+
FunctionToolCall(name="save_data", arguments='{"value": "x"}', call_id="1"),
250+
FunctionToolCall(name="switch_to_secondary", arguments="{}", call_id="2"),
251+
],
252+
)
253+
254+
handoff_entered = asyncio.Event()
255+
session = create_session(actions, speed_factor=speed)
256+
agent = HandoffSourceAgent(handoff_entered)
257+
258+
tool_executed_events: list[FunctionToolsExecutedEvent] = []
259+
session.on("function_tools_executed", tool_executed_events.append)
260+
261+
with mock.patch.object(session.llm, "chat", wraps=session.llm.chat) as mock_chat:
262+
await asyncio.wait_for(run_session(session, agent), timeout=SESSION_TIMEOUT)
263+
264+
assert handoff_entered.is_set()
265+
assert len(tool_executed_events) == 1
266+
assert tool_executed_events[0].has_agent_handoff is True
267+
assert tool_executed_events[0].has_tool_reply is True
268+
# No extra old-agent reply generation after handoff.
269+
assert mock_chat.call_count == 1
270+
271+
218272
@pytest.mark.parametrize(
219273
"resume_false_interruption, expected_interruption_time",
220274
[

0 commit comments

Comments
 (0)