When using the livekit voice agents what is the co...
# ask-ai
i
When using the livekit voice agents what is the correct way for the agent to hang up the call on its end? Should it call a tool that calls the
.say()
command and
.shutdown()
or should it say good bye as an assistant message and call a tool that calls
.shutdown()
? We're using the Python SDK
t
Thanks for reaching out, Steven. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
You're right to ask — here's the correct, reliable flow for a LiveKit voice agent (Python SDK) to hang up so the goodbye isn't cut off and the call truly ends for everyone: 1. Have the agent speak the goodbye using
session.say()
or
session.generate_reply()
(so the audio is synthesized and played). 2. Wait for playout to complete with
ctx.wait_for_playout()
so the message finishes playing. 3. Delete the room to end the call for all participants via
ctx.api.room.delete_room(...)
. 4. Optionally call
ctx.shutdown()
after deleting the room. Example:
Copy code
@function_tool
async def end_call(self, ctx: RunContext):
    await ctx.session.generate_reply(instructions="Say goodbye to the user.")
    await ctx.wait_for_playout()  # ensure speech finishes
    await ctx.api.room.delete_room(api.DeleteRoomRequest(room=ctx.room.name))
    # optional: await ctx.shutdown()
Summary: Use `.say()`/`generate_reply()` then
ctx.wait_for_playout()
before deleting the room. Do not simply send an assistant message and immediately call
.shutdown()
— that can cut off audio or leave the room open. Sources: Agent speech and audio | LiveKit docs | Agents telephony integration | LiveKit docs
i
How do we prevent the agent from responding after the tool call to end the call? I'm seeing a LLM call after the end call tool call. The user isn't hearing it but it appears to be using tokens generating it
Copy code
class TelephonyAgentBase(LiveKitAgentBase):
    """Common state and helpers shared by telephony-specific agents."""

    def __init__(
        self,
        *,
        instructions: str,
        job_metadata: AgentJobMetadata,
        chat_ctx: Optional[Any] = None,
    ) -> None:
        super().__init__(
            instructions=instructions,
            job_metadata=job_metadata,
            chat_ctx=chat_ctx,
        )

    async def _end_call(
        self, farewell: Optional[str], status: str, delay_seconds: float
    ) -> None:
        if farewell:
            await self.session.generate_reply(
                instructions=farewell, allow_interruptions=False
            )
        await self.hangup_room(status=status, delay_s=delay_seconds)

    @function_tool(
        description="End the current call with an optional farewell message and status code. Never call more than once per call.",
    )
    async def end_call(
        self,
        context: RunContext[Any],
        status: str = "ended",
        farewell: Optional[str] = None,
        delay_seconds: float = 0.6,
    ) -> None:
        """End the current call by deleting the LiveKit room."""
        await self._end_call(
            farewell=farewell, status=status, delay_seconds=delay_seconds
        )

    async def hangup_room(
        self,
        *,
        status: str = "completed",
        delay_s: float = 0.6,
    ) -> None:
        """Tear down the active LiveKit room."""
        ctx = get_job_context()
        if not ctx:
            logger.warning("No job context available; skipping hangup")
            return

        room_name = ctx.room.name
        if delay_s > 0:
            await asyncio.sleep(delay_s)

        try:
            await ctx.delete_room()
        except Exception:
            logger.exception("Failed to hang up LiveKit room %s", room_name)
            return

        <http://logger.info|logger.info>(f"Room {room_name} hung up with status {status}")
t
Thanks — I’m responding to your question about preventing the LLM from making extra calls after your end-call tool runs. What to do (concise): 1. Ensure the tool returns None (explicitly). ◦ If a tool returns any non-None value, the agent framework will send that to the LLM and may trigger another response. Add
return None
at the end of
end_call
. 1. Make sure the farewell audio is generated and fully played before teardown. ◦ Call
await self.session.generate_reply(...)
(or
say()
), then
await ctx.wait_for_playout()
so the goodbye finishes. 1. Prevent further LLM work by shutting down the agent session before deleting the room. ◦ Call
await self.session.shutdown(drain=False)
(or equivalent) before
await ctx.delete_room()
to stop pending/queued LLM tasks. 1. Keep ordering strict: generate farewell → wait for playout → shutdown session → delete room → return None. Suggested minimal change to your code:
Copy code
@function_tool(...)
async def end_call(...)-> None:
    await self._end_call(farewell=farewell, status=status, delay_seconds=delay_seconds)
    return None
And in
hangup_room
(before
ctx.delete_room()
):
Copy code
if self.session:
    await self.session.shutdown(drain=False)
If you want, share logs showing the LLM request after end_call (timestamped) and I’ll point to where it’s being triggered. Sources: Tool definition and use | LiveKit docs | Agents telephony integration | LiveKit docs | Job lifecycle | LiveKit docs