incalculable-address-14672
12/15/2025, 10:37 PM.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 SDKtall-airline-16756
12/15/2025, 10:37 PMtall-airline-16756
12/15/2025, 10:38 PMsession.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:
@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 docsincalculable-address-14672
12/15/2025, 10:41 PMclass 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}")tall-airline-16756
12/15/2025, 10:41 PMreturn 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:
@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()):
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