I notice that if I create an agent and I join the ...
# ask-ai
e
I notice that if I create an agent and I join the conversation before the agent initialises I need to use the on_enter block, if the agent joins before me I need to use the participant_connected block. Is it best that I just use the instructions as oppose to have custom logic? what is the best way to ensure that the agent always speaks first regardless of what I do and uses the main instructions I have included?
t
Thanks for reaching out, Arthur. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Thanks — that’s a great question. You’re right to notice the two different behaviors; here’s the simplest, most reliable approach. • Use the agent’s
on_enter()
lifecycle hook to initiate the first message.
on_enter()
runs when the agent becomes active in the session regardless of join order, so it guarantees the agent can speak first whether it joins before or after the user. • Keep your conversational instructions (persona/goals) in the agent’s instructions, and use
on_enter()
to call
generate_reply()
or
session.say()
with those instructions (or reference them). Instructions alone won’t make the agent speak — you must trigger the reply. • Avoid relying only on
participant_connected
for the initial greeting because that requires different handling depending on who connects first. If you want, I can show a short code example of
on_enter()
calling
generate_reply()
with your instructions — do you prefer Python or JavaScript? Sources: Pipeline nodes and hooks | LiveKit docs | Agent speech and audio | LiveKit docs
e
So I should just use my instructions on generate reply on_enter? or does using generate_reply without explicit requirements append the instruction that was included in the agent init?
t
Good question — thanks for clarifying. Short answer: yes, calling
generate_reply()
in
on_enter()
without passing new instructions will use the agent’s initial instructions you set at initialization. Use explicit
instructions
only when you want to override or refine that single turn. Recommended pattern: • Put your persona/goals in the agent init instructions. • In
on_enter()
, call
generate_reply()
(or
session.say()
) to trigger the first utterance. • Pass
instructions=
to
generate_reply()
only when you need a different or specific prompt for that initial message. Would you like a brief Python or JavaScript example of
on_enter()
using
generate_reply()
? Sources: Agent speech and audio | LiveKit docs | Voice AI quickstart | LiveKit docs | Simple Call Answering Agent | LiveKit docs
e
Given that our agent is always one on one with a participant, should we always invoke close room when the participant disconnects?
t
Short answer: yes — in a one‑on‑one agent session you should delete/close the room when the participant disconnects. Why: • Deleting the room ensures the agent session ends immediately, avoids the agent lingering, and frees server resources. • It also stops egress (recordings/transcripts) cleanly so you don’t get extra audio after the user leaves. • Relying on implicit room lifecycle can lead to timing/configuration edge cases; explicit deletion matches the expected app behavior. Recommended pattern: • Listen for the participant disconnect (or room
disconnected
event) and call
delete_room
(or your RoomService’s delete) to fully terminate. • If you need the agent to finish a final action (say a goodbye), run that via
session.shutdown()
or a final
generate_reply()
before deleting the room. Example (Python):
Copy code
@ctx.room.on("disconnected")
async def on_disconnect():
    await api_client.room.delete_room(api.DeleteRoomRequest(room=ctx.room.name))
Would you like a Node.js example or a version that runs a final agent message before deleting the room? Sources: Job lifecycle | LiveKit docs | Agents telephony integration | LiveKit docs
e
Could you write me a version that does detection that the only room participant is the AI agent and to close if that is the case?
t
Thanks — I’ll provide a concise example that detects when the only remaining participant is the agent and then deletes the room. This responds to your request for logic that closes the room only when no human participants remain. Which language do you want? I can provide: • Python (async example using room events) • Node.js / TypeScript (event-based) Pick one and I’ll send a minimal, ready-to-run snippet that includes a small delay to let participant state settle, the check for non-agent participants, and the room deletion call. Sources: server-sdk-js | livekit/livekit_composite | DeepWiki | Job lifecycle | LiveKit docs
e
Python please, this is my current agent which is super simple example
Copy code
"""
Dynamic agent implementation.

This module provides a dynamically configured agent that can be created from a config dict.
"""

from typing import Optional
from livekit.agents import Agent, ChatContext


