@function_tool async def end_call(ctx: RunContext,...
# ask-ai
b
@function_tool async def end_call(ctx: RunContext, message_to_speak: str, outcome: str, reason: str): """...""" logger.info(f"Call Ending | Outcome: {outcome} | Reason: {reason} | Spoke: {message_to_speak}") # --- Initiate farewell speech --- try: speech_handle = ctx.session.say(message_to_speak, allow_interruptions=True) except Exception as e: logger.error(f"❌ Failed to initiate end-call speech: {e}") logger.exception(" Exception details:") _shutdown(reason=f"speech_failed: {reason}") return {"outcome": "failure", "reason": f"speech_initiation_error: {e}"} # --- Wait: sets .interrupted flag, does NOT raise on user interruption --- try: await speech_handle except asyncio.CancelledError: # Framework cancelled the task (e.g. shutdown already in progress) # Re-raise — do NOT suppress, do NOT call shutdown here logger.warning("end_call task cancelled by framework — re-raising.") raise # --- Single clean decision point --- if speech_handle.interrupted: logger.info("Farewell interrupted by user — returning to LLM.") return {"outcome": "interrupted", "reason": "user_spoke_over_farewell"} logger.info("Farewell completed — initiating shutdown.") _shutdown(reason=f"Agent ended call: {reason}") return {"outcome": "success", "reason": "call_ended_normally"} def _shutdown(reason: str) -> None: job_ctx = get_job_context() if job_ctx: try: job_ctx.shutdown(reason=reason) except Exception as e: logger.error(f"❌ Shutdown failed: {e}") else: logger.error("❌ No job context — cannot shut down") is this correct way or on ayscio cancelled errror also shoukd I need to raise
✅ 1
t
Thanks for reaching out, Yethu. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Thanks — you're asking whether your
asyncio.CancelledError
handling is correct and if you should re-raise. You're handling the right scenario. What you're doing now is correct: • Log the cancellation for visibility. • Re-raise
asyncio.CancelledError
instead of suppressing it or calling your
_shutdown()
there. Why: a
CancelledError
means the framework is already tearing down the task. Suppressing it or calling
_shutdown()
from inside the cancelled task can cause double-cleanup or interfere with the framework’s lifecycle. Logging + re-raising lets the framework complete shutdown and cleanup reliably. One small suggestion for clarity: catch only
asyncio.CancelledError
(not a broad exception) and keep the handler minimal, e.g.:
Copy code
except asyncio.CancelledError:
    logger.warning("end_call task cancelled — re-raising.")
    raise
That keeps intent explicit and avoids accidentally catching other exceptions. Sources: Workflows | LiveKit Documentation | Function tool definition | LiveKit Documentation | Turns overview | LiveKit Documentation | Python Agents: Handling exceptions in your entrypoint | LiveKit, with interactive elements
b
did notunderstand
t
Thanks — sorry for the confusion. Which part was unclear: the why you should re-raise, what re-raising means, or how to change your code? If helpful, here's a very short plain-summary: • When
asyncio.CancelledError
happens, the framework is already shutting things down. • Do not call your own
_shutdown()
from inside that handler. • Log it if you want, then re-raise so the framework can finish cleanup. Do you want a one-line code example showing exactly where to re-raise? , with interactive elements