cuddly-baker-42941
12/18/2025, 2:34 AMentry function of defineAgent in typescript?tall-airline-16756
12/18/2025, 2:34 AMtall-airline-16756
12/18/2025, 2:34 AMentry function of defineAgent in TypeScript — you can read it from the SIP participant's attributes under the sip.phoneNumber key.
Example:
import { ParticipantKind, AutoSubscribe } from '@livekit/rtc-node';
import { defineAgent, AgentSession } from '<your-agent-package>';
export default defineAgent({
entry: async (ctx: JobContext) => {
await ctx.connect(undefined, AutoSubscribe.AUDIO_ONLY);
const participant = await ctx.waitForParticipant();
if (participant.kind === ParticipantKind.SIP) {
const phoneNumber = participant.attributes['sip.phoneNumber'];
console.log('Inbound caller phone number:', phoneNumber);
// use phoneNumber as needed
}
const session = new AgentSession({ /* config */ });
await session.start({ agent, room: ctx.room });
},
});
Note: sip.phoneNumber is present only for inbound SIP calls and may be hidden if your dispatch rule sets HidePhoneNumber. Would you like an example showing a lookup or conditional behavior based on the phone number?
Sources: SIP participant | LiveKit docscuddly-baker-42941
12/18/2025, 3:04 AMtall-airline-16756
12/18/2025, 3:05 AMwaitForParticipant() does inside the entry function of defineAgent.
waitForParticipant() pauses execution until the first remote participant (a human user or another agent) joins the room and then resolves with that participant object. Use it when your agent needs to access participant-dependent data (like participant.attributes['sip.phoneNumber'], participant.identity, or tracks) before continuing.
Key behaviors:
• Resolves once a participant joins the room (returns a RemoteParticipant-like object).
• Lets you safely read participant fields (attributes, identity, kind, tracks) immediately after awaiting it.
• Commonly used to delay agent actions (start session, send messages, answer call) until someone is present.
If you want, I can show how to handle timeouts, wait for a specific participant identity, or listen for additional participants. Which would you prefer?
Sources: Job lifecycle | LiveKit docscuddly-baker-42941
12/18/2025, 3:09 AMtall-airline-16756
12/18/2025, 3:09 AMctx.waitForParticipant() before session.start(), the participant never appears even though you call the LiveKit telephone number. Let's narrow this down so I can help debug.
Please share:
• A short code snippet showing the order of ctx.connect, ctx.waitForParticipant(), session.start(...), and where you create/connect the AgentSession.
• Whether the incoming call is SIP inbound (caller dials your LiveKit phone number) or something else.
• Any logs or console output from the agent process when the call happens (errors, warnings, or silence).
• Whether your dispatch rule has HidePhoneNumber enabled.
• Whether you see the call on the LiveKit dashboard or SIP provider side as connected/answered.
Quick checks you can try now:
• Move session.start(...) to run before awaiting ctx.waitForParticipant() and see if the participant arrives.
• Add a timeout or log right before and after waitForParticipant() to confirm it's awaiting.
• Log ctx.room and local participant state after ctx.connect() to confirm you're in the correct room.
Send the snippet and logs and I’ll point to the most likely cause and the exact fix.cuddly-baker-42941
12/18/2025, 3:13 AMexport default defineAgent({
prewarm: async (proc: JobProcess) => {
proc.userData.vad = await silero.VAD.load({ forceCPU: true });
},
entry: async (ctx: JobContext) => {
const vad = ctx.proc.userData.vad! as silero.VAD;
let userLastSpokeTime = Date.now();
const logger = log();
const sessionStartTime = Date.now();
// Generate conversationId for this session
const conversationId = crypto.randomUUID();
// Extract userId from SIP phone number
let userId = 'unknown';
// Set up a voice AI pipeline using OpenAI, Cartesia, AssemblyAI, and the LiveKit turn detector
const session = new voice.AgentSession({
voiceOptions: {
maxToolSteps: 150,
userAwayTimeout: 12.5,
preemptiveGeneration: false,
},
// Speech-to-text (STT) is your agent's ears, turning the user's speech into text that the LLM can understand
// See all available models at <https://docs.livekit.io/agents/models/stt/>
stt: new inference.STT({
model: 'assemblyai/universal-streaming',
language: 'en',
}),
// A Large Language Model (LLM) is your agent's brain, processing user input and generating a response
// See all providers at <https://docs.livekit.io/agents/models/llm/>
llm: new inference.LLM({
model: 'openai/gpt-4.1-nano',
}),
// Text-to-speech (TTS) is your agent's voice, turning the LLM's text into speech that the user can hear
// See all available models as well as voice selections at <https://docs.livekit.io/agents/models/tts/>
tts: new inference.TTS({
language: 'en',
voice: 'celeste',
model: 'rime/arcana',
modelOptions: {
speed: 'fast',
},
}),
// VAD and turn detection are used to determine when the user is speaking and when the agent should respond
// See more at <https://docs.livekit.io/agents/build/turns>
// turnDetection: new livekit.turnDetector.EnglishModel(),
turnDetection: 'vad',
vad: vad,
});
// Initialize thinking audio player
let thinkingAudioPlayer: voice.BackgroundAudioPlayer | null = null;
if (THINKING_AUDIO_ENABLED) {
thinkingAudioPlayer = new BackgroundAudioPlayer({
ambientSound: {
source: path.join(__dirname, '..', THINKING_AUDIO_FILE),
volume: THINKING_AUDIO_VOLUME,
},
});
}
// Track inactivity check state
let inactivityTimeoutId: NodeJS.Timeout | null = null;
let inactivityCheckCancelled = false;
async function runInactivityCheck() {
for (let i = 0; i < 3; i++) {
if (inactivityCheckCancelled) {
<http://logger.info|logger.info>('Inactivity check cancelled');
return;
}
const presenceMessages: [string, string, string] = [
'Hi, are you still there?',
"Hello? I just wanted to check if you're still with me.",
'It seems like you might have stepped away. Are you still there?',
];
session.say(presenceMessages[i]!, { addToChatCtx: true });
// Wait 10 seconds between prompts
await new Promise<void>((resolve) => {
inactivityTimeoutId = setTimeout(() => {
inactivityTimeoutId = null;
resolve();
}, 20000);
});
}
if (!inactivityCheckCancelled) {
<http://logger.info|logger.info>('User remained inactive after 3 attempts, shutting down session');
await session.say(
'Seems like you are no longer here. I will now hang up the call but if you wanna continue the conversation just call me back.',
{ addToChatCtx: true },
);
await session.close();
}
}
session.on(voice.AgentSessionEventTypes.UserStateChanged, (ev) => {
if (ev.newState === 'speaking') {
<http://logger.info|logger.info>('User started speaking');
// Cancel inactivity check when user speaks
inactivityCheckCancelled = true;
if (inactivityTimeoutId) {
clearTimeout(inactivityTimeoutId);
inactivityTimeoutId = null;
}
} else if (ev.newState === 'listening') {
<http://logger.info|logger.info>('User stopped speaking');
// Reset cancellation flag when user stops speaking (they're still active)
inactivityCheckCancelled = false;
} else if (ev.newState === 'away') {
<http://logger.info|logger.info>('User is away');
// Start inactivity check
inactivityCheckCancelled = false;
runInactivityCheck();
}
});
session.on(voice.AgentSessionEventTypes.AgentStateChanged, async (ev) => {
if (ev.newState === 'initializing') {
<http://logger.info|logger.info>('Agent is starting up');
} else if (ev.newState === 'idle') {
<http://logger.info|logger.info>('Agent is ready but not processing');
// Stop thinking audio when idle
if (thinkingAudioPlayer) {
await thinkingAudioPlayer.close().catch((err) => {
logger.debug('Error closing thinking audio (already stopped):', err);
});
thinkingAudioPlayer = null;
}
} else if (ev.newState === 'listening') {
<http://logger.info|logger.info>('Agent is listening for user input');
// Stop thinking audio when listening
if (thinkingAudioPlayer) {
await thinkingAudioPlayer.close().catch((err) => {
logger.debug('Error closing thinking audio (already stopped):', err);
});
thinkingAudioPlayer = null;
}
} else if (ev.newState === 'thinking') {
<http://logger.info|logger.info>('Agent is processing user input and generating a response');
// Start thinking audio
if (THINKING_AUDIO_ENABLED && !thinkingAudioPlayer) {
thinkingAudioPlayer = new BackgroundAudioPlayer({
ambientSound: {
source: path.join(__dirname, '..', THINKING_AUDIO_FILE),
volume: THINKING_AUDIO_VOLUME,
},
});
try {
await thinkingAudioPlayer.start({
room: ctx.room,
agentSession: session,
});
<http://logger.info|logger.info>('Thinking audio started');
} catch (error) {
logger.error('Failed to start thinking audio', { error });
thinkingAudioPlayer = null;
}
}
} else if (ev.newState === 'speaking') {
<http://logger.info|logger.info>('Agent started speaking');
// Stop thinking audio when speaking
if (thinkingAudioPlayer) {
await thinkingAudioPlayer.close().catch((err) => {
logger.debug('Error closing thinking audio (already stopped):', err);
});
thinkingAudioPlayer = null;
}
}
});
// Metrics collection, to measure pipeline performance
// For more information, see <https://docs.livekit.io/agents/build/metrics/>
const usageCollector = new metrics.UsageCollector();
session.on(voice.AgentSessionEventTypes.MetricsCollected, (ev) => {
metrics.logMetrics(ev.metrics);
usageCollector.collect(ev.metrics);
});
const logUsage = async () => {
const summary = usageCollector.getSummary();
console.log(`Usage: ${JSON.stringify(summary)}`);
};
ctx.addShutdownCallback(logUsage);
// Track agent session completion with Raindrop
ctx.addShutdownCallback(async () => {
const sessionDuration = Date.now() - sessionStartTime;
await trackAgentSession('completed', {
...(ctx.room.name ? { roomName: ctx.room.name } : {}),
agentId: 'CA_NtdtLFa9JzAw',
duration: sessionDuration,
convoId: conversationId,
userId,
});
<http://logger.info|logger.info>('Agent session completed', {
roomName: ctx.room.name,
duration: sessionDuration,
conversationId,
userId,
});
});
// Create SessionManager for handling call transfers
const sessionManager = new SessionManager(ctx, ctx.room, session, conversationId, userId);
// Add cleanup callback for SessionManager
ctx.addShutdownCallback(async () => {
await sessionManager.cleanup();
});
// Add cleanup callback for thinking audio player
ctx.addShutdownCallback(async () => {
if (thinkingAudioPlayer) {
<http://logger.info|logger.info>('Cleaning up thinking audio player');
await thinkingAudioPlayer.close().catch((err) => {
logger.debug('Error closing thinking audio during cleanup:', err);
});
}
});
// Start the session, which initializes the voice pipeline and warms up the models
await session.start({
agent: new Assistant(sessionManager),
room: ctx.room,
record: true,
inputOptions: {
// LiveKit Cloud enhanced noise cancellation
// - If self-hosting, omit this parameter
// - For telephony applications, use `BackgroundVoiceCancellationTelephony` for best results
noiseCancellation: TelephonyBackgroundVoiceCancellation(),
},
});
// Background audio player for ambient office sounds
const backgroundAudio = new BackgroundAudioPlayer({
ambientSound: [
{
source: path.join(__dirname, '..', 'office-environment-2.mp3'),
volume: 0.9,
},
],
});
// Start background audio immediately (before the VAD loop)
await backgroundAudio.start({
room: ctx.room,
agentSession: session,
});
<http://logger.info|logger.info>('Background ambient audio started');
// Initialize VAD stream
const vadStream = vad.stream();
// Listen for speech events in background (non-blocking)
(async () => {
for await (const event of vadStream) {
if (event.type === VADEventType.START_OF_SPEECH) {
userLastSpokeTime = Date.now();
}
}
})();
// Add cleanup callback for background audio player
ctx.addShutdownCallback(async () => {
<http://logger.info|logger.info>('Cleaning up background audio player');
await backgroundAudio.close().catch((err) => {
logger.debug('Error closing background audio during cleanup:', err);
});
});
// Join the room and connect to the user
await ctx.connect();
const participant = await ctx.waitForParticipant();
if (participant.kind === ParticipantKind.SIP) {
const phoneNumber = participant.attributes['sip.phoneNumber'];
console.log('Inbound caller phone number:', phoneNumber);
<http://logger.info|logger.info>('Inbound caller phone number:', phoneNumber);
userId = phoneNumber ?? 'unknown';
}
<http://logger.info|logger.info>('Starting agent session', { roomName: ctx.room.name, conversationId, userId });
// Track agent session start with Raindrop
await trackAgentSession('started', {
...(ctx.room.name ? { roomName: ctx.room.name } : {}),
agentId: 'CA_NtdtLFa9JzAw',
convoId: conversationId,
userId,
});
},
});tall-airline-16756
12/18/2025, 3:13 AMctx.waitForParticipant() in your provided code. I reviewed your snippet.
Root cause and fix (short):
• Your code calls await session.start(...) before await ctx.connect() / ctx.waitForParticipant(). The agent must connect to the room first so the room and dispatch lifecycle can create/route the SIP participant. Reorder to:
a. await ctx.connect();
b. const participant = await ctx.waitForParticipant();
c. then await session.start({...});
Why this matters:
• ctx.connect() joins the agent to the room so LiveKit can route the inbound SIP call into that room.
• waitForParticipant() waits for a remote participant to join the room; if the agent hasn't connected/registered the room properly, the inbound SIP participant may never be created/attached.
• session.start() should run after the caller is present so the voice pipeline initializes with the remote participant available.
Quick debugging steps to confirm:
• Move session.start() after waitForParticipant() and test.
• Add logs right before/after ctx.connect(), waitForParticipant(), and session.start() to confirm progress.
• Verify the call reaches your SIP trunk / dispatch and that HidePhoneNumber is not enabled if you expect sip.phoneNumber to appear.
If you want, I can return a small corrected snippet showing exactly where to place ctx.connect(), waitForParticipant(), and session.start() in your file. Which would you prefer?
Sources: How to set a fixed participant for agent audio processing | Agents telephony integration | LiveKit docs | Job lifecycle | LiveKit docs