Bug Description
AvatarSession.start() points the agent session's audio output at the avatar's datastream. AvatarSession.aclose() never puts it back. That asymmetry is invisible on the happy path, but if the avatar fails to start, the agent is then silent (and doesn't degrade to just audio).
Expected Behavior
AvatarSession.aclose() sets audio output back to what it was before.
Reproduction Steps
import asyncio
from livekit import rtc
from livekit.agents.voice import AgentSession
from livekit.agents.voice.avatar import AvatarSession, DataStreamAudioOutput
class FakeAvatarSession(AvatarSession):
"""Mimics what an avatar plugin's start() does."""
@property
def avatar_identity(self) -> str:
return "avatar-worker"
async def start(self, agent_session, room):
await super().start(agent_session, room)
agent_session.output.replace_audio_tail(
DataStreamAudioOutput(room=room, destination_identity=self.avatar_identity)
)
async def main():
session = AgentSession()
room = rtc.Room()
print("before start: ", session.output.audio)
avatar = FakeAvatarSession()
await avatar.start(session, room=room)
print("after plugin start: ", type(session.output.audio).__name__)
# the avatar never joins: wait_for_join(timeout=...) raises, the app gives up
await avatar.aclose()
print("after aclose: ", type(session.output.audio).__name__)
asyncio.run(main())
Output:
before start: None
after plugin start: DataStreamAudioOutput
after aclose: DataStreamAudioOutput
Operating System
All
Models Used
No response
Package Versions
Session/Room/Call IDs
No response
Proposed Solution
Have AvatarSession remember what it replaced in start() and restore it in aclose() — symmetric with how it already unregisters its own event handlers and cancels its own join task. (Or provide a parameter to close to do this)
Additional Context
No response
Screenshots and Recordings
No response
Bug Description
AvatarSession.start()points the agent session's audio output at the avatar's datastream.AvatarSession.aclose()never puts it back. That asymmetry is invisible on the happy path, but if the avatar fails to start, the agent is then silent (and doesn't degrade to just audio).Expected Behavior
AvatarSession.aclose()sets audio output back to what it was before.Reproduction Steps
import asyncio from livekit import rtc from livekit.agents.voice import AgentSession from livekit.agents.voice.avatar import AvatarSession, DataStreamAudioOutput class FakeAvatarSession(AvatarSession): """Mimics what an avatar plugin's start() does.""" @property def avatar_identity(self) -> str: return "avatar-worker" async def start(self, agent_session, room): await super().start(agent_session, room) agent_session.output.replace_audio_tail( DataStreamAudioOutput(room=room, destination_identity=self.avatar_identity) ) async def main(): session = AgentSession() room = rtc.Room() print("before start: ", session.output.audio) avatar = FakeAvatarSession() await avatar.start(session, room=room) print("after plugin start: ", type(session.output.audio).__name__) # the avatar never joins: wait_for_join(timeout=...) raises, the app gives up await avatar.aclose() print("after aclose: ", type(session.output.audio).__name__) asyncio.run(main()) Output: before start: None after plugin start: DataStreamAudioOutput after aclose: DataStreamAudioOutputOperating System
All
Models Used
No response
Package Versions
Session/Room/Call IDs
No response
Proposed Solution
Have AvatarSession remember what it replaced in start() and restore it in aclose() — symmetric with how it already unregisters its own event handlers and cancels its own join task. (Or provide a parameter to close to do this)
Additional Context
No response
Screenshots and Recordings
No response