How can i get the inbound caller phone number in t...
# ask-ai
c
How can i get the inbound caller phone number in the
entry
function of
defineAgent
in typescript?
t
Thanks for reaching out, Jatin. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
You're asking how to get the inbound caller's phone number inside the
entry
function of
defineAgent
in TypeScript — you can read it from the SIP participant's attributes under the
sip.phoneNumber
key. Example:
Copy code
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 docs
c
what does waitForParticipant() do here exactly?
t
You're asking what
waitForParticipant()
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 docs
c
when i use it in my current code in this order of session.start the participart never joins even though I call the telephone number of livekit. Why may that happen?
t
Thanks — you're saying that when you call
ctx.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.
c
This is my code now:
Copy code
export 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,
    });
  },
});
t
Thanks — you're asking why the SIP participant never joins when you await
ctx.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