Is it best practice to dispatch the agent to rooms...
# ask-ai
w
Is it best practice to dispatch the agent to rooms or have the agent join rooms automatically whenever there is new room created. The use case is voice agent where there is one user and one agent in every room.
t
Thanks for reaching out, Sumiya. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Thank you for sharing your use case of a voice agent with one user and one agent per room. To help you find the best approach, here are some options based on what you might need: • Automatic Dispatch: This is a great choice if you want a simple, consistent way to assign agents across all rooms. It’s easy to set up and works well when you don’t need to pick which agent joins each room. • Explicit Dispatch: This option gives you more control over who joins each room and when. You can specify the agent using APIs or during token generation, which is especially helpful for real-time support where the agent and user connect at the same time. With explicit dispatch, you can also share metadata like user details with the agent, so they have the right context to assist better. If you'd like, I’m here to provide more details or examples—just let me know! Sources: Web and mobile frontends | LiveKit Docs | Web and mobile frontends | LiveKit Docs | Dispatching agents | LiveKit Docs | Agent dispatch | LiveKit Docs | livekit-api/livekit/api/agent_dispatch_service.py
w
what do I need to do so it’s automatic dispatch instead of explicit dispatch. List all the things I need to check to make sure it’s automatic dispatch
t
To help you set up automatic dispatch for agents smoothly, here are some friendly steps to follow: 1. Keep the
agent_name
field unset:
When you’re working with
WorkerOptions
, make sure not to set the
agent_name
field. If this field is set, automatic dispatch won’t work. 2. Make sure agent workers are up and running: It’s important that your agent workers are deployed and actively running so they can handle the automatic dispatch properly. 3. Double-check your room configurations: Take a moment to verify that there are no specific dispatch rules or configurations in your room setup or access tokens that might interfere. 4. Keep an eye on agent connections: Confirm that agents are connecting to the rooms as expected, so everything flows smoothly. If any of these steps feel unclear or if you’d like a hand with anything, just let me know—I’m here to help!
w
why do i see 3 workers running when I only ran the agent once Workers Worker ID Concurrent sessions Resource load Started at Updated at Status AW_mUTfzdtxD67v 0 57.6% Sep 10, 2025, 122955 pmSep 10, 2025, 123257 pm*Ready* AW_4qzTN6AHk8Bp 0 95.9% Sep 10, 2025, 121216 pmSep 10, 2025, 123257 pm*Ready* AW_EQGefUJyo48E 0 75.8% Sep 10, 2025, 121147 pmSep 10, 2025, 123258 pm*Ready*
t
Thanks for reaching out! I understand it can be a bit confusing to see multiple workers running even though you started the agent just once. Here’s a little insight into how LiveKit manages agent workers and jobs: • When you start an agent worker, it registers itself with the LiveKit server and listens for job requests (like when a user joins a room). • Each job runs in its own isolated subprocess or thread, which helps keep things running smoothly by preventing issues in one job from affecting others. • Depending on your setup, LiveKit might keep some idle processes ready to go for faster startup, so you might notice multiple agent instances active behind the scenes. So, essentially, one worker can handle multiple agent jobs at the same time, and each looks like a separate process. If you’d like, I’m here to help you tweak your configuration or explain anything in more detail—just let me know! Sources: Worker lifecycle | LiveKit Docs | Anatomy of an Agent | LiveKit Docs | livekit-agents/livekit/agents/worker.py | Self-hosting the Egress Service | LiveKit Docs
w
here is my python agent code that’s expected to join the room automatically but it’s not joinning #!/usr/bin/env python3 “”" Production-leaning LiveKit Voice Agent (Python) - Env-only secrets (no in-file os.environ mutation) - Room dispatch filtering by prefix - Prewarm Silero VAD & turn detector - AgentSession + RoomInputOptions start “”" import asyncio import logging import os from dotenv import load_dotenv # LiveKit Agents (Python) from livekit import agents from livekit.agents import ( JobContext, WorkerOptions, cli, AgentSession, Agent, RoomInputOptions, AutoSubscribe, ) # Plugins from livekit.plugins import openai, silero from livekit.plugins.turn_detector.english import EnglishModel # ----------------------------------------------------------------------------- # Configuration # ----------------------------------------------------------------------------- load_dotenv() LIVEKIT_URL = os.getenv(“LIVEKIT_URL”, “”) LIVEKIT_API_KEY = os.getenv(“LIVEKIT_API_KEY”, “”) LIVEKIT_API_SECRET = os.getenv(“LIVEKIT_API_SECRET”, “”) OPENAI_API_KEY = os.getenv(“OPENAI_API_KEY”, “”) OPENAI_LLM_MODEL = os.getenv(“OPENAI_LLM_MODEL”, “gpt-4o-mini”) OPENAI_STT_MODEL = os.getenv(“OPENAI_STT_MODEL”, “gpt-4o-mini-transcribe”) OPENAI_TTS_MODEL = os.getenv(“OPENAI_TTS_MODEL”, “tts-1") OPENAI_TTS_VOICE = os.getenv(“OPENAI_TTS_VOICE”, “alloy”) AGENT_INSTRUCTIONS = os.getenv( “AGENT_INSTRUCTIONS”, “You are a friendly ESL tutor. Be concise, natural, and helpful.“, ) ROOM_PREFIX = os.getenv(“ROOM_PREFIX”, “voice-chat-“) AGENT_NAME = os.getenv(“AGENT_NAME”, “nuva-esl”) NUM_IDLE_PROCESSES = int(os.getenv(“NUM_IDLE_PROCESSES”, “1")) # prewarmed workers LOG_LEVEL = os.getenv(“LOG_LEVEL”, “INFO”).upper() logging.basicConfig( level=getattr(logging, LOG_LEVEL, logging.INFO), format=“%(asctime)s %(levelname)s %(message)s”, ) logger = logging.getLogger(AGENT_NAME) def _require_env(_name_: str) -> str: v = os.getenv(name) if not v: raise RuntimeError(f”Missing required env: {name}“) return v def _validate_required_env() -> None: _require_env(“LIVEKIT_URL”) _require_env(“LIVEKIT_API_KEY”) _require_env(“LIVEKIT_API_SECRET”) _require_env(“OPENAI_API_KEY”) # ----------------------------------------------------------------------------- # Diagnostics: log effective (masked) configuration # ----------------------------------------------------------------------------- def _mask_secret(_value_: str) -> str: if not value: return “NOT_SET” if len(value) <= 6: return “***” return f”{value[:3]}***{len(value)}” def _log_effective_config() -> None: _try_: logger.info( “Config: LIVEKIT_URL=%s, AGENT_NAME=%s, ROOM_PREFIX=%s, NUM_IDLE_PROCESSES=%s, LOG_LEVEL=%s”, LIVEKIT_URL, AGENT_NAME, ROOM_PREFIX, NUM_IDLE_PROCESSES, LOG_LEVEL, ) logger.info( “Keys: LIVEKIT_API_KEY=%s, LIVEKIT_API_SECRET=%s, OPENAI_API_KEY=%s”, _mask_secret(LIVEKIT_API_KEY), _mask_secret(LIVEKIT_API_SECRET), _mask_secret(OPENAI_API_KEY), ) except Exception as e: logger.warning(“Env logging failed: %s”, e) # ----------------------------------------------------------------------------- # Prewarm: load heavy models once per process # ----------------------------------------------------------------------------- def prewarm_fnc(_proc_: agents.JobProcess) -> None: “”" Called for each idle worker process. We load models here so first call has no cold start penalty. Must not raise. “”" _try_: # Best-effort process label (API differs by version) pid = getattr(proc, “pid”, None) if pid is None: process_obj = getattr(proc, “process”, None) pid = getattr(process_obj, “pid”, None) label = pid if pid is not None else “unknown” logger.info(“🔧 Prewarming process %s ...“, label) # Silero VAD (you can tune thresholds if desired) proc.userdata[“vad”] = silero.VAD.load( # Common, conservative defaults; tweak if you want snappier cuts: _# min_speech_duration=0.05,_ _# min_silence_duration=0.55,_ _# activation_threshold=0.3,_ _# prefix_padding_duration=0.5,_ ) # English semantic end-of-utterance model proc.userdata[“turn_detection”] = EnglishModel() logger.info(“ Prewarm complete for process %s”, label) except Exception as e: # Never let prewarm kill the worker; log and continue logger.error(“Prewarm error (ignored): %s”, e) # ----------------------------------------------------------------------------- # Request filtering: only accept intended rooms # ----------------------------------------------------------------------------- async def request_fnc(_req_: agents.JobRequest): “”" Only accept jobs for rooms that start with ROOM_PREFIX. This avoids accidental joins and lets you run multiple agents safely. “”" room_name = req.room.name or “” _try_: if room_name.startswith(ROOM_PREFIX): logger.info(“🛎️ JobRequest received: accepting room=%s agent=%s”, room_name, AGENT_NAME) return await req.accept(attributes={“agent”: AGENT_NAME}) _else_: reason = f”Room ‘{room_name}’ does not match prefix ’{ROOM_PREFIX}’” logger.info(“🛎️ JobRequest received: rejecting room=%s reason=%s”, room_name, reason) return await req.reject(reason=reason) except Exception as e: logger.error(“JobRequest handler error: %s”, e) # Best-effort reject on error to avoid stuck jobs return await req.reject(reason=str(e)) # ----------------------------------------------------------------------------- # Job entrypoint # ----------------------------------------------------------------------------- async def entrypoint(_ctx_: JobContext): “”" Main job for each dispatched room. Builds the voice pipeline and starts the agent. “”" _validate_required_env() # Helpful context in logs ctx.log_context_fields = {“room”: ctx.room.name, “job_id”: ctx.job_id, “agent”: AGENT_NAME} logger.info(“🚀 Starting agent for room: %s (job: %s)“, ctx.room.name, ctx.job_id) _# Connect as a participant. AUDIO_ONLY avoids needless video subscription._ await ctx.connect(_auto_subscribe_=AutoSubscribe.AUDIO_ONLY) # docs show this enum # Build plugins (use your provider keys) stt = openai.STT(_api_key_=OPENAI_API_KEY, model=OPENAI_STT_MODEL, language=“en”) llm = openai.LLM(_api_key_=OPENAI_API_KEY, model=OPENAI_LLM_MODEL, _system_prompt_=AGENT_INSTRUCTIONS) tts = openai.TTS(_api_key_=OPENAI_API_KEY, model=OPENAI_TTS_MODEL, voice=OPENAI_TTS_VOICE) # Pull prewarmed models or load on the fly as a fallback vad = (getattr(ctx, “proc”, None) and ctx.proc.userdata.get(“vad”)) or silero.VAD.load() turn_detection = (getattr(ctx, “proc”, None) and ctx.proc.userdata.get(“turn_detection”)) or EnglishModel() # Define agent instructions agent = Agent( instructions=AGENT_INSTRUCTIONS, ) # Compose voice session session = AgentSession( stt=stt, llm=llm, tts=tts, vad=vad, _turn_detection_=turn_detection, _allow_interruptions_=True, # natural barge-in ) # Start the session bound to this room await session.start( room=ctx.room, agent=agent, _room_input_options_=RoomInputOptions( _close_on_disconnect_=False, # keep session alive if the front-end reconnects _# noise_cancellation=None, # set if you want server-side NC_ ), ) # Optional: initial greeting (await ensures audio actually plays before we idle) await session.say(“Hi! I’m ready. Talk to me anytime—no need to press a button.“) logger.info(“🎧 Agent is live in room: %s”, ctx.room.name) # Idle the job until shutdown. Framework will call shutdown hooks on exit. await asyncio.Future() # ----------------------------------------------------------------------------- # Main (Worker) # ----------------------------------------------------------------------------- if name == “__main__“: _# NOTE: Worker uses LIVEKIT_URL / LIVEKIT_API_KEY / LIVEKIT_API_SECRET from env_ # to register for jobs. Do NOT embed secrets in code. _log_effective_config() cli.run_app( WorkerOptions( _entrypoint_fnc_=entrypoint, _request_fnc_=request_fnc, _prewarm_fnc_=prewarm_fnc, _# Do not set agent_name to enable automatic dispatch_ _num_idle_processes_=NUM_IDLE_PROCESSES, _shutdown_process_timeout_=30.0, ) )