async def *hangup_call*(): """End the call by ...
# ask-ai
s
async def *hangup_call*(): """End the call by gracefully shutting down and deleting the room.""" ctx = get_job_context() if ctx is None: logger.warning("hangup_call called but no job context available") return logger.info(f"Hanging up call - shutting down job for room {ctx.room.name}") _try_: await ctx.wait_for_playout() ctx.shutdown() # Ensure the room is deleted promptly after graceful shutdown. await ctx.api.room.delete_room( api.DeleteRoomRequest(room=ctx.room.name) ) except Exception as e: logger.error(f"Error during hangup/shutdown: {e}") class Assistant(Agent): def *__init__*( self, _instructions_: Optional[str] = None, _inactivity_timeout_: float = 15.0, _extended_inactivity_timeout_: float = 45.0, ) -> None: prompt_text = instructions super().__init__(instructions=prompt_text) self._last_dtmf_time: float = 0 # Inactivity detection settings self._inactivity_timeout = _inactivity_timeout_ # Normal timeout (seconds) self._extended_inactivity_timeout = _extended_inactivity_timeout_ _# After skip_turn_ self._use_extended_timeout = False # Flag to use extended timeout self._inactivity_prompt_count = 0 # Number of "are you there" prompts sent self._max_inactivity_prompts = 2 # Max prompts before hanging up @function_tool() async def *send_dtmf*( self, _digit_: Annotated[str, Field(description="Single DTMF digit to send (0-9, *, or #)")], ) -> str: """ Send a DTMF tone to navigate IVR/phone menus. Call this when you need to press a phone key to select a menu option (e.g., "press 1 for sales"). Only send ONE digit at a time. Wait for the IVR response before pressing the next key. """ # Cooldown to prevent rapid DTMF presses (3 seconds as per IVR navigator example) current_time = time.time() if current_time - self._last_dtmf_time < 3: logger.info("DTMF rejected due to cooldown") return "Please wait a moment before pressing another key" self._last_dtmf_time = current_time ctx = get_job_context() if ctx is None: return "Cannot send DTMF - no active call" # Validate digit _it is saying that ctx does not have attribute wait_for_playout()_
t
Thanks for reaching out, Ayush. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Thanks — I see you're hitting an error because the code calls ctx.wait_for_playout(), but ctx (a JobContext) doesn't have that method. I'll explain why that happens and show a safe fix you can apply. Summary of the issue • You're calling await ctx.wait_for_playout(), but JobContext does not implement wait_for_playout(). • The method you want to await belongs to a SpeechHandle (returned by session.generate_reply() / session.say() or available on the participant as a current_speech handle). What to do instead • Before shutting down, find the active SpeechHandle and await its wait_for_playout() so any ongoing playback finishes. • If you don't have a SpeechHandle, skip waiting and proceed to shutdown/delete. Suggested corrected hangup_call implementation 1. Attempt to get the current speech handle from the room's local participant and await its playout. 2. Then call ctx.shutdown() and delete the room. Example (apply to your code style):
Copy code
async def hangup_call():
    """End the call by gracefully shutting down and deleting the room."""
    ctx = get_job_context()
    if ctx is None:
        logger.warning("hangup_call called but no job context available")
        return

    <http://logger.info|logger.info>(f"Hanging up call - shutting down job for room {ctx.room.name}")

    try:
        # Get the current SpeechHandle (if any) and wait for playout
        speech_handle = None
        if hasattr(ctx, "room") and ctx.room is not None:
            # local_participant.current_speech is where the active SpeechHandle is stored
            speech_handle = getattr(ctx.room.local_participant, "current_speech", None)

        if speech_handle:
            await speech_handle.wait_for_playout()

        ctx.shutdown()

        # Ensure the room is deleted promptly after graceful shutdown.
        await ctx.api.room.delete_room(
            api.DeleteRoomRequest(room=ctx.room.name)
        )
    except Exception as e:
        logger.error(f"Error during hangup/shutdown: {e}")
Notes and alternatives • If you obtain SpeechHandle directly when you call
session.say()
or
session.generate_reply()
(e.g.,
handle = await session.say(...)
), you can store that handle in your agent/session context and await
handle.wait_for_playout()
during hangup instead of reading
current_speech
. • If this function may be called from different contexts (e.g., from a function tool where you have a
context.session
object), use the session-provided
current_speech
there. • If you only need to force immediate shutdown (skip waiting for audio), you can call
ctx.shutdown()
right away and delete the room — just be aware that any in-flight speech may be cut off. If you'd like, share: • How you obtain/track the SpeechHandle in your flow (do you call
session.say()
or
session.generate_reply()
?), or • A short snippet showing where you set
current_speech
, and I can adapt the snippet to your exact setup. Sources: Agents telephony integration | LiveKit Documentation | Agent speech and audio | LiveKit Documentation
s
async def *hangup_call*(): """End the call by gracefully shutting down and deleting the room.""" ctx = get_job_context() if ctx is None: logger.warning("hangup_call called but no job context available") return logger.info(f"Hanging up call - shutting down job for room {ctx.room.name}") _try_: speech_handle = None if hasattr(ctx, "room") and ctx.room is not None: _#ctx.room.local_participant.current_speech is where the active SpeechHandle is stored_ speech_handle = getattr(ctx.room.local_participant, "current_speech", None) if speech_handle: await speech_handle.wait_for_playout() ctx.shutdown() # Ensure the room is deleted promptly after graceful shutdown. await ctx.api.room.delete_room( api.DeleteRoomRequest(room=ctx.room.name) ) except Exception as e: logger.error(f"Error during hangup/shutdown: {e}") ai_agent-v1.1.3h1nwfiwx86n@ayush-vm | {"level": "INFO", "name": "agent", "message": "Hanging up call - shutting down job for room aks-test-room-127", "pid": 39, "job_id": "AJ_PpF2CsWkxMwQ", "room_id": "RM_xtJP2EZzBQZd", "timestamp": "2026-02-25T070526.535126+00:00"} ai_agent-v1.1.3h1nwfiwx86n@ayush-vm | {"level": "ERROR", "name": "agent", "message": "Error during hangup/shutdown: cannot access local participant before connecting", "pid": 39, "job_id": "AJ_PpF2CsWkxMwQ", "room_id": "RM_xtJP2EZzBQZd", "timestamp": "2026-02-25T070526.535302+00:00"} ai_agent-v1.1.3h1nwfiwx86n@ayush-vm | {"level": "WARNING", "name": "livekit.agents", "message": "The job task completed without establishing a connection or performing a proper shutdown. Ensure that job_ctx.connect()/job_ctx.shutdown() is called and the job is correctly finalized.", "pid": 39, "job_id": "AJ_PpF2CsWkxMwQ", "room_id": "RM_xtJP2EZzBQZd", "timestamp": "2026-02-25T070526.536631+00:00"} Now i am getting this error
t
Thanks — that error message explains what's happening: your code is trying to read
ctx.room.local_participant
before the agent has connected, and that property isn't available until after a successful connect. I'll walk through a safe, clear fix and a couple of alternatives so you can choose what fits your flow. What caused the error •
cannot access local participant before connecting
happens because
ctx.room.local_participant
is only valid after the session connects. • Also,
current_speech
is a property of the AgentSession (or the SpeechHandle you obtained when calling
session.say()
/
session.generate_reply()
), not something you should rely on via
local_participant
before connection. Recommended approach (safe ordering) 1. Keep a reference to the active AgentSession or any SpeechHandle when you create it, and store that reference in the job context (for example
ctx.userdata["session"]
or
ctx.userdata["current_speech"]
). 2. On hangup: ◦ If you have a SpeechHandle, await its
wait_for_playout()
to let audio finish. ◦ Call the session shutdown/drain to gracefully close audio/speech. ◦ Await the job context close/shutdown. ◦ Delete the room only after shutdown completes. Example implementation you can adapt:
Copy code
async def hangup_call():
    """End the call by gracefully shutting down and deleting the room."""
    ctx = get_job_context()
    if ctx is None:
        logger.warning("hangup_call called but no job context available")
        return

    <http://logger.info|logger.info>(f"Hanging up call - shutting down job for room {ctx.room.name}")

    try:
        # Get stored session or speech handle from context userdata (store these at session creation)
        session = None
        speech_handle = None
        if hasattr(ctx, "userdata"):
            session = ctx.userdata.get("session")
            speech_handle = ctx.userdata.get("current_speech")

        # If you have a SpeechHandle, wait for playout
        if speech_handle:
            await speech_handle.wait_for_playout()

        # If you have the AgentSession, request graceful shutdown that drains queued speech
        if session:
            # If the session provides a drain option, use it. Otherwise call its shutdown API.
            try:
                session.shutdown(drain=True)
            except Exception:
                # Fallback to calling shutdown without drain if drain option not present
                session.shutdown()

        # Ensure the job context fully closes/shuts down
        # Use ctx.aclose() or ctx.shutdown() depending on your environment (await the appropriate method)
        if hasattr(ctx, "aclose"):
            await ctx.aclose()
        else:
            ctx.shutdown()

        # Only delete the room after the session and ctx are shut down
        await ctx.api.room.delete_room(api.DeleteRoomRequest(room=ctx.room.name))

    except Exception as e:
        logger.error(f"Error during hangup/shutdown: {e}")
How to store the session/speech handle when you create them • When you create the AgentSession or call
session.say()
/
session.generate_reply()
, save references:
Copy code
# Example when creating session:
session = AgentSession(...)
ctx.userdata["session"] = session

# Example when calling say/generate_reply:
speech_handle = await session.say("Goodbye")
ctx.userdata["current_speech"] = speech_handle
Alternative quick fixes (use with caution) • If you don't have the session or speech handle available, skip accessing
local_participant
entirely and proceed to shutdown/delete the room. Note: this may cut off in-flight audio. • Before reading
ctx.room.local_participant
, check a connection flag or only access it after
ctx.connect()
has executed successfully. Why this will fix the error • You avoid accessing
local_participant
before connect. • You await the SpeechHandle's playout via
wait_for_playout()
which is the correct object/method to wait on. • You shut down the session and await ctx close before deleting the room, preventing race conditions and premature resource deletion. If you want, next I can: • Adapt the example to the exact methods your AgentSession exposes (share the session creation snippet or method names you use). • Show a minimal entrypoint example that stores session and speech handles in
ctx.userdata
so hangup_call can always find them. Sources: Agent speech and audio | LiveKit Documentation | Job lifecycle | LiveKit Documentation