How tp end the call automatically after saying som...
# ask-ai
a
How tp end the call automatically after saying something to the user if no activity for 2 minutes
t
Thanks for reaching out, Shajahan. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
You can end the call after 2 minutes of no activity by tracking when the user last spoke and disconnecting the room when silence exceeds 120 seconds. Below is a concise pattern you can implement. What I'll cover: detecting user speech, tracking last-spoke time, and disconnecting after 2 minutes. 1) Key idea • Update a
last_spoke
timestamp whenever the user starts speaking. • Run a background monitor that checks elapsed time and disconnects the room when it exceeds 120 seconds. 2) Minimal example (async pattern)
Copy code
import asyncio
import time

SILENCE_THRESHOLD = 120  # 2 minutes

async def entrypoint(ctx):
    user_last_spoke = time.time()

    @agent.on("user_started_speaking")
    def on_user_started_speaking(_msg):
        nonlocal user_last_spoke
        user_last_spoke = time.time()

    async def monitor():
        while True:
            if time.time() - user_last_spoke > SILENCE_THRESHOLD:
                try:
                    await ctx.room.disconnect()
                except Exception:
                    pass
                break
            await asyncio.sleep(1)

    monitor_task = asyncio.create_task(monitor())
    await agent.start(ctx.room, participant)

    async def on_shutdown():
        if not monitor_task.done():
            monitor_task.cancel()
    ctx.add_shutdown_callback(on_shutdown)
3) Notes and options • Poll interval (here 1s) can be increased to reduce CPU usage. • If you need more robust detection, combine this with voice-activity or LLM-based voicemail detection. • Tell me how you trigger speech events in your setup (server-side agent, client SDK, or webhook) and I’ll adapt this snippet to your environment. Sources: How to detect and handle silence during calls | Agents Framework | livekit/livekit_composite | DeepWiki
a
I use @'server.rtc_session()
t
Thanks — since you use
@server.rtc_session()
, you can use the session's user-state events to detect when the user stops speaking and start a 2‑minute timer that disconnects the room if the user doesn't speak again. Do you want a concise runnable snippet adapted to your code (Python) or a short explanation of the event/timer flow? Sources: Events and error handling | LiveKit docs | Agent server options | LiveKit docs | Job lifecycle | LiveKit docs
a
yes
t
Below is a concise, ready-to-run Python example using
@server.rtc_session()
that speaks once, then monitors user activity and disconnects the room if the user stays silent for 2 minutes. Important points first: • We use
UserStateChangedEvent
(session events) to detect when the user finishes speaking (
new_state == "listening"
). • When that happens we start a 120s timer; if the user does not start speaking again before the timer ends, we call
ctx.room.disconnect()
. • The monitor task is cancelled on shutdown to avoid leaks. Code (adapt to your imports and environment as needed):
Copy code
import asyncio
from livekit.agents import server, AgentSession, JobContext
from livekit.agents.events import UserStateChangedEvent

SILENCE_TIMEOUT = 120.0  # seconds

@server.rtc_session()
async def my_agent(ctx: JobContext):
    # create and start your AgentSession (configure STT/LLM/TTS as required)
    session = AgentSession(...)  # fill with your session config
    await session.start(room=ctx.room)

    async def silence_monitor():
        try:
            while True:
                # Wait for a user_state_changed event
                event: UserStateChangedEvent = await session.wait_for_event("user_state_changed")
                # If user moved to "listening" (finished speaking), start timeout
                if getattr(event, "new_state", None) == "listening":
                    # Wait the silence timeout while allowing interruption if user starts speaking
                    try:
                        await asyncio.wait_for(_wait_for_user_speaking(), timeout=SILENCE_TIMEOUT)
                        # If _wait_for_user_speaking returns, user spoke again; loop continues
                        continue
                    except asyncio.TimeoutError:
                        # No speech for SILENCE_TIMEOUT -> disconnect
                        await ctx.room.disconnect()
                        return
        except asyncio.CancelledError:
            return

    async def _wait_for_user_speaking():
        # Await the next event where user starts speaking
        while True:
            ev: UserStateChangedEvent = await session.wait_for_event("user_state_changed")
            if getattr(ev, "new_state", None) == "speaking":
                return

    monitor_task = asyncio.create_task(silence_monitor())

    # Ensure monitor_task is cancelled on shutdown
    @ctx.on("shutdown")
    def _on_shutdown():
        monitor_task.cancel()

    # Optional: have the agent speak once
    await session.generate_reply(instructions="Hello — I will end the call if you remain silent for 2 minutes.")

    # Wait for session to finish (or be disconnected)
    await session.aclose()
