rich-art-45250
12/20/2025, 2:41 AMtall-airline-16756
12/20/2025, 2:41 AMtall-airline-16756
12/20/2025, 2:41 AMctx.addShutdownCallback() pattern you showed is a correct/common shutdown practice for TypeScript agents. Short answer: yes.
Why this is correct
• ctx.addShutdownCallback() is the intended hook to run cleanup when the agent is shutting down.
• Awaiting a Promise that resolves inside that callback keeps the entry function alive until ctx.shutdown() is invoked, preventing the process from exiting while the call/session is still active.
• ctx.shutdown() triggers the registered callbacks; the framework will wait (up to the configured grace period) to let callbacks finish before forcefully terminating.
Recommended usage
• Register cleanup logic in ctx.addShutdownCallback() (close connections, save state, etc.).
• Keep the entry alive with the Promise pattern you posted so the agent only exits after shutdown:
await new Promise<void>((resolve) => {
ctx.addShutdownCallback(() => resolve());
});
• Call ctx.shutdown() when you want to end the session programmatically (or rely on the framework to call it when participants leave).
Notes and caveats
• Ensure shutdown callbacks run quickly or complete within the framework’s grace period to avoid forced termination.
• If you have other long-running async work, either await it before calling ctx.shutdown() or register appropriate cleanup inside the shutdown callback.
If you want, I can:
• Show an example that includes cleanup (closing tracks/connections) inside the shutdown callback, or
• Help adapt this pattern to your existing agent code — share the entry function and I’ll suggest exactly where to add the Promise and callbacks. Sources: Job lifecycle | LiveKit docsrich-art-45250
12/20/2025, 2:41 AMtall-airline-16756
12/20/2025, 2:42 AMctx.addShutdownCallback().
import { defineAgent, JobContext } from '@livekit/agents';
import { LocalTrack, RemoteTrack } from 'livekit-client';
export default defineAgent({
entry: async (ctx: JobContext) => {
// Keep references to resources that need cleanup
let publishedTracks: LocalTrack[] = [];
let backgroundTask: Promise<void> | null = null;
// Example setup: publish a track and start a background task
try {
// Replace with your actual track creation/publishing code
// const track = await createLocalAudioTrack();
// await ctx.room.localParticipant.publishTrack(track);
// publishedTracks.push(track);
// Example background task (e.g., processing loop)
backgroundTask = (async () => {
while (!ctx.isShutdownRequested) {
// do work...
await new Promise((r) => setTimeout(r, 1000));
}
})();
} catch (err) {
console.error('setup error', err);
// Optionally trigger shutdown on fatal setup errors
// ctx.shutdown('setup failed');
}
// Register a shutdown callback to unpublish/stop tracks and close resources
ctx.addShutdownCallback(async () => {
console.log('Shutdown requested — cleaning up resources');
// Stop and unpublish local tracks
for (const t of publishedTracks) {
try {
// Unpublish from the local participant if still published
try {
await ctx.room.localParticipant.unpublishTrack(t);
} catch (unpubErr) {
// ignore if already unpublished
console.warn('unpublish error', unpubErr);
}
// Stop/close the track itself (API depends on track type)
try {
t.stop?.();
} catch (stopErr) {
console.warn('track stop error', stopErr);
}
} catch (err) {
console.error('error cleaning track', err);
}
}
publishedTracks = [];
// Wait for background tasks to finish or cancel them if applicable
if (backgroundTask) {
try {
await backgroundTask;
} catch (err) {
console.warn('background task error during shutdown', err);
}
}
// Optionally close any other external connections here
// await someExternalClient.close();
console.log('Cleanup complete');
});
// Keep the entry function alive until shutdown is invoked
await new Promise<void>((resolve) => {
ctx.addShutdownCallback(() => resolve());
});
},
});
If you want, I can adapt this to your actual publish/track APIs (share the code that creates/publishes tracks) and show exact cleanup calls for those objects.
Sources: Job lifecycle | LiveKit docs