cuddly-vr-85751
02/16/2026, 9:14 AMtall-airline-16756
02/16/2026, 9:14 AMtall-airline-16756
02/16/2026, 9:14 AMcuddly-vr-85751
02/16/2026, 9:14 AMtall-airline-16756
02/16/2026, 9:14 AMagent.py and adjust environment variables as noted.
# Minimal voice-enabled Agent example (Python)
import logging
from dotenv import load_dotenv
from livekit.agents import AgentSession, JobContext, AgentServer, cli, inference
from livekit.plugins import silero
load_dotenv()
logger = logging.getLogger("livekit-agent")
logger.setLevel(<http://logging.INFO|logging.INFO>)
server = AgentServer()
def prewarm(proc):
proc.userdata["vad"] = silero.VAD.load()
server.setup_fnc = prewarm
@server.rtc_session()
async def on_session(ctx: JobContext):
<http://logger.info|logger.info>("Starting agent session")
session = AgentSession(
stt=inference.STT(model="deepgram/nova-2"),
llm=inference.LLM(model="openai/gpt-4o"),
tts=inference.TTS(model="cartesia/sonic-3"),
vad=ctx.proc.userdata["vad"],
preemptive_generation=True,
)
await session.start(ctx.room)
await ctx.connect()
await session.generate_reply(instructions="Greet the participant and ask how you can help.")
if __name__ == "__main__":
cli.run_app(server)
.env (example)
LIVEKIT_API_KEY=your_api_key
LIVEKIT_API_SECRET=your_api_secret
LIVEKIT_URL=<wss://your-project.livekit.cloud>
Commands:
• Install deps: pip install "livekit-agents[silero]" python-dotenv
• Run locally: python agent.py console
• Deploy: lk agent create --agent-file agent.py
If you’d rather do text-only, remove `stt`/`tts` (set them to None) and use session.chat_ctx.add_message() plus session.generate_reply().
Tell me which language and mode (voice or text) you want and whether you’d like a Node.js example or a frontend integration — I’ll send a tailored code sample and next steps.
Sources: Simple Call Answering Agent | LiveKit Documentationcuddly-vr-85751
02/16/2026, 9:16 AMtall-airline-16756
02/16/2026, 9:16 AMcuddly-vr-85751
02/16/2026, 9:17 AMtall-airline-16756
02/16/2026, 9:17 AMpip install "livekit-agents" python-dotenv
2) Create a .env file in the same folder:
LIVEKIT_API_KEY=your_api_key
LIVEKIT_API_SECRET=your_api_secret
LIVEKIT_URL=<wss://your-project.livekit.cloud>
3) Save this as `text_agent.py`:
# text_agent.py
import logging
from dotenv import load_dotenv
from livekit.agents import AgentServer, AgentSession, Agent, JobContext, cli, inference
from <http://livekit.agents.io|livekit.agents.io> import room_io
load_dotenv()
logging.basicConfig(level=<http://logging.INFO|logging.INFO>)
logger = logging.getLogger("text_agent")
server = AgentServer()
class TextOnlyAgent(Agent):
def __init__(self):
super().__init__(instructions="You are a helpful text-only assistant.")
async def on_enter(self):
# Optional greeting when the agent joins the room
await self.session.say("Hello! I am a text-only assistant. How can I help you today?")
@server.rtc_session()
async def on_session(ctx: JobContext):
<http://logger.info|logger.info>("Starting text-only agent session")
# Create an AgentSession with no STT/TTS; use an LLM for responses
session = AgentSession(
stt=None, # no speech-to-text
tts=None, # no text-to-speech
llm=inference.LLM(model="openai/gpt-4o"), # replace with your preferred model or remove
)
# RoomOptions to ensure no audio is used and text input/output enabled
room_opts = room_io.RoomOptions(
audio_input=False,
audio_output=False,
text_input=True,
text_output=True,
)
# Start the session with the TextOnlyAgent
await session.start(
agent=TextOnlyAgent(),
room=ctx.room,
room_options=room_opts,
)
# Connect the agent to the LiveKit room
await ctx.connect()
# Keep running until the session ends
await session.wait_until_closed()
4) How it works (short)
• stt=None and tts=None disable audio processing.
• room_io.RoomOptions disables audio publishing and enables text input/output.
• The agent receives participant text via LiveKit text streams (topic lk.chat) and publishes responses on the transcription/text stream (lk.transcription).
• session.say() sends a text reply into the room.
5) Run locally for testing
python text_agent.py console
This runs the agent in console mode. Use a client (web or other) to join the same LiveKit room and send text messages; the agent will read them and respond as text.
6) Client notes
• On the client, listen for the text stream for agent replies (topic lk.transcription).
• When participants send messages, they appear on the lk.chat text stream; the agent can read and respond to those.
Would you like:
• A minimal web client example (JavaScript) that sends/receives text to/from the agent?
• A version that uses only rule-based responses (no LLM)?
• Help picking or configuring an LLM model and API keys?
Tell me which next step you prefer and I’ll provide that code.
Sources: Text and transcriptions | LiveKit Documentation | How to build an agent with speech-to-text input and text-only output | Live... | Transcriber | LiveKit Documentationcuddly-vr-85751
02/16/2026, 9:18 AMtall-airline-16756
02/16/2026, 9:18 AMlk.transcription text stream).
Summary of what you’ll get:
• Install steps
• Token generation note (server-side)
• A simple React component that:
◦ Connects to LiveKit with a token
◦ Sends text messages (published to lk.chat)
◦ Listens for text streams and shows agent replies (lk.transcription) and participant chat
Prerequisites
• Your Python text-only agent is running and joined the same LiveKit room.
• You have a server endpoint that returns a LiveKit join token for the client (short-lived access token). The frontend needs that token to connect. If you want an example token endpoint, I can provide one separately.
1) Install dependencies
npm install livekit-client
# or with yarn
# yarn add livekit-client
2) Minimal React component (functional) Save as e.g. `ChatRoom.jsx`:
import React, { useEffect, useState, useRef } from "react";
import { connect, Room, LocalTrackPublication } from "livekit-client";
export default function ChatRoom({ tokenUrl, roomName, userName }) {
const [room, setRoom] = useState(null);
const [connected, setConnected] = useState(false);
const [messages, setMessages] = useState([]); // {sender, text, topic}
const inputRef = useRef();
// Helper to append message
function pushMessage(sender, text, topic = "lk.chat") {
setMessages((m) => [...m, { sender, text, topic, id: Date.now() + Math.random() }]);
}
// Connect to LiveKit on mount
useEffect(() => {
let r;
async function init() {
try {
// Request a token from your server
const resp = await fetch(`${tokenUrl}?room=${encodeURIComponent(roomName)}&name=${encodeURIComponent(userName)}`);
const { token, url } = await resp.json(); // token and optional custom LiveKit url
r = await connect(url || undefined, token, {
// Avoid publishing audio/video for text-only client
publishDefaults: { audio: false, video: false },
});
setRoom(r);
setConnected(true);
// Listen for incoming text streams
r.on("textMessage", (msg, participant) => {
// This event is for chat messages sent via the text API (legacy)
// Some clients use explicit text streams; handle both.
const sender = participant?.identity || msg.participantSid || "unknown";
pushMessage(sender, msg.text || msg, "lk.chat");
});
// Register text stream handlers (for stream topics like lk.transcription)
r.on("textStreamPublished", (stream, participant) => {
// When a new text stream is published, attach a reader
attachTextStream(stream, participant);
});
// Attach any existing text streams (in case agent already published)
r.participants.forEach((p) => {
p.getPublishedTextTracks?.()?.forEach((track) => attachTextStream(track, p));
});
// Local participant also may have text streams published
r.localParticipant.getPublishedTextTracks?.()?.forEach((track) => attachTextStream(track, r.localParticipant));
} catch (err) {
console.error("connect error", err);
}
}
init();
return () => {
if (r) r.disconnect();
};
}, [tokenUrl, roomName, userName]);
// Attach text stream reader function
async function attachTextStream(track, participant) {
try {
// livekit-client exposes a reader to consume text stream messages
const reader = await track.createReader();
(async () => {
for await (const msg of reader) {
// msg will be a string or object depending on publisher
const text = typeof msg === "string" ? msg : JSON.stringify(msg);
const sender = participant?.identity || "agent";
const topic = track.topic || "lk.transcription";
pushMessage(sender, text, topic);
}
})();
} catch (e) {
console.warn("failed to attach text stream", e);
}
}
// Send a text message into the room (published to lk.chat)
async function sendMessage() {
if (!room) return;
const text = inputRef.current.value.trim();
if (!text) return;
// Publish via LiveKit text message API
// The livekit-client provides publishChatMessage
try {
room.localParticipant.publishChatMessage(text);
pushMessage(userName, text, "lk.chat");
inputRef.current.value = "";
} catch (e) {
console.error("send failed", e);
}
}
return (
<div style={{ maxWidth: 720, margin: "0 auto" }}>
<h3>Text Chat Room: {roomName}</h3>
<div style={{ border: "1px solid #ddd", padding: 12, height: 400, overflow: "auto" }}>
{messages.map((m) => (
<div key={m.id} style={{ marginBottom: 8 }}>
<strong>{m.sender}</strong> <em style={{ color: "#666", fontSize: 12 }}>[{m.topic}]</em>
<div>{m.text}</div>
</div>
))}
</div>
<div style={{ marginTop: 8 }}>
<input ref={inputRef} placeholder="Type a message..." style={{ width: "70%" }} onKeyDown={(e) => { if (e.key === "Enter") sendMessage(); }} />
<button onClick={sendMessage} style={{ marginLeft: 8 }}>Send</button>
</div>
<div style={{ marginTop: 12 }}>
<strong>Status:</strong> {connected ? "connected" : "disconnected"}
</div>
</div>
);
}
3) How it works — quick notes
• The client requests a join token from your server at tokenUrl. Your server should return JSON { token, url? } where token is a LiveKit access token and url is the LiveKit websocket URL if needed.
• The component disables audio/video publishing (publishDefaults) so the client stays text-only.
• Messages typed by the user are sent with room.localParticipant.publishChatMessage(text). Those appear on the lk.chat stream for the agent to read.
• The agent publishes replies on the lk.transcription text stream (or lk.chat depending on how it's implemented). The frontend listens for text streams and displays messages. We listen both to the legacy textMessage event and text stream publications to cover both patterns.
• Agent replies typically show up on a text stream with topic lk.transcription. The code attaches readers for published text streams and pushes incoming messages into local state for display.
4) Server token endpoint (brief) Your frontend cannot hold the LiveKit API secret. Create a small server endpoint that issues tokens (example not included here yet). The endpoint should return: { "token": "<access_token>", "url": "wss://<your-livekit-host>" } If you want, I can provide a minimal Python/Node token-creation example next.
5) Next steps I can provide
• A minimal server example (Python/Node) that issues LiveKit tokens.
• A refined UI that separates agent messages visually and highlights the agent.
• Instructions on how to ensure your Python agent publishes replies on lk.transcription (I can show sample code for session.say() or direct text stream publishing).
Which of those do you want next — the token endpoint, a polished UI, or confirmation that the agent is publishing on lk.transcription?tall-airline-16756
03/22/2026, 3:43 PMlk.chat text stream so the agent receives chat messages. That’s unexpected behavior and it’s reasonable to consider it a regression if it previously worked automatically.
Why this happens (short)
• The AgentSession normally subscribes to text input on lk.chat when text input is enabled (via RoomInputOptions / text_input=True). That lets the agent automatically react to incoming chat.
• In 1.5.0 the automatic hookup of the lk.chat handler appears to have been removed or changed (intentional change or regression). Because the agent no longer automatically attaches the text-stream handler in your environment, you need to register it yourself — exactly like your workaround does.
Why your workaround is correct
• Your `_register_lk_chat_text_handler`:
◦ Ignores messages from the local participant (prevents echo).
◦ Reads the text from the text stream and strips it.
◦ Interrupts the current session and calls session.generate_reply(user_input=text).
◦ Runs the reader as a background task.
• That matches the intended behavior the embedded handler used to provide. It’s a safe and appropriate way to restore the original behavior until the root cause is resolved.
Practical next steps
1. Short-term: keep your manual handler in place (your code is fine). It restores expected behavior immediately and is robust.
2. Verify config: confirm your RoomOptions still have text_input=True (that’s default but good to check), so you’re not accidentally opting out elsewhere.
3. If you can, test on the previous livekit-agents version (the one that worked) to confirm this is an upgrade regression in your environment. That helps isolate whether it’s a breaking change or a bug.
4. Share minimal repro: if you want me to help file a clear bug report, I can help craft a minimal reproduction snippet showing the difference between pre-1.5.0 and 1.5.0 behavior (your snippet + a small room setup is perfect for that).
What I can help with next
• I can produce a slightly improved/cleaned version of your handler (non-blocking loop, graceful shutdown), if you’d like.
• I can help build a minimal repro and a concise issue report to post to the agents repo so maintainers can investigate faster.
• I can check your `AgentSession`/`RoomOptions` code to confirm nothing else disables automatic text handling.
If you want the improved handler now, here’s a small cleanup (keeps same behavior but uses an async loop and logs errors):
def _register_lk_chat_text_handler(room: rtc.Room, session: AgentSession) -> None:
local_identity = room.local_participant.identity
def on_text_stream(reader: rtc.TextStreamReader, participant_identity: str) -> None:
async def consume() -> None:
if participant_identity == local_identity:
return
try:
text = (await reader.read_all()).strip()
except Exception:
logger.exception("Failed to read lk.chat text stream")
return
if not text:
return
try:
await session.interrupt()
except RuntimeError:
# session may already be closed or interrupt not allowed
pass
# Fire-and-forget reply generation
asyncio.create_task(session.generate_reply(user_input=text))
asyncio.create_task(consume())
room.register_text_stream_handler(TOPIC_CHAT, on_text_stream)
Tell me which of the next steps you want:
• I’ll help prepare a minimal repro + issue text for the GitHub repo.
• I’ll review your AgentSession/RoomOptions for anything suspicious.
• I’ll provide a more fully-featured handler (e.g., streaming reads, rate limiting, or message batching).
You did the right thing by adding that handler — let me know how you want to proceed and I’ll help you get this fixed or reported cleanly.
Sources: Text and transcriptions | LiveKit Documentation | examples/other | livekit/livekit_composite | DeepWiki, with interactive elements