If you’d like, I can: • adapt this snippet to your exact AgentSession constructor arguments, or • provide a variant that polls
session.user_state
instead of using events.
a
@ctx.on("shutdown") ^^^^^^ AttributeError: 'JobContext' object has no attribute 'on'
t
You're seeing that error because
JobContext
doesn't have an
on
method. Use
ctx.add_shutdown_callback(...)
to register shutdown cleanup. Below is a corrected, concise snippet replacing the
@ctx.on("shutdown")
usage with
ctx.add_shutdown_callback
, and ensuring the monitor task is cancelled cleanly.
Copy code
import asyncio
from livekit.agents import server, AgentSession, JobContext
from livekit.agents.events import UserStateChangedEvent

SILENCE_TIMEOUT = 120.0  # seconds

@server.rtc_session()
async def my_agent(ctx: JobContext):
    session = AgentSession(...)  # fill with your session config
    await session.start(room=ctx.room)

    async def _wait_for_user_speaking():
        while True:
            ev: UserStateChangedEvent = await session.wait_for_event("user_state_changed")
            if getattr(ev, "new_state", None) == "speaking":
                return

    async def silence_monitor():
        try:
            while True:
                ev: UserStateChangedEvent = await session.wait_for_event("user_state_changed")
                if getattr(ev, "new_state", None) == "listening":
                    try:
                        await asyncio.wait_for(_wait_for_user_speaking(), timeout=SILENCE_TIMEOUT)
                        continue  # user spoke again
                    except asyncio.TimeoutError:
                        await ctx.room.disconnect()
                        return
        except asyncio.CancelledError:
            return

    monitor_task = asyncio.create_task(silence_monitor())

    # Correct shutdown registration
    def _cleanup():
        if not monitor_task.done():
            monitor_task.cancel()
    ctx.add_shutdown_callback(_cleanup)

    # Optional greeting
    await session.generate_reply(instructions="Hello — I will end the call if you remain silent for 2 minutes.")

    await session.aclose()
If you want, I can: • adapt this to your actual
AgentSession(...)
config, or • provide a variant that polls
session.user_state
instead of waiting for events. Which do you prefer?
a
there is no wait_for_event event: UserStateChangedEvent = await session.wait_for_event("user_state_changed")
t
Thanks — since
wait_for_event
and
ctx.on
aren't available, use
@session.on("user_state_changed")
to reset a single asyncio timer task. Below is a minimal, corrected Python example for
@server.rtc_session()
that: • has the agent speak once, • starts a 120s inactivity timer after speaking, • cancels & restarts the timer when user speaks, • calls
ctx.room.disconnect()
when timer expires, • uses
ctx.add_shutdown_callback
to cancel the timer on shutdown. Adapt the AgentSession constructor and imports to your project.
Copy code
import asyncio
from livekit.agents import server, AgentSession, JobContext, UserStateChangedEvent

SILENCE_TIMEOUT = 120.0  # seconds

