little-king-41579
02/16/2026, 8:30 AMtall-airline-16756
02/16/2026, 8:30 AMtall-airline-16756
02/16/2026, 8:30 AMsession.room_io.set_participant(new_identity) to change which participant’s audio the agent transcribes.
• Optionally swap the whole agent by updating the session if you need different behavior for different participants.
Example flow (Python)
1. Start your agent session and connect to the room.
2. Register a handler for the room’s participant_connected event.
3. In that handler call session.room_io.set_participant(participant.identity) (run it as an async task if the handler is synchronous).
Minimal example
import asyncio
@server.rtc_session()
async def entrypoint(ctx: JobContext):
session = AgentSession(
stt=deepgram.STT(), # or your STT provider
# other config...
)
await session.start(agent=MyTranscriberAgent(), room=ctx.room)
await ctx.connect()
def on_participant_connected(participant: rtc.RemoteParticipant):
# schedule async call to switch the linked participant
asyncio.create_task(session.room_io.set_participant(participant.identity))
ctx.room.on("participant_connected", on_participant_connected)
Notes and key points
• session.room_io.set_participant(identity) tells the agent session to transcribe only that participant’s audio.
• By default the session may link to the first participant; calling set_participant switches the linked participant at runtime.
• If you need different transcribing logic per participant (not just changing the audio source), you can replace the agent instance via session.update_agent(NewAgent()).
• Make sure your event handler does not block the event loop; schedule async tasks when needed (as shown).
Questions to tailor this for you
• Do you want the agent to always switch to the newest participant, or to follow some rule (e.g., only switch for specific roles or names)?
• Are you using a specific STT provider/config (Deepgram, Whisper, etc.) or any custom transcription logic?
• Do you want to transcribe multiple participants concurrently, or always only one at a time?
Tell me which behavior you want (always newest, role-based, manual toggle, or concurrent multi-transcribe), and I’ll adapt the example to match and show error handling and clean-up steps.
Sources: Agent session | LiveKit Documentation | How to set a fixed participant for agent audio processing | LiveKit | Pi Zero Transcriber | LiveKit Documentationlittle-king-41579
02/16/2026, 8:32 AMAgentSession.update_agent being invoked.
{"room": "voice_assistant_room_7289", "pid": 139267, "job_id": "AJ_ZwGyMJgTeeNk", "room_id": "RM_oFLx9gyikH4D"}
Exception ignored in: <coroutine object AgentActivity._create_speech_task.<locals>._context_aware_coro at 0x7d9c10146500>
Traceback (most recent call last):
File "/home/azureuser/AI-development/.venv/lib/python3.12/site-packages/livekit/agents/voice/agent_activity.py", line 411, in _context_aware_coro
return await coro
^^^^^^^^^^
File "/home/azureuser/AI-development/.venv/lib/python3.12/site-packages/opentelemetry/util/_decorator.py", line 71, in async_wrapper
return await func(*args, **kwargs) # type: ignore
^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/azureuser/AI-development/.venv/lib/python3.12/site-packages/livekit/agents/utils/log.py", line 16, in async_fn_logs
return await fn(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/azureuser/AI-development/.venv/lib/python3.12/site-packages/livekit/agents/voice/agent_activity.py", line 479, in _traceable_on_enter
_OnEnterContextVar.reset(tk)
ValueError: <Token var=<ContextVar name='agents_activity_on_enter' at 0x7d9c18f07f60> at 0x7d9c1013a740> was created in a different Context
ERROR livekit.agents Error in _traceable_on_enter
Traceback (most recent call last):
File "/home/azureuser/AI-development/.venv/lib/python3.12/site-packages/livekit/agents/voice/agent_activity.py", line 477, in
_traceable_on_enter
await self._agent.on_enter()
File "/home/azureuser/AI-development/src/agents/agent.py", line 50, in on_enter
patient_result = await ClaimStatusIdentifyScenarioTask(tools=tools,chat_ctx=self.chat_ctx,userdata=self.userdata)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
RuntimeError: coroutine ignored GeneratorExit
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/home/azureuser/AI-development/.venv/lib/python3.12/site-packages/livekit/agents/utils/log.py", line 16, in async_fn_logs
return await fn(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/azureuser/AI-development/.venv/lib/python3.12/site-packages/livekit/agents/voice/agent_activity.py", line 479, in
_traceable_on_enter
_OnEnterContextVar.reset(tk)
ValueError: <Token var=<ContextVar name='agents_activity_on_enter' at 0x7d9c18f07f60> at 0x7d9c1013a740> was created in a different Context
{"room": "voice_assistant_room_7289", "pid": 139267, "job_id": "AJ_ZwGyMJgTeeNk", "room_id": "RM_oFLx9gyikH4D"}tall-airline-16756
02/16/2026, 8:33 AMAgentSession.update_agent() or otherwise terminate the session. That causes tasks to be destroyed while still pending and leads to a ContextVar token being reset in a different execution context, which raises the ValueError.
• The log “ClaimStatusIdentifyScenarioTask completed, but the agent has changed in the meantime” indicates a handoff raced with an agent update — the task finished after you already swapped agents.
Concrete fixes to apply 1) Don’t update the agent while a lifecycle task is still running
• Before calling session.update_agent(...) or otherwise replacing the agent, await any important lifecycle tasks that influence agent state (like tasks you run in on_enter).
• Example pattern:
# await the critical task result before changing agent
result = await ClaimStatusIdentifyScenarioTask(...)
# only after result, update the agent
session.update_agent(NewAgent(...))
2) Avoid fire-and-forget for agent lifecycle work
• Do not wrap on_enter()-like tasks in asyncio.create_task() if they depend on agent context. That disconnects contextvars and lets the agent be swapped underneath the running coroutine.
• If you must run background work, make sure it does not touch agent lifecycle ContextVars (decouple it), or track and cancel it explicitly on agent update.
3) Use structured concurrency to manage lifetime of related tasks
• Use Python 3.11+ TaskGroup (or manual tracking) so tasks are awaited or cancelled cleanly rather than being destroyed while pending.
async with asyncio.TaskGroup() as tg:
tg.create_task(some_background_job())
result = await tg.create_task(ClaimStatusIdentifyScenarioTask(...))
# TaskGroup will ensure proper cancel/cleanup on exit
4) Preserve ContextVar semantics
• The ValueError arises when you reset a ContextVar token created in a different task/context. Keep ContextVar use local to the task or ensure you set/reset tokens within the same Task. Do not share the token across tasks.
• Practically: avoid resetting ContextVars in callbacks that run in different tasks, and avoid creating tokens in one task and resetting in another.
5) If you need to switch transcription source frequently
• Use session.room_io.set_participant(identity) to change which participant audio the agent transcribes. That change is lighter-weight than swapping the entire agent and reduces the risk of racing lifecycle tasks.
• If you must update the agent, first ensure all agent tasks are idle or cancelled, then call update_agent.
Quick checklist to debug your code now
• Where do you call session.update_agent()? Add logging and make sure no agent-relevant tasks are pending at that time.
• Do you ever call asyncio.create_task() from inside on_enter or other lifecycle functions? Convert to awaited calls or ensure created tasks are independent and do not use ContextVars from the lifecycle.
• Add explicit task tracking: store the Task object(s) and await them or cancel them on agent update.
• Reproduce with controlled steps: join participant A, start transcription task, then trigger update; confirm whether the update raced with task completion.
If you want, share short excerpts of:
• how you call update_agent() (the surrounding code),
• your on_enter() implementation or the code that launches ClaimStatusIdentifyScenarioTask,
• any place you call asyncio.create_task() for agent logic.
With those snippets I can point out exactly where to await/cancel tasks or how to refactor to avoid ContextVar cross-task resets and the pending-task destruction.
Sources: Agents and handoffs | LiveKit Documentation | Collect recording consent with tasks | LiveKit Documentation | Agent session | LiveKit Documentationlittle-king-41579
02/16/2026, 8:34 AMtall-airline-16756
02/16/2026, 8:35 AMsession.room_io.set_participant(identity),
• avoiding the "Task was destroyed but it is pending!" warning,
• preventing ContextVar token errors by not leaking context across tasks,
• safely updating agents when tasks are complete.
This example uses structured concurrency (TaskGroup if available) and explicit task tracking so you can adapt it to your LiveKit agent code.
Save as e.g. agent_safe_switch.py and adapt the imports/config to match your environment.
import asyncio
import logging
from typing import Optional
# Replace these imports with your actual LiveKit SDK imports
# from livekit import rtc
# from livekit.agents import AgentSession, AgentContext, JobContext, cli, inference
# Dummy placeholders used only for this example; replace with real classes.
class rtc:
class RemoteParticipant:
def __init__(self, identity): self.identity = identity
class Room:
def __init__(self): self.remote_participants = {}
connected = True
def on(self, ev, fn): pass
class AgentSession:
def __init__(self, **kwargs): self.room_io = None
async def start(self, agent, room): self.room_io = RoomIO(room); self._agent = agent
async def generate_reply(self, instructions: str): pass
def shutdown(self): pass
async def update_agent(self, new_agent): self._agent = new_agent
class RoomIO:
def __init__(self, room): self.room = room
async def set_participant(self, participant_or_identity):
# In real SDK: accepts identity or RemoteParticipant
await asyncio.sleep(0) # simulate async op
# Replace with your real task that used to cause trouble
async def ClaimStatusIdentifyScenarioTask(*, tools=None, chat_ctx=None, userdata=None):
await asyncio.sleep(0.1)
return "identified"
logging.basicConfig(level=<http://logging.INFO|logging.INFO>)
logger = logging.getLogger("safe-agent")
class SafeAgent:
"""
Example agent demonstrating safe lifecycle patterns.
- Do not create long-running lifecycle tasks with create_task() that depend on ContextVars.
- Track tasks you start and await/cancel them on agent update or shutdown.
"""
def __init__(self):
self._session: Optional[AgentSession] = None
self._linked_participant_identity: Optional[str] = None
self._background_tasks: set[asyncio.Task] = set()
async def on_enter(self, ctx):
# framework will call this; set session reference from ctx
self._session = ctx.session
# run short init work synchronously (awaited), not fire-and-forget
await self._synchronous_startup()
async def _synchronous_startup(self):
# Example: run a small task that must complete before agent changes
<http://logger.info|logger.info>("on_enter: running startup task")
result = await ClaimStatusIdentifyScenarioTask()
<http://logger.info|logger.info>(f"on_enter: startup result: {result}")
# OK to schedule background work that is independent of ContextVars:
t = asyncio.create_task(self._independent_background_loop())
self._background_tasks.add(t)
t.add_done_callback(self._background_tasks.discard)
async def on_exit(self):
<http://logger.info|logger.info>("on_exit: shutting down agent")
# Cancel any background tasks we started
if self._background_tasks:
for t in list(self._background_tasks):
t.cancel()
await asyncio.gather(*self._background_tasks, return_exceptions=True)
# If session exists, call shutdown cleanup
if self._session:
self._session.shutdown()
self._session = None
async def _independent_background_loop(self):
# long-running background task that must NOT touch lifecycle ContextVars
try:
while True:
await asyncio.sleep(5)
<http://logger.info|logger.info>("background: heartbeat")
except asyncio.CancelledError:
<http://logger.info|logger.info>("background: cancelled")
raise
async def switch_to_participant(self, identity: str):
"""
Safely switch the RoomIO linked participant.
This is the recommended approach instead of swapping the agent while tasks are active.
"""
if not self._session or not self._session.room_io:
logger.warning("No active session/room_io to switch participant")
return
# If you have the remote participant object, pass it; otherwise pass identity
await self._session.room_io.set_participant(identity)
self._linked_participant_identity = identity
<http://logger.info|logger.info>(f"Now transcribing participant: {identity}")
# Short awaited reply to confirm the switch. Keep quick and awaited.
await self._session.generate_reply(instructions=f"Switched to {identity} for transcription.")
async def safely_update_agent(self, new_agent):
"""
Example: safely replace this agent with new_agent.
Ensure all important tasks are complete or cancelled first.
"""
<http://logger.info|logger.info>("Preparing to update agent: waiting for important tasks to finish")
# If you started any critical (non-background) tasks, await them here before updating.
# For demonstration, we don't have extra critical tasks; cancel background tasks:
if self._background_tasks:
for t in list(self._background_tasks):
t.cancel()
await asyncio.gather(*self._background_tasks, return_exceptions=True)
# Now it is safe to update the agent in the session
if self._session:
await self._session.update_agent(new_agent)
<http://logger.info|logger.info>("Agent updated successfully")
# Minimal ctx-like objects to simulate framework lifecycle in this example
class DummyCtx:
def __init__(self, room):
self.room = room
self.session = None
async def connect(self): pass
# Entrypoint that shows safe wiring for participant events and session lifecycle
async def main_entrypoint():
# Simulated room and session creation
room = rtc.Room()
# Add two participants to simulate presence
room.remote_participants["alice"] = rtc.RemoteParticipant("alice")
room.remote_participants["bob"] = rtc.RemoteParticipant("bob")
ctx = DummyCtx(room)
# Create session and agent
session = AgentSession()
agent = SafeAgent()
# Start session with agent (framework would pass ctx.session into agent on_enter)
class DummyAgentContext:
def __init__(self, session): self.session = session
await session.start(agent=agent, room=room)
# Simulate framework calling on_enter with context
await agent.on_enter(DummyAgentContext(session))
# Option A: Switch linked participant safely (recommended)
await agent.switch_to_participant("alice")
# Simulate a new participant joining - we will switch to them
<http://logger.info|logger.info>("Simulating new participant 'bob' joining")
# Recommended: call session.room_io.set_participant to change transcription source
await agent.switch_to_participant("bob")
# If you must update the whole agent, do it safely via agent.safely_update_agent(...)
# new_agent = SafeAgent()
# await agent.safely_update_agent(new_agent)
# Wait a bit to demonstrate background activity
await asyncio.sleep(0.5)
# Now simulate shutdown / agent exit
await agent.on_exit()
# Run the example
if __name__ == "__main__":
try:
asyncio.run(main_entrypoint())
except KeyboardInterrupt:
pass
Key patterns shown and why they fix your errors
• Await critical tasks instead of scheduling them fire-and-forget. That prevents “Task was destroyed but it is pending!” when the agent changes.
• Keep ContextVar usage local to each task and avoid creating a context in one Task and resetting it in another. By awaiting lifecycle tasks in the same task and not detaching them via create_task (for context-dependent work), the token ValueError is avoided.
• Use session.room_io.set_participant(identity) to change which participant is transcribed — this is lighter and safer than swapping the full agent while tasks are running.
• Track background tasks and cancel them in on_exit so nothing is unexpectedly destroyed by the runtime.
• If you must replace the agent, cancel/await relevant tasks first, then call session.update_agent(...).
If you want, I can:
• Adapt this example to your exact LiveKit imports and STT/TTs providers (Deepgram/Whisper etc.).
• Show a real example of using Python 3.11's asyncio.TaskGroup for structured concurrency.
• Review your actual on_enter and update_agent snippets and point out the exact lines to change.
Which would help most next?
Sources: Agent session | LiveKit Documentation | Agents and handoffs | LiveKit Documentation | How to detect and handle silence during calls | LiveKit | Text and transcriptions | LiveKit Documentation