glamorous-oil-12326
02/18/2026, 3:18 AMtall-airline-16756
02/18/2026, 3:18 AMtall-airline-16756
02/18/2026, 3:18 AMon_participant_connected handler) or creating a SIP participant with wait_until_answered=True, put a 2-second async delay right after you detect the connection and before you send/play the greeting.
Two common patterns:
1) When handling participant-connected events:
@room.on("participant_connected")
async def on_participant_connected(participant: rtc.RemoteParticipant):
# Wait 2 seconds after participant joins
await asyncio.sleep(2)
# Then send / play the greeting
await session.generate_reply(instructions="Greet the user warmly and offer assistance.")
2) When placing an outbound SIP call and waiting until answered:
await ctx.api.sip.create_sip_participant(
api.CreateSIPParticipantRequest(
room_name=ctx.room.name,
sip_trunk_id='your-trunk-id',
sip_call_to=phone_number,
participant_identity=phone_number,
wait_until_answered=True
)
)
# Delay the greeting by 2 seconds after call is answered
await asyncio.sleep(2)
await session.generate_reply(instructions="Hello, how can I help you?")
If you’d like, tell me which approach you’re using (event handler vs. creating SIP participant), which language/runtime you’re running, and whether the greeting is generated TTS or pre-recorded audio. I can then provide a tailored code snippet or show how to play a recorded file after the delay.
Sources: SIP Lifecycle Management Agent | LiveKit Documentation | Agents telephony integration | LiveKit Documentationglamorous-oil-12326
02/18/2026, 3:20 AMtall-airline-16756
02/18/2026, 3:21 AMuser_started_speaking / user_state_changed).
3. If the user speaks within 2 seconds:
◦ Cancel the timer and immediately call your reply/send-greeting routine.
1. If the timer completes without speech:
◦ Play or generate the greeting.
If you want, I can provide:
• A complete Python example using asyncio and user_state_changed.
• A Node.js example using Promises and the equivalent speaking event.
• Extra detail on enabling VAD and making sure only one response plays (race/cancel handling).
Tell me which language and whether VAD is available, and I’ll send the tailored code snippet.
Sources: Turns overview | LiveKit Documentation | Agent Builder | LiveKit Documentation | Agent speech and audio | LiveKit Documentation | How does LiveKit Agents compare to Pipecat? | LiveKit | Agents telephony integration | LiveKit Documentationglamorous-oil-12326
02/18/2026, 3:21 AMtall-airline-16756
02/18/2026, 3:21 AMimport asyncio
# Assume session is your AgentSession with VAD enabled and TTS available.
# This function runs after call is answered / participant connected.
async def conditional_greeting(session):
delay = 2.0
delay_task = None
async def delayed_greet():
try:
await asyncio.sleep(delay)
# No user speech detected within 2s -> generate greeting
await session.generate_reply(instructions="Hello! How can I help you today?")
except asyncio.CancelledError:
# Delay cancelled because user spoke
return
# Handler for VAD/user speaking event
@session.on("user_state_changed")
async def _on_user_state_changed(ev):
nonlocal delay_task
# Adjust the speaking-state check to match your event shape
if getattr(ev, "new_state", None) == "speaking":
# User started speaking within the window -> cancel delay and reply now
if delay_task and not delay_task.done():
delay_task.cancel()
# Immediately respond to the user's input; avoid awaiting here
# to keep event loop responsive, create a task for the reply
asyncio.create_task(
session.generate_reply(instructions="Respond to user's speech immediately.")
)
# Start the delayed greeting
delay_task = asyncio.create_task(delayed_greet())
# Optionally: await session lifecycle if needed, or return so caller continues.
# If you need to wait until the greeting or reply finishes, you can await delay_task.
Notes and tips
• Use asyncio.create_task for immediate replies so the event handler returns quickly and the loop stays responsive.
• The user_state_changed event above expects ev.new_state == "speaking". If your VAD event uses a different field or event name (e.g., user_started_speaking), adjust accordingly.
• If you want the immediate reply to take the user’s utterance into account (e.g., include recently captured audio for context), ensure your STT pipeline captures the user audio and is available to session.generate_reply.
• To avoid duplicate replies in edge cases, check whether a reply is already in progress (for example, track a boolean reply_in_progress) before starting another generate_reply.
If you’d like, I can:
• Adapt this snippet to your exact event object shape (paste a sample user_state_changed event structure).
• Show how to include the user’s immediate audio/text as context in session.generate_reply.glamorous-oil-12326
02/18/2026, 3:23 AMtall-airline-16756
02/18/2026, 3:23 AMwait_until_answered=True when creating the SIP participant or listen for the participant-connected event).
• Start the 2s conditional timer only after you detect the participant is connected/answered.
• Listen for VAD user_state_changed (speaking) during that 2s window and cancel the delay if the user speaks.
• Generate the greeting immediately if speech is detected, otherwise generate it when the timer completes.
Example (Python, asyncio) — adapt names to your codebase:
import asyncio
async def place_outbound_and_conditional_greet(ctx, phone_number, session):
# Create outbound SIP participant and wait until it's answered
await ctx.api.sip.create_sip_participant(
api.CreateSIPParticipantRequest(
room_name=ctx.room.name,
sip_trunk_id='your-trunk-id',
sip_call_to=phone_number,
participant_identity=phone_number,
wait_until_answered=True # ensures this call returns after answer
)
)
# At this point the remote participant has answered / connected.
# Start the conditional greeting logic now.
delay = 2.0
delay_task = None
greeting_sent = False
reply_lock = asyncio.Lock() # prevent duplicate replies
async def delayed_greet():
nonlocal greeting_sent
try:
await asyncio.sleep(delay)
async with reply_lock:
if not greeting_sent:
greeting_sent = True
await session.generate_reply(instructions="Hello! How can I help you today?")
except asyncio.CancelledError:
return
@session.on("user_state_changed")
async def _on_user_state_changed(ev):
nonlocal delay_task, greeting_sent
# adjust this check if your event uses a different field
if getattr(ev, "new_state", None) == "speaking":
# User spoke within the window -> cancel delay and reply immediately
if delay_task and not delay_task.done():
delay_task.cancel()
async with reply_lock:
if not greeting_sent:
greeting_sent = True
# If you want the reply to use captured user audio/text, include it here
asyncio.create_task(
session.generate_reply(instructions="Respond to the user's speech right away.")
)
# Start the delayed greeting after the call was answered
delay_task = asyncio.create_task(delayed_greet())
# Optionally await the greeting to finish or return so other session work continues
# await delay_task
If you cannot or do not want to use wait_until_answered=True, instead:
• Create the SIP participant without waiting.
• Subscribe to the participant-connected event for that participant (or to room participant_connected) and start the conditional greeting from that event handler when you see the specific outbound participant connect.
Would you like the version that starts from a participant-connected event instead of wait_until_answered? If so, paste the shape of your participant connected event (or the field that identifies your outbound participant) and I’ll adapt the handler to start the 2s window only for that participant.glamorous-oil-12326
02/18/2026, 3:34 AMtall-airline-16756
02/18/2026, 3:35 AMparticipant_connected can happen when the dial starts, so you must start the 2‑second conditional greeting only when the SIP call is actually answered (when sip.callStatus becomes active).
Below is a focused Python pattern you can drop into your outbound flow. It assumes:
• You have VAD enabled and the user_state_changed event is fired when the remote participant starts speaking.
• Your code can observe participant_attributes_changed (or similar) and inspect sip.callStatus.
• Greetings are generated at runtime with session.generate_reply().
What it does
• Listens for participant_attributes_changed for SIP participants and starts the 2s window only when sip.callStatus == "active".
• Starts a 2s timer that will send the greeting if no speech is detected.
• Cancels the timer and responds immediately if VAD signals the participant started speaking within the window.
• Uses a lock/flag to avoid duplicate replies.
Example (Python, asyncio):
import asyncio
async def outbound_conditional_greeting(ctx, phone_number, session):
# Create SIP participant (no assumptions about wait_until_answered)
await ctx.api.sip.create_sip_participant(
api.CreateSIPParticipantRequest(
room_name=ctx.room.name,
sip_trunk_id='your-trunk-id',
sip_call_to=phone_number,
participant_identity=phone_number,
# you can set wait_until_answered=False or True depending on flow;
# this example relies on sip.callStatus updates instead of wait_until_answered
)
)
greeting_delay = 2.0
greeting_sent = False
greeting_task: asyncio.Task | None = None
reply_lock = asyncio.Lock()
async def delayed_greet():
nonlocal greeting_sent
try:
await asyncio.sleep(greeting_delay)
async with reply_lock:
if not greeting_sent:
greeting_sent = True
await session.generate_reply(instructions="Hello! How can I help you today?")
except asyncio.CancelledError:
return
@session.on("user_state_changed")
async def on_user_state_changed(ev):
nonlocal greeting_sent, greeting_task
# adapt field name if your event uses a different attribute
if getattr(ev, "new_state", None) == "speaking":
# User spoke during the window -> cancel timer and respond immediately
if greeting_task and not greeting_task.done():
greeting_task.cancel()
async with reply_lock:
if not greeting_sent:
greeting_sent = True
# Use create_task so the handler returns quickly
asyncio.create_task(
session.generate_reply(instructions="Responding to your question right away.")
)
# Room-level handler: look for sip.callStatus -> "active"
@ctx.room.on("participant_attributes_changed")
async def on_participant_attributes_changed(participant, attributes):
nonlocal greeting_task, greeting_sent
# Only care about SIP participants (adjust check to your participant object)
if getattr(participant, "kind", None) != "sip":
return
new_status = attributes.get("sip.callStatus")
# When status becomes "active", start the 2-second conditional window
if new_status == "active" and not greeting_sent:
# Start the timer only when call is truly answered (sip.callStatus == "active")
greeting_task = asyncio.create_task(delayed_greet())
Notes and tips
• If your SDK represents participant kind or events differently, replace "sip" and event argument shapes with your actual objects/fields.
• If you want to include the user’s immediate speech content in the immediate reply, ensure your STT buffer is accessible to session.generate_reply() and include that transcription in the request.
• Keep the event handler lightweight; use asyncio.create_task for longer operations so the event loop stays responsive.
• Use the reply lock and greeting_sent flag to avoid duplicate replies in race scenarios.
If you’d like, I can:
• Adapt this snippet to the exact shapes your SDK returns (paste a real participant_attributes_changed payload and the user_state_changed event object).
• Show an alternate flow that uses wait_until_answered=True plus attribute polling if you prefer that pattern. Sources: SIP Lifecycle Management Agent | LiveKit Documentation