@server.rtc_session()
async def my_agent(ctx: JobContext):
    session = AgentSession(...)  # fill in your config
    await session.start(room=ctx.room)

    inactivity_task: asyncio.Task | None = None

    def start_inactivity_timer():
        nonlocal inactivity_task
        if inactivity_task and not inactivity_task.done():
            inactivity_task.cancel()
        inactivity_task = asyncio.create_task(_inactivity_wait())

    async def _inactivity_wait():
        try:
            await asyncio.sleep(SILENCE_TIMEOUT)
            await ctx.room.disconnect()
        except asyncio.CancelledError:
            return

    @session.on("user_state_changed")
    def _on_user_state_changed(ev: UserStateChangedEvent):
        # user started speaking -> reset timer so we don't disconnect
        if getattr(ev, "new_state", None) == "speaking":
            if inactivity_task and not inactivity_task.done():
                inactivity_task.cancel()
        # user finished speaking -> start timer
        elif getattr(ev, "new_state", None) == "listening":
            start_inactivity_timer()
        # optional: handle away state immediately
        elif getattr(ev, "new_state", None) == "away":
            asyncio.create_task(ctx.room.disconnect())

    # ensure cleanup on shutdown
    def _cleanup():
        nonlocal inactivity_task
        if inactivity_task and not inactivity_task.done():
            inactivity_task.cancel()
    ctx.add_shutdown_callback(_cleanup)

    # have the agent speak, then start the inactivity timer
    await session.generate_reply(instructions="Hello — I will end the call if you remain silent for 2 minutes.")
    start_inactivity_timer()

    await session.aclose()
If you want, tell me your AgentSession constructor args (STT/TTS/LLM names) and I’ll plug them into this snippet. Sources: Events and error handling | LiveKit docs | Agent session | LiveKit docs
a
what is the difference between ctx.room.disconnect(), session.aclose() and ctx.shutdown()?
t
Good question — here’s a concise breakdown of what each call does and when to use it. • ctx.room.disconnect() ◦ What it does: Disconnects the agent’s participant from the LiveKit room (closes WebRTC/WebSocket). ◦ When to use: You want the agent to leave the call but not necessarily tear down the agent session or job. Use this for ending the network connection only (e.g., after a timeout). • session.aclose() ◦ What it does: Gracefully stops the AgentSession’s processing loop (STT/TTS/LLM I/O), flushes any in-progress turns/transcripts, and stops the session’s internal work. ◦ When to use: You want to stop the agent’s speech/transcription/logic while the room connection may remain. Use this to shut down the session-level processing. • ctx.shutdown() ◦ What it does: High-level job shutdown. Performs full, graceful cleanup of the agent job: it coordinates shutdown of sessions, drains pending operations, runs shutdown hooks, and typically results in the session and room being cleaned up as well. ◦ When to use: You want to fully terminate the agent job/process and ensure all cleanup runs (preferred for ending the whole lifecycle). Quick guidance: • To just drop the call: use ctx.room.disconnect(). • To stop agent processing but keep network state separate: use await session.aclose(). • To fully end the job and run all cleanup: use await ctx.shutdown(). If you tell me the exact flow you want (e.g., agent says message → wait 2 minutes silence → fully stop agent vs just leave room), I’ll recommend the exact sequence to call. Sources: How to detect and handle silence during calls | How to capture real-time agent transcriptions during conversations | Job lifecycle | LiveKit docs
a
which is preferred
t
For automatic call termination after inactivity, prefer calling ctx.shutdown() (full job shutdown). Why: • It performs a complete, graceful cleanup (stops session processing, runs shutdown hooks, and ensures resources are released). • It coordinates session and room teardown, avoiding partial shutdown states or leaked tasks. When you might choose the others: • Use ctx.room.disconnect() if you only want the agent to leave the LiveKit room but keep the session/job running for other work. • Use await session.aclose() if you want to stop the AgentSession’s processing (STT/TTS/LLM) but keep the job or connection logic alive for other purposes. Recommended sequence for your silence timeout flow: 1. Detect silence timeout. 2. Call await ctx.shutdown() to fully terminate and clean up. If you want, I can show a short code snippet that calls ctx.shutdown() from the inactivity timer.