Use case
Voice agent with a long-running async tool (AsyncToolset): the user asks for something, the tool runs for 30–90s, and the user quite reasonably waits in silence. After user_away_timeout the session flips user_state to "away" and emits user_state_changed once.
The problem: "away" is edge-triggered, and the away timer only re-arms while the user is "listening" — _set_user_away_timer() is called from _update_agent_state / _update_user_state under a mutual-listening check (state == "listening" and self._user_state == "listening", agent_session.py, v1.6.10). So once a silent user is marked away, the agent finishing its tool run and speaking the results does not restart the window, and no further user_state_changed events ever fire for that user unless they speak. Any idle etiquette built on that event (check in once, then say goodbye and close the room) is silently disarmed for the rest of the session: results get announced to a silent user and the session hangs open indefinitely, holding a live room on nobody.
What we do today
We call the private method to flip the user back and let the framework re-arm its own timer:
if session.user_state == "away":
session._update_user_state("listening")
This works well — _update_user_state no-ops on same-state, re-arms the away timer under the mutual-listening check, and emits the event — but it's private surface, so it's pinned-version-only and has to be re-verified on every SDK upgrade.
Feature request
A supported way to tell the session "this user is still here / restart the away window", for example any of:
session.reset_user_away_timer() — re-arm the window without touching state; or
- a public
session.set_user_state("listening") (with the same guards _update_user_state already has); or
- an option like
user_away_resets_on_agent_speech: bool so agent speech re-arms the window even when the user is already "away" — which would cover the "user is waiting on the agent" case with no application code at all.
Happy to open a PR for whichever shape you'd prefer.
Environment: livekit-agents 1.6.10, STT-LLM-TTS pipeline, async tools via AsyncToolset.
Use case
Voice agent with a long-running async tool (
AsyncToolset): the user asks for something, the tool runs for 30–90s, and the user quite reasonably waits in silence. Afteruser_away_timeoutthe session flipsuser_stateto"away"and emitsuser_state_changedonce.The problem:
"away"is edge-triggered, and the away timer only re-arms while the user is"listening"—_set_user_away_timer()is called from_update_agent_state/_update_user_stateunder a mutual-listening check (state == "listening" and self._user_state == "listening",agent_session.py, v1.6.10). So once a silent user is marked away, the agent finishing its tool run and speaking the results does not restart the window, and no furtheruser_state_changedevents ever fire for that user unless they speak. Any idle etiquette built on that event (check in once, then say goodbye and close the room) is silently disarmed for the rest of the session: results get announced to a silent user and the session hangs open indefinitely, holding a live room on nobody.What we do today
We call the private method to flip the user back and let the framework re-arm its own timer:
This works well —
_update_user_stateno-ops on same-state, re-arms the away timer under the mutual-listening check, and emits the event — but it's private surface, so it's pinned-version-only and has to be re-verified on every SDK upgrade.Feature request
A supported way to tell the session "this user is still here / restart the away window", for example any of:
session.reset_user_away_timer()— re-arm the window without touching state; orsession.set_user_state("listening")(with the same guards_update_user_statealready has); oruser_away_resets_on_agent_speech: boolso agent speech re-arms the window even when the user is already"away"— which would cover the "user is waiting on the agent" case with no application code at all.Happy to open a PR for whichever shape you'd prefer.
Environment: livekit-agents 1.6.10, STT-LLM-TTS pipeline, async tools via
AsyncToolset.