early-butcher-66889
02/28/2026, 6:56 PMtall-airline-16756
02/28/2026, 6:57 PMtall-airline-16756
02/28/2026, 6:57 PMlk.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):
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):
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 Documentationearly-butcher-66889
02/28/2026, 11:51 PMx?tall-airline-16756
02/28/2026, 11:51 PM{ "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:
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:
@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