Skip to content

fix(voice): don't hold run open on updateAgent handoffs#2098

Merged
toubatbrian merged 1 commit into
mainfrom
port-python-6500-update-agent-on-enter
Jul 24, 2026
Merged

fix(voice): don't hold run open on updateAgent handoffs#2098
toubatbrian merged 1 commit into
mainfrom
port-python-6500-update-agent-on-enter

Conversation

@rosetta-livekit-bot

@rosetta-livekit-bot rosetta-livekit-bot Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Ports livekit/agents#6500 to JS: updateAgent() handoffs no longer wait for the new agent's onEnter task directly, but still watch that task on the active run() so delayed onEnter output is captured.

Adds parity coverage for delayed onEnter output and long-lived onEnter flows that need a future user turn.

Source diff coverage
Source file Classification Target coverage
livekit-agents/livekit/agents/voice/agent_session.py Ported agents/src/voice/agent_session.ts: updateAgent() now calls _updateActivity(..., { waitOnEnter: false }), then watches the new activity's _onEnterTask on the active RunResult when present.
tests/test_update_agent_long_on_enter.py Adapted agents/src/voice/agent_session_update_agent_long_on_enter.test.ts: ported both source regression scenarios using Vitest, JS Agent/AgentTask, and the target FakeLLM/RunResult test helpers.

Testing

  • pnpm prettier --check "agents/src/voice/agent_session.ts" "agents/src/voice/agent_session_update_agent_long_on_enter.test.ts"
  • pnpm --filter @livekit/agents typecheck
  • pnpm --filter @livekit/agents lint (passes with existing warnings)
  • pnpm vitest run agents/src/voice/agent_session_update_agent_long_on_enter.test.ts
  • pnpm vitest run agents --maxWorkers=1
  • pnpm build:agents

Note: the default parallel pnpm vitest run agents hit the existing agents/src/utils.test.ts nested task ordering flake twice; that file passed standalone, and the full agents suite passed serially with --maxWorkers=1.

Cue voice E2E

  • Exact head: 4df08ec5a2e20eb20a631843fcf5f4040ac47c09
  • Session: sid_ceaa915daa36 (--mode voice)
  • Predicate: function_tools_executed(.function_calls[0].name="switch_to_delayed_agent") -> conversation_item_added(.item.message.role="ASSISTANT",.item.message.content[0].text="Rosetta 2098 delayed on enter verified.")[20s]
  • Result: resolved in 16,058 ms. Persisted event offset 18 records switch_to_delayed_agent, whose implementation calls session.updateAgent(new DelayedGreetingAgent()); offset 28 records the agent handoff; after the new agent's delayed onEnter, offset 34 records the exact assistant output Rosetta 2098 delayed on enter verified.
  • Artifacts: ~/.cue-cli/sessions/sid_ceaa915daa36/manifest.json, events.jsonl, recordings/001_run/result.json, recording.wav, and recordings/001_run/recording.wav
  • Providers: voice input synthesized by Cue with Cartesia; agent STT deepgram/nova-3, LLM openai/gpt-4.1-mini, TTS cartesia/sonic-3 through LiveKit Inference, and Silero VAD (manifest.json reports vad: "silero").
  • Limitations: persisted framework events and both stereo WAVs were inspected, but the resulting agent audio was not independently re-transcribed or semantically scored.

Ported from livekit/agents#6500

Original PR description

What

session.run() deadlocks when a function tool calls session.update_agent(new_agent) and the new agent's on_enter awaits an AgentTask (or TaskGroup) that needs a future user turn to complete.

The chain: update_agent()_update_activity_task (watched by the active RunResult so the handoff result lands before run completion) → _update_activity(agent) with the default wait_on_enter=True → awaits the new agent's on_enter → which awaits an AgentTask that needs the next user turn. But the driver can't send the next turn until run() returns — a circular wait. The run hangs until an external timeout cancels everything (surfacing as ToolError: AgentTask ... is cancelled).

Both other activity-transition paths already guard against exactly this:

  • session.start() passes wait_on_enter=False (agent_session.py:916), which is why the examples/survey pattern (agent on_enter awaiting a whole TaskGroup) works when the agent is the starting agent.
  • AgentTask.__await_impl passes wait_on_enter=False with a comment describing this exact deadlock: "on_enter may spawn nested AgentTasks that require user input, but session.run() can't return until all watched handles complete — creating a circular wait."

update_agent() was the only path still waiting. Entering the same agent that works via session.start() through an update_agent() handoff deadlocks — reproduced on 1.6.4, 1.6.5, and 1.6.6.

Fix

Pass wait_on_enter=False in _update_activity_task, matching the other two paths.

Test

tests/test_update_agent_long_on_enter.py — FakeLLM-scripted: a greeter tool calls update_agent() into an agent whose on_enter awaits an AgentTask needing another user turn; asserts run() returns on the handoff turn and the task completes on the following turn. Deadlocks (times out) without the fix; passes with it.

Validation:

  • tests/test_agent_session.py, test_nested_agent_task.py, test_audio_recognition_handoff.py, test_agent_task_close_race.py, test_drain_timeout.py, test_tool_results_preserved_on_interruption.py, test_user_turn_exceeded.py, test_speech_handle_exception.py, test_agent_update_options.py + the new test: 116 passed.
  • examples/survey/test_survey_agent.py: 5 passed (live LLM).
  • A production voice agent using this shape (greeter agent routing via update_agent() to an agent whose on_enter drives a multi-task loop): its full offline suite of ~1000 tests passes against this patch; on current releases every workflow test deadlocks.

@rosetta-livekit-bot
rosetta-livekit-bot Bot requested a review from a team as a code owner July 23, 2026 11:18
@rosetta-livekit-bot
rosetta-livekit-bot Bot requested a review from longcw July 23, 2026 11:19

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Open in Devin Review

@toubatbrian
toubatbrian merged commit 38490e5 into main Jul 24, 2026
6 checks passed
@toubatbrian
toubatbrian deleted the port-python-6500-update-agent-on-enter branch July 24, 2026 02:40
@github-actions github-actions Bot mentioned this pull request Jul 24, 2026
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.

1 participant