class DynamicAgent(Agent):
    """A dynamically configured agent that can be created from a config dict."""
    
    def __init__(
        self,
        instructions: str,
        on_enter_instructions: Optional[str] = None,
        chat_ctx: Optional[ChatContext] = None,
    ) -> None:
        super().__init__(
            instructions=instructions,
            chat_ctx=chat_ctx,
        )
        self._on_enter_instructions = on_enter_instructions
    
    async def on_enter(self) -> None:
        """Called when the agent enters the conversation."""
        if self._on_enter_instructions:
            await self.session.generate_reply(
                instructions=self._on_enter_instructions
            )
        else:
            # If no instructions are provided, use default instructions
            await self.session.generate_reply()
t
Thanks — I’ll update your existing
DynamicAgent
with a minimal async handler that detects when only the agent remains and then deletes the room. This will: • Wait briefly after a disconnect so participant state settles. • Check remaining participants for any non-agent kinds. • Call the room delete API when only agent(s) remain. Add this to your module (adjust import/room client call to match your app):
Copy code
import asyncio
import logging
from typing import Optional
from livekit.agents import Agent, ChatContext
from livekit import api  # adjust if your client is in a different module

logger = logging.getLogger(__name__)

class DynamicAgent(Agent):
    def __init__(
        self,
        instructions: str,
        on_enter_instructions: Optional[str] = None,
        chat_ctx: Optional[ChatContext] = None,
        room_service_client=None,  # pass a client able to delete rooms
    ) -> None:
        super().__init__(instructions=instructions, chat_ctx=chat_ctx)
        self._on_enter_instructions = on_enter_instructions
        self._room_service_client = room_service_client

    async def on_enter(self) -> None:
        if self._on_enter_instructions:
            await self.session.generate_reply(instructions=self._on_enter_instructions)
        else:
            await self.session.generate_reply()

    async def participant_disconnected(self, participant) -> None:
        """
        Call this from your room disconnect event wiring (or register as handler)
        to check remaining participants and delete the room if only agents remain.
        """
        # small delay to let server update participant list
        await asyncio.sleep(0.1)

        # Acquire current participants from the session/room context
        # Adjust attribute access to match your runtime object model
        try:
            participants = self.session.room.remote_participants  # dict-like
        except Exception:
            # fallback: try other path if your runtime differs
            participants = getattr(self.session.room, "participants", {})

        # Determine if any non-agent participants remain
        has_non_agent = any(
            p.kind != p.ParticipantKind.PARTICIPANT_KIND_AGENT
            for p in participants.values()
        )

        if not has_non_agent:
            <http://logger.info|logger.info>("No human participants remain — deleting room %s", self.session.room.name)
            try:
                if not self._room_service_client:
                    raise RuntimeError("room_service_client not provided")
                # call your RoomService delete method; adjust to your client API
                await self._room_service_client.delete_room(self.session.room.name)
                <http://logger.info|logger.info>("Room deleted successfully")
            except Exception as e:
                logger.exception("Failed to delete room: %s", e)
Notes: • Provide a
room_service_client
with permission to delete rooms (roomAdmin grant). • Ensure your websocket/room event wiring calls
participant_disconnected
when a participant leaves (example earlier used
@ctx.room.on("disconnected")
). • You can run a final
await self.session.generate_reply(instructions="Goodbye")
before deletion if you want a last agent utterance; perform that before calling
delete_room
. Would you like me to adapt this snippet to your exact room client API or add a final goodbye flow before deletion?
e
If rooms are created using the dispatch method (create room with dispatch agent block) - does the agent automatically get admin rights?
t
Short answer: No — agents dispatched via the dispatch/create-room flow do not automatically get RoomAdmin privileges. Details and recommended action: • The agent’s permissions come from the token/dispatch configuration you provide, not from the fact that the room was created via dispatch. • If your agent needs admin capabilities (manage participants, delete rooms, start/stop egress, etc.), include the RoomAdmin grant (or equivalent room admin permissions) in the agent’s dispatch token or dispatch request. • If you prefer least-privilege, grant only the specific abilities the agent needs (e.g.,
canPublish
,
canPublishData
, egress/recording controls) instead of full RoomAdmin. If you want, I can show an example dispatch payload/token that grants RoomAdmin or a minimal-permissions variant — Python or Node.js? Sources: server-sdk-kotlin | livekit/livekit_composite | DeepWiki | Agent dispatch | LiveKit docs
e
This is the example provided in your documents - does delete room simply not work unless you have the permissions?
Copy code
# Add these imports at the top of your file
from livekit import api, rtc
from livekit.agents import get_job_context

# Add this function definition anywhere
async def hangup_call():
    ctx = get_job_context()
    if ctx is None:
        # Not running in a job context
        return
    
    await ctx.api.room.delete_room(
        api.DeleteRoomRequest(
            room=ctx.room.name,
        )
    )

class MyAgent(Agent):
    ...

    # to hang up the call as part of a function call
    @function_tool
    async def end_call(self, ctx: RunContext):
        """Called when the user wants to end the call"""
        await ctx.wait_for_playout() # let the agent finish speaking

        await hangup_call()
t
Short answer: Yes — calling
delete_room
requires the agent (or the code calling the API) to have room-admin permissions. If the token or service account used to call
ctx.api.room.delete_room(...)
does not include the RoomAdmin grant (or equivalent server permission), the delete will fail. What to do: • Ensure the agent’s dispatch/token includes RoomAdmin (or grant delete permission) when you need it to call
delete_room
. • If you prefer least-privilege, grant only the specific permissions required (e.g., room deletion/management) instead of full RoomAdmin. • Confirm the call is made from a job/context that has access to the RoomService client (like
ctx.api
in job context). If
get_job_context()
is None or the token lacks the grant, the API call will be rejected. If you want, I can: • Show an example dispatch/token payload that includes RoomAdmin, or • Show a minimal-permissions alternative that still allows room deletion. Which would you prefer?
e
At what stage do I need to create this permission - do I do it in the JobContext?
t
Great question — you must grant the room-management permission when you mint the agent’s access token (or when you create the dispatch), not from inside a running JobContext. Key points • Permissions are encoded in the access token or dispatch configuration at creation time. The runtime JobContext inherits whatever grants were baked into that token. • If the token used to start the agent/dispatch does not include RoomAdmin (or an equivalent delete/manage-room grant), calls like
ctx.api.room.delete_room(...)
will be rejected at runtime. • JobContext is where you call the API, but it cannot add permissions — it only uses the permissions the token already provides. Recommended approach • When you create the agent dispatch or generate an access token for the agent, include the RoomAdmin grant (or the specific room-management grant you need). • If you want least-privilege, add only the specific management capabilities (delete room, manage participants, start/stop egress) instead of full RoomAdmin. Example (high-level) • When building the token/dispatch request, include room admin in the video grants or explicit RoomAdmin flag. For example, set RoomAdmin(true) or add the room-management grant in the dispatch/token payload so the agent’s job runs with those rights. If you’d like, I can: • Show a concrete token/dispatch example in Python or Node.js that includes RoomAdmin, or • Show a minimal set of grants that allow
delete_room
without full admin. Which would you prefer?
e
But this is not possible for a SIP Trunk with a dispatch rule to add an agent is it?
t
Short answer: You cannot add RoomAdmin permissions at the moment of a SIP inbound call itself — the agent only has the permissions that were assigned when its dispatch/token was created. For SIP trunk dispatches that add an agent, you must ensure the dispatched agent’s token or dispatch configuration already includes the required room-management grants (e.g., RoomAdmin or the specific delete/manage grants) before the SIP call triggers the agent. Key points: • Permissions are granted when the agent/dispatch token is minted or when the dispatch is created, not at SIP call time or from inside the running JobContext. • A SIP trunk dispatch rule that spawns an agent will use the agent configuration/token you provided; if that config lacks RoomAdmin (or the specific management grants),
delete_room
and other room-admin actions will fail. • If your workflow for SIP inbound calls doesn’t let you supply a custom dispatch token with elevated grants, then the dispatched agent won’t be able to perform admin actions — you’ll need an alternative: ◦ Pre-create the dispatch/agent with the correct grants (RoomAdmin or specific management grants), or ◦ Perform room-management actions from a separate service account or backend that has the necessary grants and can be invoked when the SIP call ends. If you tell me how your SIP dispatch is configured (are you using a pre-provisioned agent/dispatch token, or letting the SIP rule create agents dynamically?), I can suggest the exact practical setup (example dispatch/token payload or backend pattern) to allow room deletion.
e
If this is my rule in LiveKit cloud
Copy code
{
  "rule": {
    "dispatchRuleIndividual": {
      "roomPrefix": "call-"
    }
  },
  "name": "My dispatch rule",
  "roomConfig": {
    "agents": [
      {
        "agentName": "inbound-agent",
        "metadata": "job dispatch metadata"
      }
    ]
  }
}
Is there a way to ensure that the agent gets the permissions?
t
Thanks — good question. The dispatch rule JSON you posted does not by itself grant RoomAdmin or room-management permissions. Permissions must be assigned when the agent’s token/dispatch is created. What you can do • Pre-provision the dispatched agent with a dispatch/token that includes the required grants (e.g., RoomAdmin or specific room-management grants). The SIP dispatch will use that agent configuration and inherit its permissions. • If your provisioning flow supports it, include the admin grant in the agent entry when you create the dispatch via API or UI (not all UIs show this field; use the API if needed). • If you cannot attach elevated grants to agents created by the SIP rule, run room-management actions (delete room, stop egress) from a separate backend/service account that does have RoomAdmin and is invoked when the SIP call ends. Practical next steps • If you create the dispatch via API, include the agent token or grant flags that enable RoomAdmin (or the specific minimal grants for delete/manage). I can show an example API payload for that if you want. • If you use the Cloud UI and it doesn’t expose grant settings, pre-create the agent/dispatch programmatically with the needed grants, then reference that agent in your rule. • Alternatively, implement a lightweight backend endpoint (with RoomAdmin) that your agent or job calls to delete the room when the participant leaves. Would you like an example dispatch/token payload in Python or Node.js that includes RoomAdmin, or help checking whether your current cloud UI flow can set these grants?
e
Is one way around it to give my LiveKit agent an API token with admin permissions to call this? Is that insecure or is this a recommended way?
t
Short answer: yes—you can give the agent an API token with admin permissions so it can call
delete_room
, but it’s safer to follow least‑privilege practices instead of handing full admin keys to the agent. What to consider • Security risk: A token with full RoomAdmin lets whoever holds it manage/delete rooms, control egress, etc. If that token is leaked or misused, it can be abused across rooms the token covers. • When permissions are applied: Grants are baked into the token/dispatch at creation time. JobContext cannot add permissions later. • Recommended principle: Grant the minimal permissions required (e.g., a specific room‑management grant or scoped RoomAdmin for only the rooms the agent will use) rather than an unrestricted admin token. Practical options (ranked by safety) • Preferred: Mint a scoped token that includes only the specific room-management grant(s) you need (or RoomAdmin scoped to a limited set of rooms). Use that token for the dispatched agent. • Alternative: Keep agent token minimal and perform sensitive actions (delete_room) from a separate backend/service account that holds RoomAdmin. The agent signals that backend (webhook/job) when it’s time to delete the room. • Least safe (but workable): Give the agent an API token with full RoomAdmin. Acceptable for quick tests or tightly controlled environments, but not recommended for production. How to implement • When creating the dispatch or minting the agent token, include the required grant(s) in the token payload (or dispatch config). The agent’s runtime will then be able to call
ctx.api.room.delete_room(...)
. • If you need an example token/dispatch payload that grants just the deletion capability (or RoomAdmin), tell me your preferred language (Python or Node) and I’ll show it. If you want, I can: • Show a minimal grant example that allows
delete_room
without giving full admin, or • Show the full RoomAdmin token example and note where to restrict scope. Which would you like?
e
I will solve this problem another time - how do I simply just disconnect the agent when a participant leaves? I assume the web RTC room closes on idle as I set it to within 5 mins
t
Thanks — I’ll keep this short and practical. You can simply disconnect the agent when the participant leaves and rely on the room idle timeout to close the WebRTC room. Add a handler that listens for participant disconnects and calls
room.disconnect()
(or
ctx.room.disconnect()
from a job context). Await the call to ensure clean teardown. Here’s a minimal Python patch you can drop into your existing DynamicAgent module. It uses the session/room on‑event and disconnects the agent when no human participants remain:
Copy code
import asyncio
from typing import Optional
from livekit.agents import Agent, ChatContext

class DynamicAgent(Agent):
    def __init__(
        self,
        instructions: str,
        on_enter_instructions: Optional[str] = None,
        chat_ctx: Optional[ChatContext] = None,
    ) -> None:
        super().__init__(instructions=instructions, chat_ctx=chat_ctx)
        self._on_enter_instructions = on_enter_instructions

    async def on_enter(self) -> None:
        if self._on_enter_instructions:
            await self.session.generate_reply(instructions=self._on_enter_instructions)
        else:
            await self.session.generate_reply()

    async def participant_disconnected(self, participant) -> None:
        # brief pause to let server state settle
        await asyncio.sleep(0.1)

        # Get current participants from the room context
        participants = getattr(self.session.room, "remote_participants", {})
        # If your runtime exposes a different attribute, adjust accordingly.

        # If no non-agent participants remain, disconnect the agent
        has_non_agent = any(
            p.kind != p.ParticipantKind.PARTICIPANT_KIND_AGENT
            for p in participants.values()
        )
        if not has_non_agent:
            # optional: let agent finish speaking before disconnecting
            # await self.session.wait_for_playout()
            await self.session.room.disconnect()
Notes: • Use
participant_disconnected
as the trigger (it runs when a participant leaves). • Keep the small sleep to let the participant list update. • Optionally call
await self.session.wait_for_playout()
before disconnecting if you want the agent to finish speaking. • The room will auto-close after your configured idle timeout (e.g., 5 minutes) once nobody is left. Would you like me to adapt this to call a final goodbye before disconnecting? Sources: Server lifecycle | LiveKit docs | Job lifecycle | LiveKit docs
e
I see you use asynchio sleep - is this a known problem and should be used everywhere for lifecycle events to avoid race conditions?
t
Good question — short answer: no, you should not sprinkle
asyncio.sleep()
everywhere. Use it only when you need a short, explicit delay for UX/sequencing (e.g., let audio finish) — not as a general fix for race conditions. When to use `asyncio.sleep()`: • To give audio/egress a moment to complete (playout) before a disruptive action (delete room, transfer call). • To allow transient server state (participant lists) to settle in edge cases where you don’t have an event that guarantees ordering. Why avoid using it everywhere: • It masks root-cause ordering issues and can make timing brittle. • It adds unnecessary latency and can hide flakiness in production. Better patterns to prefer: • Await built-in helpers like
await self.session.wait_for_playout()
so the agent finishes speaking deterministically. • Use lifecycle/shutdown hooks (
ctx.add_shutdown_callback()
,
session.shutdown()
, or job shutdown flows) to perform cleanup in the correct phase. • Rely on explicit events (e.g.,
participant_disconnected
) and then inspect the current participant list to decide actions. • Perform idempotent checks (re-check participant state) rather than assuming state after a fixed sleep. • Move sensitive operations (room deletion) to a backend/service with proper permissions — then call that service when appropriate. Practical rule: • Use
asyncio.sleep()
sparingly for UX timing. Prefer explicit waits, shutdown callbacks, and state checks for correctness. If you want, I can review a specific lifecycle handler (yours or another) and show how to replace a sleep with
wait_for_playout()
or a shutdown callback. Sources: Survey Calling Agent | LiveKit docs | Company directory phone assistant | LiveKit docs | Job lifecycle | LiveKit docs | Collect recording consent with tasks | LiveKit docs