abundant-alligator-24067
03/26/2026, 6:48 PMtall-airline-16756
03/26/2026, 6:48 PMtall-airline-16756
03/26/2026, 6:49 PMon_exit vs add_shutdown_callback.
Quick lifecycle summary
• An agent server registers with LiveKit and waits for job requests.
• When a user joins a room, the server assigns a job to an agent; that job runs your entrypoint code (the session).
• A session ends when the job completes or the room closes (for example, when the last non-agent participant leaves).
• The framework supports two kinds of shutdown hooks for different purposes and timing: one for agent handoffs and one for final session cleanup.
When to use each method
• on_exit()
◦ Scope: agent-level hook during a multi-agent workflow inside the same session.
◦ When it runs: right before the agent gives control to another agent in the same session (an agent-to-agent handoff).
◦ Typical use: let the departing agent perform agent-specific final actions (e.g., send a final message to the user, save transient agent state needed by the next agent, or tidy up agent-scoped resources).
◦ Example pattern:
async def on_exit(self):
await self.session.generate_reply(instructions="Tell the user a friendly goodbye before you exit.")
• add_shutdown_callback()
◦ Scope: session/job-level hook for final cleanup after the whole session ends.
◦ When it runs: when the session/job is terminating (regardless of how it terminated).
◦ Typical use: persist user state to a database, upload logs/artifacts, or other cleanup that must run once the job is fully finished.
◦ Example pattern:
async def entrypoint(ctx: JobContext):
async def my_shutdown_hook():
# save user state
...
ctx.add_shutdown_callback(my_shutdown_hook)
Key differences (short)
• Purpose: on_exit = prepare for handing off to another agent; add_shutdown_callback = final cleanup after session end.
• Timing: on_exit runs during intra-session agent transitions; shutdown callbacks run when the job/session is ending.
• Scope: on_exit affects the agent workflow; shutdown callbacks affect persistent/session-level cleanup.
Practical notes
• Keep shutdown hooks short: the framework waits about 10 seconds before forcefully terminating the process, so long-running tasks should be designed accordingly.
• Both hooks are complementary: use on_exit for graceful handoff behavior and add_shutdown_callback for final persistence/cleanup.
If you want, tell me:
• whether you’re implementing a multi-agent workflow (so I can give a concrete example of agent handoff), or
• what cleanup steps you need to perform at session end (so I can show a safe pattern for a shutdown callback). Sources: Server lifecycle | LiveKit Documentation | Pipeline nodes and hooks | LiveKit Documentation | Job lifecycle | LiveKit Documentation, with interactive elementsabundant-alligator-24067
03/26/2026, 6:52 PMtall-airline-16756
03/26/2026, 6:52 PMon_exit() to generate and present the summary just before the agent exits the session or hands off. This runs while the session is still active, so you can speak the summary to the user.
• After presenting the summary, call session.shutdown(drain=True) to ensure any spoken audio finishes playing before the session actually closes.
Why this works
• on_exit() is intended for agent-to-agent handoff or when an agent is leaving a session — exactly the right timing to say a final summary.
• add_shutdown_callback() is for final session-level cleanup after the session finishes; it runs too late if you want the user to hear the summary before hangup.
• Use session.shutdown(drain=True) so the framework waits for pending speech (the summary) to complete.
Concise example pattern (pseudo/Python-style)
1. Inside your agent class, implement `on_exit()`:
async def on_exit(self):
# 1) Build a summary from the conversation history
convo = self.session.history # access history
summary_prompt = f"Create a short call summary from this conversation:\n{convo}"
# 2) Generate summary with your LLM method (example)
summary = await self.session.generate_reply(instructions=summary_prompt)
# 3) Present the summary to the user (speak/send)
await self.session.say(summary) # or generate_reply/send as appropriate
# 4) Gracefully shutdown, waiting for speech to finish
await self.session.shutdown(drain=True)
Notes and best practices
• Keep the summary short so it finishes within the shutdown grace period. The runtime will only wait a short time (about 10s) before force-termination, so long runs should be avoided.
• If summary generation might take longer, consider:
◦ Precomputing partial summaries during the call and finalizing in on_exit(), or
◦ Generating a short spoken summary in on_exit() and saving a fuller transcript/summary via add_shutdown_callback() for backend persistence.
• If you prefer a ready-made tool, the EndCallTool can generate a closing response controlled by end_instructions and then end the call; it encapsulates the generate-then-shutdown flow if that fits your needs.
If you want, tell me:
• which API you use to generate LLM replies (so I can tailor the code snippet), and
• whether you want the summary both spoken and posted to chat or saved to a database (so I can show the pattern for both). Sources: Pipeline nodes and hooks | LiveKit Documentation | EndCallTool | LiveKit Documentation | Agent Builder | LiveKit Documentation | Building multi-agent architectures with LiveKit agents | LiveKit | Agents and handoffs | LiveKit Documentation, with interactive elementsabundant-alligator-24067
03/26/2026, 6:53 PMtall-airline-16756
03/26/2026, 6:54 PMon_exit() is not called when the user disconnects unexpectedly. Use shutdown callbacks for cleanup and summary generation when a session ends.
More detail and recommended pattern
• Purpose difference (recap):
◦ on_exit() runs only for agent-to-agent handoffs inside an active session. It’s meant for the departing agent to do final agent-scoped actions before control transfers.
◦ ctx.add_shutdown_callback() registers a session/job-level callback that runs when the session ends, regardless of how it ends (user disconnects, manual shutdown, job completion).
• What happens on user disconnect:
◦ If the user drops or leaves the call, the session can close automatically (depending on room options).
◦ on_exit() will not be invoked for that event.
◦ Any callbacks added with ctx.add_shutdown_callback() will run — this is where you should do final persistence, reporting, and any non-interactive summary generation.
How to present a summary to the caller and still handle disconnects
• If you want the caller to hear a spoken summary before hangup:
◦ Generate and speak the summary from on_exit() only when an agent is intentionally exiting while the session is still active (e.g., you are handing off but the user remains).
◦ After speaking, call session.shutdown(drain=True) so the system waits for speech to finish before disconnecting.
• If the user might disconnect unexpectedly:
◦ Use ctx.add_shutdown_callback() to generate the summary and store/send it (database, webhook, email, etc.). Media output may not be possible if the user already left, but the summary will still be generated and persisted.
◦ To improve chances the user hears a summary before disconnect, consider generating partial/near-real-time summaries during the call (e.g., every N minutes or after key moments) so a short final summary is cheap to produce in the shutdown path.
Practical patterns
• Best-effort spoken summary + reliable persistence:
a. During the call, keep lightweight rolling summary state (update every few minutes).
b. On planned agent exit: in on_exit() assemble a short spoken summary from the rolling state, session.say() or session.generate_reply(), then await session.shutdown(drain=True).
c. Always register a ctx.add_shutdown_callback() to persist the full summary and transcript to your backend (so you don’t lose it if the user disconnects).
• If you prefer a tool-based approach: the EndCallTool can generate a closing message from end_instructions and handle the shutdown flow for you.
If you like, I can:
• Show a concrete code snippet combining a short spoken summary in on_exit() with a persistent save in add_shutdown_callback(), or
• Help design a rolling-summary approach that minimizes final-generation time so you can reliably speak a summary before hangup. Which would you prefer? Sources: Pipeline nodes and hooks | LiveKit Documentation | Job lifecycle | LiveKit Documentation | Why is my Agent insights data missing? | LiveKit | Agent session | LiveKit Documentation | Agent Builder | LiveKit Documentation, with interactive elementsabundant-alligator-24067
03/26/2026, 6:56 PMtall-airline-16756
03/26/2026, 6:56 PMctx.add_shutdown_callback() that (1) builds the conversation prompt from session.history, (2) calls the agent/LLM to generate a summary, and (3) persists that summary to your database.
Why use a shutdown callback
• add_shutdown_callback() runs when the session/job ends (including user disconnects), so your summary will be generated and saved even if media output is no longer possible.
• Do not rely on on_exit() here, because it is only called for agent-to-agent handoffs and not on user disconnect.
Example pattern (Python-style pseudocode)
async def entrypoint(ctx: JobContext):
session = ctx.session
async def persist_summary():
# 1) Build a concise prompt from conversation history
convo = session.history # access recorded messages/events
prompt = f"Generate a short call summary (3-4 bullet points) from this conversation:\n{convo}"
# 2) Generate summary via agent/LLM
# Use the session/agent LLM call available in your runtime
summary_resp = await session.generate_reply(instructions=prompt)
summary_text = summary_resp.text if hasattr(summary_resp, "text") else str(summary_resp)
# 3) Persist to DB
# Replace with your DB client/ORM call
await save_summary_to_db(
call_id=session.call_id,
summary=summary_text,
generated_at=datetime.utcnow(),
)
# Register the shutdown callback so it runs when the session ends
ctx.add_shutdown_callback(persist_summary)
# ... your normal session logic here ...
Practical notes and best practices
• Keep the shutdown task reasonably short. The runtime has a graceful timeout (about 10 seconds) before forcefully terminating. If your DB write or LLM call may take longer:
◦ Make the shutdown callback enqueue a background job (e.g., push to a queue like Redis/RabbitMQ or a serverless function) and return quickly.
◦ Or precompute a partial summary during the call and finalize it in shutdown.
• If the user disconnects abruptly, media output won’t be possible but the summary generation and DB persistence will still run.
• Use structured summary prompts (e.g., ask for bullets, key decisions, action items) to make the stored summary more useful downstream.
• Consider storing both the summary text and metadata (call_id, participants, timestamps, prompt used, model/version) for traceability.
• If you want higher reliability, persist a rough draft periodically (every N minutes) during the call so the final shutdown step only needs to refine and store the final version.
If you want, I can:
• Provide a concrete example for a specific database (Postgres with asyncpg or SQLAlchemy, MongoDB, etc.).
• Show how to offload longer tasks to a background queue within the shutdown callback. Which database or persistence method are you using?, with interactive elements