early-butcher-66889
12/24/2025, 9:09 AMtall-airline-16756
12/24/2025, 9:09 AMtall-airline-16756
12/24/2025, 9:09 AMon_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 docsearly-butcher-66889
12/24/2025, 9:11 AMtall-airline-16756
12/24/2025, 9:11 AMgenerate_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 docsearly-butcher-66889
12/24/2025, 9:21 AMtall-airline-16756
12/24/2025, 9:22 AMdisconnected 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):
@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 docsearly-butcher-66889
12/24/2025, 9:22 AMtall-airline-16756
12/24/2025, 9:23 AMearly-butcher-66889
12/24/2025, 9:24 AM"""
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()tall-airline-16756
12/24/2025, 9:24 AMDynamicAgent 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):
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?early-butcher-66889
12/24/2025, 9:32 AMtall-airline-16756
12/24/2025, 9:32 AMcanPublish, 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 docsearly-butcher-66889
12/24/2025, 9:46 AM# 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()tall-airline-16756
12/24/2025, 9:47 AMdelete_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?early-butcher-66889
12/24/2025, 9:47 AMtall-airline-16756
12/24/2025, 9:48 AMctx.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?early-butcher-66889
12/24/2025, 9:48 AMtall-airline-16756
12/24/2025, 9:48 AMdelete_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.early-butcher-66889
12/24/2025, 9:51 AM{
"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?tall-airline-16756
12/24/2025, 9:51 AMearly-butcher-66889
12/24/2025, 9:52 AMtall-airline-16756
12/24/2025, 9:52 AMdelete_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?early-butcher-66889
12/24/2025, 9:58 AMtall-airline-16756
12/24/2025, 9:58 AMroom.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:
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 docsearly-butcher-66889
12/24/2025, 10:00 AMtall-airline-16756
12/24/2025, 10:00 AMasyncio.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