fix: allow agent handoff after ctx.update() in AsyncToolset (fixes #5936)#6254
fix: allow agent handoff after ctx.update() in AsyncToolset (fixes #5936)#6254C1-BA-B1-F3 wants to merge 2 commits into
Conversation
Nova Sonic 2 API rejects sessionStart when endpointingSensitivity is placed at the top level. It must be nested under an 'endpointing' key. Fixes livekit#6200 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
β¦vekit#5936) When a tool in an AsyncToolset calls ctx.update() and then returns an Agent for handoff, the Agent was being silently dropped with an error log. This happened because ctx.update() resolves first_update_fut early, causing execute() to return before the tool completes, and the Agent return had no surface to carry it back. Fix: - Store the Agent in run_ctx._deferred_agent_handoff when returned after ctx.update() - In the _on_done callback, trigger handoff via session.update_agent() if a deferred Agent exists This allows tools to send progress updates via ctx.update() and still perform agent handoff when they complete.
| if run_ctx._deferred_agent_handoff is not None: | ||
| session.update_agent(run_ctx._deferred_agent_handoff) |
There was a problem hiding this comment.
π© Deferred handoff bypasses FunctionToolsExecutedEvent emission
The normal agent handoff path (in livekit-agents/livekit/agents/voice/agent_activity.py:3109-3116) emits a FunctionToolsExecutedEvent with _handoff_required=True and also creates an AgentHandoff history item (via _update_activity_task at agent_session.py:1477). The new deferred path calls session.update_agent() directly from the _on_done callback, which still creates the AgentHandoff item (inside _update_activity_task), but no FunctionToolsExecutedEvent is emitted for the handoff itself. If any listener relies on seeing _handoff_required=True on that event to know a handoff occurred from this specific tool execution, it won't be notified. This may be intentional since the tool's initial output already triggered a function_tools_executed event (with the progress message), but downstream consumers may miss that a handoff was ultimately triggered by this tool.
Was this helpful? React with π or π to provide feedback.
What does this PR do?
Fixes #5936 β allows agent handoff to work after calling
ctx.update()in anAsyncToolsettool.Problem
When a tool in an
AsyncToolsetcallsctx.update()and then returns anAgentfor handoff, the Agent was being silently dropped with an error log:This happened because
ctx.update()resolvesfirst_update_futearly, causingexecute()to return before the tool completes. The Agent return value had no surface to carry it back to the caller.Solution
Store the Agent for deferred handoff: When
_execute_tool()detects that the tool returned an Agent afterctx.update()has already been called, it stores the Agent inrun_ctx._deferred_agent_handoffinstead of logging an error.Trigger handoff on task completion: In the
_on_donecallback (which fires when the tool task completes), check if a deferred Agent exists and callsession.update_agent()to perform the handoff.Changes
livekit-agents/livekit/agents/voice/events.py: Add_deferred_agent_handofffield toRunContextlivekit-agents/livekit/agents/voice/tool_executor.py: Store Agent and trigger handofftests/test_tools.py: Add tests for deferred agent handoffTesting
Added
TestDeferredAgentHandoffclass with two tests:test_agent_return_after_update_stored_for_handoffβ verifies Agent is stored when returned afterctx.update()test_agent_return_without_update_works_normallyβ verifies existing behavior (Agent returned directly) still worksAll 87 tests in
test_tools.pypass.Example
This now works: