I currently have a testing workflow where we have ...
# ask-ai
e
I currently have a testing workflow where we have the Web UI join as a call participant and interact with our AI flow. I now want to modify this so that we use a SIP trunk to dial a participant outwards before getting the Web UI to join for monitoring purposes (integration testing). Is there a way to join a call and stream the room transcript without being a participant?
t
Thanks for reaching out, Arthur. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Thanks — I’m responding to your question about using a SIP trunk to dial out and having your Web UI join only to monitor/stream the room transcript without being an active audio participant. Yes — you can join a room for monitoring and receive the room transcript without publishing an audio track. The recommended approach is to disable audio output for the agent session and consume transcript text from the
lk.transcription
text stream. This keeps the SIP participant (dialed via your trunk) as the active audio party while your Web UI only receives transcripts and other data. What to do (high level) • Start your agent session with audio output disabled so no agent audio track is published (example: set
audio_output=False
in
RoomOutputOptions
). • Have your UI join the room but do not publish audio/video tracks — join as a receive-only participant. • Listen to the
lk.transcription
text stream topic from the room; the agent publishes its text responses there. • Filter messages by the
lk.transcribed_track_id
attribute to distinguish live speech transcriptions (transcribed from audio tracks) from agent text responses (agent text responses will not include
lk.transcribed_track_id
). Minimal code pointers 1. Disable audio output when starting the agent session (Python example):
Copy code
await session.start(
    agent=MyAgent(),
    room=ctx.room,
    room_options=room_io.RoomOptions(
        audio_output=False,  # disable TTS/audio output
    ),
)
1. Register a text stream handler on the frontend (TypeScript example):
Copy code
room.registerTextStreamHandler('lk.transcription', async (reader, participantInfo) => {
    const message = await reader.readAll();
    // if no lk.transcribed_track_id attribute, this is agent text response
    if (!reader.info.attributes['lk.transcribed_track_id']) {
        console.log(`Agent response: ${message}`);
    } else {
        console.log(`Speech transcription: ${message}`);
    }
});
Notes and gotchas • The built-in playground UI uses legacy transcription events and may not show these text-streamed agent responses when audio tracks are not published. You’ll need a custom receiver in your UI to read
lk.transcription
. • Joining the room without publishing audio/video still allows you to receive text streams and monitor the call. • This approach is suitable for integration testing where the SIP trunk handles the live call participant and the Web UI is used purely for monitoring/transcript consumption. If you’d like, tell me: • Which SDK/browser/client you’re using for the Web UI (JS/TS, React, etc.), and • Whether you want agent responses shown alongside live transcriptions or merged into a single transcript view. I can then provide a short, specific code example for your client and a suggested UI pattern for displaying agent vs. transcribed-speech messages. Sources: How to build an agent with speech-to-text input and text-only output | Live... | Outbound Calling Script | LiveKit Documentation
e
How do I stop my agent replying to my frontend UI joining as an observer? do I need to prefix their joiner / user name and say do not trigger generate_reply if the name matches
x
?
t
Thanks — I’m responding to your question about stopping the agent from replying when your frontend UI joins as an observer. You don’t need to rely on participant names. Use participant metadata to mark the observer and have the agent ignore participants with that metadata. Metadata is structured and reliable compared with string matching on names. What to do (high level) • When your UI joins as an observer, set a metadata field like
{ "type": "observer" }
on that participant. • In your agent’s
participant_connected
handler, check participant.metadata and skip calling
generate_reply
for observers. Examples 1) Frontend (TypeScript) — set observer metadata when joining:
Copy code
const participant = room.localParticipant;
await participant.setMetadata(JSON.stringify({ type: "observer" }));
// Join but do not publish audio/video if you only need receive-only monitoring
2) Agent side (Python) — ignore observers in participant_connected:
Copy code
@ctx.room.on("participant_connected")
def on_participant_connected(participant: rtc.RemoteParticipant):
    # participant.metadata is expected to be a JSON-parsed dict
    if participant.metadata and participant.metadata.get("type") == "observer":
        <http://logger.info|logger.info>(f"Ignoring observer participant: {participant.identity}")
        return

    # Only generate replies for real users
    asyncio.create_task(
        agent.session.generate_reply(instructions="Welcome the user.")
    )
Notes and tips • Ensure the metadata you set is JSON-serializable and consistent (e.g.,
type: "observer"
). • This prevents accidental triggers even if the observer’s display name changes. • If your observer truly doesn't need to appear as a participant, you can also join without publishing tracks or use a separate channel for monitoring, but metadata is the simplest way to keep the observer in the room while preventing agent responses. • If you want, you can extend this approach to other participant roles (e.g.,
{ type: "tester" }
,
{ role: "admin" }
) and make the agent behavior role-driven. If you tell me which SDK/framework your frontend and agent use (JS/TS React, plain JS, Python agent runtime, etc.), I can provide a short, tailored snippet for that environment. Sources: STT Metrics | LiveKit Documentation | How to set a fixed participant for agent audio processing | LiveKit