LiveKit Voice Foundations
Build a browser voice app that talks to a Python agent over LiveKit.
Introduction
30 Second Summary
A voice conversation feels natural when every response arrives at the right moment. The moment it stalls, audio alone gives you few clues about what went wrong.
In this project, you will build a React page for a real browser-to-Python voice conversation through a LiveKit Cloud WebRTC room. The finished page exposes the agent's live state through labels and responsive audio bars.
What You'll Build
Your finished studio shows a live voice exchange in Safari as your spoken question travels from the microphone to an audible agent reply.
By the end of this project, you'll have:
- A real browser-to-agent conversation where your microphone reaches the room before the agent's answer plays through the page.
- Visible lifecycle feedback that shows the agent listening, thinking, or speaking during each turn.
- Signal-driven audio bars that move from the agent's actual remote audio track.
- Secret Mission: Add a second signal-driven visualizer for your local microphone track so User activity stays separate from Agent activity.
Are there any prerequisites?
You need a Mac with a microphone plus Node.js 20.19+ or 22.12+, Python 3.10 to 3.14, uv, npm, Git, and a code editor. You also need enough React and Python experience to follow the code plus permission to create a free LiveKit Cloud project.
Before We Start
Before the hands-on work begins, lock in who will use your Voice Agent Studio and the short conversation it should handle. This keeps every decision tied to a clear purpose.
Set Up LiveKit Cloud and the Local Toolchain
Voice Agent Studio needs a shared realtime network before its browser and Python participants can exchange audio.
A LiveKit Cloud project provides that network. The LiveKit CLI connects your Mac to the same project for authenticated local commands.
In this step, get ready to:
- Create a LiveKit Cloud project on the Build plan.
- Verify the runtimes and development tools already on your Mac.
- Install the LiveKit CLI.
- Authenticate the LiveKit CLI with your cloud project.
Create your LiveKit Cloud project
The cloud project becomes the meeting point for the browser participant and the Python agent. Every room later in the project lives inside it.
Pricing screens can feel like a commitment. The Build plan costs $0/mo and requires no credit card.
- Open the LiveKit Cloud account page in your browser.
- Create your free account through the guided sign-up flow.
- Create one LiveKit Cloud project through the project creation flow.
- Open the plan details for your new project.
- Confirm that the project uses the Build plan.
- Confirm that the listed price is $0/mo.
Keep Voice Tests Brief
The Build plan includes $2.50 in credits for LiveKit Inference.
The plan also includes 5,000 minutes of WebRTC usage. Real model calls consume the inference credits.
- Keep your voice tests short during this project.
- Stop each local process after you finish testing it.
That is your first foundation in place: the browser and agent now have one cloud project to share.
Verify your existing local toolchain
Node.js runs the frontend toolchain. Python runs the voice agent.
The checks also confirm that npm and uv are available. They confirm that Git is ready for source control.
- Press Cmd+Space to open Spotlight.
- Type Terminal into Spotlight.
- Press Enter to open Terminal.
- Print the installed tool versions by running these commands:
node --version
npm --version
python3 --version
uv --version
git --version
What Do These Checks Prove?
- The node result identifies the runtime available to the React frontend.
- The npm result confirms that the frontend package manager is available.
- The python3 result identifies the interpreter available to the Python agent.
- The uv result confirms that the backend project manager is available.
- The git result confirms that source control is available.
The Node.js result determines which path applies to your frontend runtime.
✔️ My Node.js version is supported
Your Node.js installation is ready when it satisfies 20.19+ or 22.12+.
Your existing runtime can run the pinned Vite frontend.
ⓧ My Node.js version is outside the range
This project requires Node.js 20.19+ or 22.12+.
- Open the official Node.js download page.
- Choose a release that satisfies the required Node.js range.
- Complete the macOS installation flow.
- Open a fresh Terminal window through Spotlight.
- Repeat the tool version checks shown above.
ⓧ Node.js is not found
Your shell cannot find Node.js. A supported Node.js installation also provides npm.
- Open the official Node.js download page.
- Choose a release that satisfies 20.19+ or 22.12+.
- Complete the macOS installation flow.
- Open a fresh Terminal window through Spotlight.
- Repeat the tool version checks shown above.
The Python result determines which path applies to your voice-agent runtime.
✔️ My Python version is supported
Python 3.10 through 3.14 can run the pinned LiveKit Agents dependencies.
ⓧ My Python version is outside the range
Python installations on macOS can be fiddly because the Apple-managed interpreter may answer the version check. This project requires Python 3.10 through 3.14.
- Open the official Python downloads page for macOS.
- Choose a Python release inside the supported range.
- Complete the macOS installation flow with its default options.
- Open a fresh Terminal window through Spotlight.
- Repeat the tool version checks shown above.
Still Seeing the Apple-Managed Python?
Keep the Apple-managed Python installation unchanged. Confirm that the default python.org installer options added the newer installation to your shell path.
Use Help me select my supported Python installation on macOS.
ⓧ Python is not found
Your shell cannot find Python 3. The official macOS installer provides a compatible interpreter.
- Open the official Python downloads page for macOS.
- Choose a Python release from 3.10 through 3.14.
- Complete the macOS installation flow with its default options.
- Open a fresh Terminal window through Spotlight.
- Repeat the tool version checks shown above.
- Confirm that the npm check returned a version number.
- Confirm that the uv check returned a version number.
- Confirm that the Git check returned a version number.
Missing a Supporting Tool?
Open a fresh Terminal window after any recent installation. Repeat the version checks to confirm that your shell can find the tool.
Use Help me restore the missing tool on my Mac.
- Press Cmd+Space to open Spotlight.
- Type the name of your existing code editor into Spotlight.
- Press Enter to launch the editor.
Your editor opens successfully. Your existing local development toolchain is ready.
Install and authenticate the LiveKit CLI
The LiveKit CLI gives your terminal authenticated access to the cloud project. This walkthrough uses version 2.19.2.
Homebrew provides the shortest installation path on macOS. The official release archive provides a fallback for Macs without Homebrew.
- Check for an existing LiveKit CLI installation by running this command:
lk --version
What Does This Command Do?
The command prints the installed LiveKit CLI version. Its result determines the installation path you need.
✔️ I see the required LiveKit CLI version
The output shows LiveKit CLI version 2.19.2. Continue to authentication below.
ⓧ I see a different LiveKit CLI version
Update the existing installation so it matches version 2.19.2 used in this walkthrough.
Homebrew
- Update the Homebrew package information and upgrade the LiveKit CLI by running this command:
brew update && brew upgrade livekit-cli
What Does This Command Do?
Homebrew refreshes its package information. It then replaces the existing LiveKit CLI with the current formula version.
- Repeat the LiveKit CLI version check shown above.
The output now shows version 2.19.2.
Official Release Archive
- Open the official LiveKit CLI release page.
- Download lk_2.19.2_darwin_arm64.tar.gz for an Apple silicon Mac.
- Download lk_2.19.2_darwin_amd64.tar.gz for an Intel Mac.
- Extract the matching archive in Finder.
- Replace the existing lk binary in the directory already used by your shell.
- Open a fresh Terminal window through Spotlight.
- Repeat the LiveKit CLI version check shown above.
The output now shows version 2.19.2.
Still Seeing the Previous Version?
Your shell may still be finding an older LiveKit CLI binary. Open a fresh Terminal window after the update.
Use Help me locate the LiveKit CLI binary used by my shell.
ⓧ The LiveKit CLI is not found
The LiveKit CLI is not available on your shell path. Choose the installation method available on your Mac.
Homebrew
- Install the LiveKit CLI package by running this command:
brew install livekit-cli
What Does This Command Do?
Homebrew downloads the LiveKit CLI package. It makes the lk executable available to your shell.
- Repeat the LiveKit CLI version check shown above.
The output shows version 2.19.2.
Official Release Archive
- Open the official LiveKit CLI release page.
- Download lk_2.19.2_darwin_arm64.tar.gz for an Apple silicon Mac.
- Download lk_2.19.2_darwin_amd64.tar.gz for an Intel Mac.
- Extract the matching archive in Finder.
- Place the extracted lk binary in a directory already available on your shell path.
- Open a fresh Terminal window through Spotlight.
- Repeat the LiveKit CLI version check shown above.
The output shows version 2.19.2.
Terminal Cannot Find the CLI?
Confirm that the extracted binary is named lk. Its containing directory must be available on your shell path.
Use Help me make the LiveKit CLI available in Terminal.
Version 2.19.2 is now available in Terminal. Authentication connects this local CLI to the LiveKit Cloud project you created.
The authentication command opens a browser login flow. Use the account that owns your new Build-plan project.
Before you authenticate, which LiveKit Cloud project do you expect your Mac to link?
- Start the LiveKit Cloud authentication flow by running this command:
lk cloud auth
What Does This Command Do?
The command starts a browser-based LiveKit Cloud login. Your approval links the local CLI to a project owned by that account.
- Complete the browser login with the account that owns your new project.
- Choose your Build-plan project if the flow requests a project.
- Return to Terminal after the browser confirms the connection.
The terminal confirms that a LiveKit Cloud project is linked. Your Mac can now run authenticated commands against that project.
Authentication Not Completing?
- Confirm that the browser uses the account that owns your new project.
- Close an expired browser flow.
- Repeat the authentication command shown above.
Use Help me troubleshoot the LiveKit Cloud authentication flow.
✔️ Awesome, I've got everything!
Your LiveKit Cloud project and local CLI now match the required setup.
ⓧ I'd like to double check the full code
- LiveKit CLI: Version 2.19.2 installed on macOS and authenticated to the LiveKit Cloud project.
- LiveKit Cloud project: Created on the $0 Build plan and linked to the local LiveKit CLI.
Your shared realtime network and local tools are ready. Next, you will build the Python voice pipeline and hear its first response.
Build and Hear the Python Voice Agent
Your authenticated LiveKit Cloud project gives the agent access to realtime inference. The next proof needs to show that the Python voice stack can complete a spoken turn.
Console mode isolates the cascaded voice pipeline from browser transport. It gives STT, an LLM, TTS, and turn detection a clean first test.
In this step, get ready to:
- Assemble the pinned backend project files.
- Generate local Cloud credentials and install the Python environment.
- Complete a spoken conversation with the agent in console mode.
Create the backend structure and configuration
The backend keeps the agent code separate from the browser client you add later. Its configuration pins every dependency that the voice pipeline needs.
You will use VS Code to create each file inside a named project folder on your Desktop.
- Select Desktop in the Finder sidebar.
- Click File in the macOS menu bar.
- Click New Folder.
- Enter voice-agent-studio-foundation as the folder name.
- Press Return to create the folder.
You will see voice-agent-studio-foundation on your Desktop.
- Open the voice-agent-studio-foundation folder in Finder.
- Click File in the macOS menu bar.
- Click New Folder.
- Enter backend as the folder name.
- Press Return to create the folder.
The backend folder now gives the Python project a fixed location.
- Open the backend folder in Finder.
- Click File in the macOS menu bar.
- Click New Folder.
- Enter src as the folder name.
- Press Return to create the folder.
You will see an empty src folder inside backend.
- Press Cmd+Space on macOS or the Windows key on Windows to open system search.
- Type Visual Studio Code into the search field.
- Press Enter to open the editor.
- Click File in the top menu bar.
- Click Open Folder.
- Select the backend folder inside voice-agent-studio-foundation on your Desktop.
- Click Open to load the backend folder in VS Code.
The Explorer sidebar now shows the backend folder with src inside it.
- Click the New File icon in the Explorer toolbar.
- Enter pyproject.toml as the file name.
- Add the pinned project metadata to pyproject.toml by pasting this configuration:
[project]
name = "voice-agent-studio-foundation"
version = "0.1.0"
description = "A real browser-to-agent LiveKit voice conversation"
requires-python = ">=3.10,<3.15"
dependencies = [
"livekit-agents==1.8.9",
"python-dotenv==1.2.4",
]
What does this configuration do?
- The requires-python range prevents the project from running with an unsupported Python version.
- The livekit-agents==1.8.9 dependency supplies the agent server and voice pipeline APIs.
- The python-dotenv==1.2.4 dependency loads local Cloud credentials into the agent process.
- Save pyproject.toml.
- Confirm pyproject.toml appears beside the src folder in the Explorer sidebar.
Configuration file missing?
Check that pyproject.toml is directly inside backend. A copy inside src will not define the backend environment.
Confirm that the file name ends with .toml. Help me check my backend Python project configuration.
✔️ Awesome, I've got everything!
Great. Double-check that you saved pyproject.toml inside backend.
ⓧ I'd like to double check the full code
[project]
name = "voice-agent-studio-foundation"
version = "0.1.0"
description = "A real browser-to-agent LiveKit voice conversation"
requires-python = ">=3.10,<3.15"
dependencies = [
"livekit-agents==1.8.9",
"python-dotenv==1.2.4",
]
- Click the New File icon in the Explorer toolbar.
- Enter .gitignore as the file name.
- Protect generated credentials and Python artifacts by pasting this configuration into .gitignore:
.env.local
.venv/
__pycache__/
*.pyc
What does this configuration protect?
- The .env.local rule keeps generated LiveKit credentials out of Git.
- The .venv/ rule excludes the installed local Python environment.
- The remaining rules exclude Python cache files that your runtime can recreate.
- Save .gitignore.
- Confirm .gitignore appears beside pyproject.toml in the Explorer sidebar.
Can't see the ignored file?
Make sure the file begins with a full stop. Check that your editor did not save it as .gitignore.txt.
Confirm that .env.local is the first line. Help me verify my backend ignore rules.
✔️ Awesome, I've got everything!
Your generated credentials and local Python files now have matching ignore rules.
ⓧ I'd like to double check the full code
.env.local
.venv/
__pycache__/
*.pyc
- Click the New File icon in the Explorer toolbar.
- Enter .env.local.example as the file name.
- Document the required credential names by pasting this template into .env.local.example:
LIVEKIT_API_KEY=your-api-key-here
LIVEKIT_API_SECRET=your-api-secret-here
LIVEKIT_URL=your-livekit-url-here
Why keep an environment template?
The template records the three settings that the agent expects. Its placeholder values make the file safe to keep with the project.
The generated .env.local file holds the real values. That file remains local because of the ignore rule you just added.
- Save .env.local.example.
- Confirm .env.local.example appears beside .gitignore in the Explorer sidebar.
Template showing real credentials?
Replace any real values in .env.local.example with the three placeholder values shown above. Real credentials belong only in the generated .env.local file.
Never paste a credential into an AI chat. Help me check whether my environment template is safe to keep.
✔️ Awesome, I've got everything!
Your safe environment template now documents every setting the agent needs.
ⓧ I'd like to double check the full code
LIVEKIT_API_KEY=your-api-key-here
LIVEKIT_API_SECRET=your-api-secret-here
LIVEKIT_URL=your-livekit-url-here
Generate credentials and install the environment
The next command writes credentials for the linked Cloud project into .env.local. The ignore rule keeps this sensitive file outside version control.
Credentials can feel risky because they authorize access to your project. Keep .env.local closed during screenshots and never share its contents.
- Click Terminal in the VS Code menu bar.
- Click New Terminal to start a terminal inside backend.
The terminal prompt should point to the backend folder that contains pyproject.toml.
- Write the linked project's credentials to .env.local by running this command:
lk app env -w
What does this command do?
The authenticated LiveKit CLI reads the linked Cloud project. It writes LIVEKIT_API_KEY, LIVEKIT_API_SECRET, and LIVEKIT_URL into .env.local.
The agent loads these values at runtime. You do not need separate model-provider credentials for LiveKit Inference.
- Confirm .env.local appears in the Explorer sidebar.
- Keep .env.local closed so its credential values remain private.
The new file confirms that the authenticated CLI can reach the linked Cloud project.
Credentials file not appearing?
Confirm that the terminal is inside backend. The command writes .env.local into the current folder.
If the CLI cannot identify a Cloud project, repeat the authenticated project selection flow without sharing any credential values. Help me troubleshoot the missing LiveKit environment file.
A virtual environment keeps this backend's Python packages isolated from other projects on your Mac. The pinned versions in pyproject.toml make that environment reproducible.
- Install the pinned backend dependencies by running this command:
uv sync
What does this command install?
The command reads pyproject.toml and resolves the pinned dependencies. It creates the local .venv environment for the backend.
The installed packages provide the agent runtime and the environment-file loader used by agent.py.
- Wait for the terminal to return to its prompt.
- Confirm the terminal finishes without a dependency installation error.
- Confirm .venv appears inside backend.
The backend now has an isolated environment with the exact agent dependencies installed.
Dependency installation failing?
Confirm that pyproject.toml is saved before rerunning the command. Check that the terminal is still inside backend.
An unsupported Python interpreter can also block the pinned packages. Help me diagnose this uv installation failure.
Build and test the voice agent
The agent file separates three responsibilities. StudioGuide defines behavior while AgentSession coordinates the voice pipeline.
The server entrypoint makes that session runnable from the LiveKit CLI. This structure keeps the agent's personality separate from its model configuration.
- Expand the src folder in the Explorer sidebar.
- Click the New File icon beside src.
- Enter agent.py as the file name.
- Add the imports and agent instructions to agent.py by pasting this code:
from dotenv import load_dotenv
from livekit import agents
from livekit.agents import (
Agent,
AgentServer,
AgentSession,
TurnHandlingOptions,
inference,
)
load_dotenv(".env.local")
class StudioGuide(Agent):
def __init__(self) -> None:
super().__init__(
instructions=(
"You are the first voice agent inside Voice Agent Studio. "
"Be friendly, concise, and conversational. "
"Answer in one or two short sentences unless the user asks for detail."
)
)
server = AgentServer()
What does this code do?
- The imports bring the agent server and voice pipeline APIs into the file.
- The load_dotenv() call loads credentials from .env.local into the process.
- The StudioGuide class gives the agent its concise conversational behavior.
- The AgentServer instance provides the runnable server entrypoint.
- Save agent.py.
- Confirm the editor recognizes StudioGuide as a class in the file.
Imports or strings highlighted as incomplete?
Check that every opening parenthesis has a matching closing parenthesis. Confirm that each instruction fragment has opening and closing quotation marks.
Keep server = AgentServer() outside the class indentation. Help me compare this first agent chunk.
- Leave two empty lines below server = AgentServer().
- Add the voice session entrypoint by pasting this code:
@server.rtc_session(agent_name="voice-agent-studio")
async def voice_agent(ctx: agents.JobContext) -> None:
session = AgentSession(
stt=inference.STT(
model="assemblyai/universal-3-6-pro",
language="en",
),
llm=inference.LLM(model="google/gemma-4-31b-it"),
tts=inference.TTS(
model="fishaudio/s2.1-pro",
voice="fa4c9eb3dccc4806b382b40d61c6b10a",
),
turn_handling=TurnHandlingOptions(
turn_detection=inference.TurnDetector(),
),
)
await session.start(
room=ctx.room,
agent=StudioGuide(),
)
await session.generate_reply(
instructions="Greet the user and ask what they want to explore about realtime voice AI."
)
How does the voice session work?
- The session sends microphone speech to the AssemblyAI STT model for transcription.
- The Gemma LLM generates the agent's response from that text.
- The Fish Audio TTS model converts the response into spoken audio.
- The turn detector decides when your spoken turn has ended so the pipeline can respond.
- Save agent.py.
- Confirm voice_agent appears below the server declaration.
- Confirm AgentSession contains the STT, LLM, TTS, and turn-handling settings.
Session block showing indentation problems?
Keep session and both await calls inside voice_agent. Each nested model setting needs one additional indentation level.
Confirm that the model and voice identifiers match the code exactly. Help me diagnose this AgentSession block.
- Leave two empty lines below the final session.generate_reply() call.
- Add the runnable application entrypoint by pasting this code:
if __name__ == "__main__":
agents.cli.run_app(server)
What does the entrypoint do?
The entrypoint runs the configured AgentServer when the file starts as an application. The LiveKit CLI uses that server to launch console mode.
- Save agent.py.
- Confirm the final two lines sit at the far-left indentation level.
Entrypoint nested inside the session?
Remove any leading spaces before if __name__ == "__main__":. Keep four spaces before agents.cli.run_app(server).
The entrypoint belongs below the completed voice_agent function. Help me fix the application entrypoint.
✔️ Awesome, I've got everything!
Your complete agent file is saved. The backend is ready for its first spoken test.
ⓧ I'd like to double check the full code
from dotenv import load_dotenv
from livekit import agents
from livekit.agents import (
Agent,
AgentServer,
AgentSession,
TurnHandlingOptions,
inference,
)
load_dotenv(".env.local")
class StudioGuide(Agent):
def __init__(self) -> None:
super().__init__(
instructions=(
"You are the first voice agent inside Voice Agent Studio. "
"Be friendly, concise, and conversational. "
"Answer in one or two short sentences unless the user asks for detail."
)
)
server = AgentServer()
@server.rtc_session(agent_name="voice-agent-studio")
async def voice_agent(ctx: agents.JobContext) -> None:
session = AgentSession(
stt=inference.STT(
model="assemblyai/universal-3-6-pro",
language="en",
),
llm=inference.LLM(model="google/gemma-4-31b-it"),
tts=inference.TTS(
model="fishaudio/s2.1-pro",
voice="fa4c9eb3dccc4806b382b40d61c6b10a",
),
turn_handling=TurnHandlingOptions(
turn_detection=inference.TurnDetector(),
),
)
await session.start(
room=ctx.room,
agent=StudioGuide(),
)
await session.generate_reply(
instructions="Greet the user and ask what they want to explore about realtime voice AI."
)
if __name__ == "__main__":
agents.cli.run_app(server)
- Keep the VS Code terminal focused inside backend.
- Approve microphone access if macOS displays a permission request after the agent starts.
Before you run the agent, do you expect the complete spoken pipeline to answer your first question?
- Start the agent in console mode by running this command:
lk agent console
What does console mode test?
Console mode starts one local voice session through the agent server. It captures microphone audio and runs it through the configured model pipeline.
This isolates the agent logic from browser networking. A successful response proves that the models and credentials cooperate.
You will hear StudioGuide greet you. The terminal will show live audio activity while the session listens.
- Ask What makes voice AI feel realtime? aloud.
- Pause after your question so turn detection can close your spoken turn.
- Listen for the agent's short spoken response.
The terminal activity moves from listening through response generation. You will hear the synthesized answer after the turn detector closes your input.
Agent not hearing or answering you?
Confirm that macOS granted microphone access to the terminal application running VS Code. Check that your microphone is available to another app.
If the session cannot reach inference, confirm that .env.local exists without opening or sharing it. Help me troubleshoot this console voice session.
- Take a screenshot of the terminal after the spoken response completes.
- Keep .env.local and all credential values outside the screenshot.
That successful answer proves the real model pipeline works. Console mode handled the audio locally, so no browser participant or WebRTC room took part.
This is the intended limitation of the first test. You now have a working voice agent that still needs realtime room transport.
- Press Ctrl+C in the terminal after capturing your result.
Your Python agent can now hear a spoken turn and answer through its cascaded pipeline. Next, you will connect the same agent to a named LiveKit room.
Connect the Agent to a WebRTC Room
Your console test proved that StudioGuide can process a spoken turn. It also proved that the voice pipeline can answer with synthesized speech.
Console mode keeps the test outside a shared realtime network. A named WebRTC room in LiveKit Cloud gives the agent a place to meet another participant through audio tracks.
In this step, get ready to:
- Trace the existing server-to-room runtime path.
- Connect the Python agent to my-test-room.
- Confirm the agent participant is waiting for browser audio.
Trace the room runtime path
The existing voice_agent() entrypoint already accepts a room job through ctx. Starting its AgentSession with ctx.room lets automatically managed RoomIO bridge the session to room media.
- Select backend/src/agent.py in the editor from earlier.
- Locate server = AgentServer().
- Find the @server.rtc_session(agent_name="voice-agent-studio") decorator.
- Follow the decorated function into voice_agent().
- Find the await session.start( call.
- Confirm that its room argument receives ctx.room.
How does the room path work?
- AgentServer owns the entrypoint launched by the LiveKit CLI.
- @server.rtc_session registers an RTC session named voice-agent-studio.
- voice_agent() receives the room job through ctx.
- AgentSession manages the existing STT to LLM to TTS pipeline.
- RoomIO is created automatically when the session starts. It bridges the session to room media tracks.
- A browser microphone track can become session input once its participant joins the same room.
- Synthesized speech returns to the room as the agent's published audio track.
The room behavior is already present in agent.py. No code changes are needed for direct connection mode.
✔️ Awesome, I've got everything!
Great. Your existing agent.py already connects AgentSession to the room supplied by the server job.
ⓧ I'd like to double check the full code
from dotenv import load_dotenv
from livekit import agents
from livekit.agents import (
Agent,
AgentServer,
AgentSession,
TurnHandlingOptions,
inference,
)
load_dotenv(".env.local")
class StudioGuide(Agent):
def __init__(self) -> None:
super().__init__(
instructions=(
"You are the first voice agent inside Voice Agent Studio. "
"Be friendly, concise, and conversational. "
"Answer in one or two short sentences unless the user asks for detail."
)
)
server = AgentServer()
@server.rtc_session(agent_name="voice-agent-studio")
async def voice_agent(ctx: agents.JobContext) -> None:
session = AgentSession(
stt=inference.STT(
model="assemblyai/universal-3-6-pro",
language="en",
),
llm=inference.LLM(model="google/gemma-4-31b-it"),
tts=inference.TTS(
model="fishaudio/s2.1-pro",
voice="fa4c9eb3dccc4806b382b40d61c6b10a",
),
turn_handling=TurnHandlingOptions(
turn_detection=inference.TurnDetector(),
),
)
await session.start(
room=ctx.room,
agent=StudioGuide(),
)
await session.generate_reply(
instructions="Greet the user and ask what they want to explore about realtime voice AI."
)
if __name__ == "__main__":
agents.cli.run_app(server)
Connect to my-test-room
Direct connection mode assigns this local agent process to one fixed room. The fixed name keeps the focus on media transport before production dispatch enters the architecture.
Why connect mode first?
Production agent dispatch selects an agent for each session through additional routing logic. Direct connection mode pins one local process to my-test-room.
This smaller path exposes the room model without adding token-based routing. You can see exactly where the browser participant joins in the next step.
- Return to the backend terminal from earlier.
- Stop the console process with your terminal's interrupt control if it is still active.
- Confirm that the terminal is inside backend/.
Before you run the connection command, do you expect the process to exit or stay active while it waits for another participant?
- Connect the agent directly to my-test-room by running this command:
uv run src/agent.py connect --room my-test-room
What does this command do?
- uv runs the application inside the installed backend environment.
- src/agent.py starts the existing agent server.
- connect selects direct development connection mode.
- --room my-test-room assigns the process to the fixed room named my-test-room.
You'll see terminal activity showing that the agent connected to my-test-room. The process stays active while it waits for a browser participant.
That is the transport layer online. Your voice-agent-studio participant is ready to receive microphone audio through RoomIO.
Agent not connecting to the room?
Confirm that the terminal is inside backend/. The relative path to .env.local depends on that location.
Check that backend/.env.local still contains the credentials written for the linked Cloud project. Keep those credential values private.
Confirm that the LiveKit CLI remains linked to the same Cloud project. Help me troubleshoot this room connection.
- Leave the backend terminal running while the agent waits in my-test-room.
Your Python agent is now waiting inside a real room. Next, the browser can join that same room to publish microphone audio and receive the agent's reply.
Join the Room from React
Your Python agent is waiting inside the LiveKit Cloud room from the previous step. The next participant is the person speaking through your browser.
A React client connects that person to the room over WebRTC. It publishes a microphone audio track for the agent. It also plays the agent's remote audio track.
In this step, get ready to:
- Enable development authentication for the browser client.
- Create the pinned frontend files and install their dependencies.
- Join the room from Safari and hold a real voice conversation.
Enable development authentication
The development token server gives the browser a temporary room token. That token lets the browser join without placing LiveKit API credentials in frontend code.
Why Use the Development Token Server?
The development token server is the shortest official route to a working local browser connection. It keeps this project focused on room media instead of adding a separate authentication backend.
This server is insecure for production. A production app generates tokens on a trusted backend because token generation requires API credentials.
- Return to the LiveKit Cloud project from the earlier setup.
- Open the project's Settings page.
- Enable Token server.
- Record the displayed ID here: your-development-token-server-id.
That is an important bridge completed. Your browser can now request a temporary token for the room without exposing the backend credentials.
Create the React client
The frontend uses pinned packages so every session hook behaves consistently. The files also keep authentication, room controls, audio playback, and styling in separate layers.
- In your editor's Explorer sidebar, select the voice-agent-studio-foundation folder that contains backend.
- Create a frontend folder beside backend using the Explorer toolbar's new-folder control.
- Create a src folder inside frontend using the same control.
The Explorer now shows the browser client as a sibling of the existing backend. This keeps both room participants inside one project folder.
- Create package.json inside frontend.
- Create tsconfig.json inside frontend.
- Create vite.config.ts inside frontend.
- Create index.html inside frontend.
- Create main.tsx inside frontend/src.
- Create App.tsx inside frontend/src.
- Create index.css inside frontend/src.
Each empty file now has a visible place in the Explorer. Use the reference tab to populate them with the pinned client.
✔️ Awesome, I've got everything!
Great. Double-check that all seven frontend files are saved in the locations shown above.
ⓧ I'd like to double check the full code
{
"name": "voice-agent-studio-frontend",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"preview": "vite preview"
},
"dependencies": {
"@livekit/components-react": "2.9.24",
"@livekit/components-styles": "1.2.0",
"livekit-client": "2.23.0",
"react": "19.3.1",
"react-dom": "19.3.1"
},
"devDependencies": {
"@types/react": "19.3.0",
"@types/react-dom": "19.3.0",
"@vitejs/plugin-react": "6.1.2",
"typescript": "6.0.3",
"vite": "8.3.5"
}
}
How Does This Manifest Shape the Client?
- The dependencies provide React, LiveKit room controls, audio rendering, and component styles.
- The development dependencies compile TypeScript through Vite.
- The scripts start the local development server or preview its production build.
{
"compilerOptions": {
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["src"]
}
What Does This TypeScript Configuration Do?
- Strict type checking catches invalid room and session values during development.
- Bundler module resolution matches how Vite loads the client modules.
- The JSX setting lets TypeScript compile React components from the src folder.
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [react()],
})
What Does the Vite Configuration Do?
- The React plugin transforms the JSX inside the TypeScript components.
- The exported configuration gives Vite the plugin when the development server starts.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Voice Agent Studio LiveKit foundation" />
<title>Voice Agent Studio</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
What Does This Page Provide?
- The root element gives React a place to render the interface.
- The module script loads main.tsx as the browser entry point.
- The metadata keeps the page readable on desktop and mobile screens.
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.tsx'
import './index.css'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)
How Does the React Entry Point Work?
- The entry point imports the application component and its stylesheet.
- The renderer mounts App inside the page's root element.
- Strict Mode adds development checks around the component tree.
import { useState } from 'react'
import {
ControlBar,
RoomAudioRenderer,
SessionProvider,
useSession,
} from '@livekit/components-react'
import '@livekit/components-styles'
import { TokenSource } from 'livekit-client'
const tokenSource = TokenSource.developmentTokenServer(
'[[DEVELOPMENT_TOKEN_SERVER_ID="your-development-token-server-id"]]',
)
export default function App() {
const session = useSession(tokenSource, {
roomName: 'my-test-room',
participantIdentity: 'browser-user',
})
const [error, setError] = useState<string | null>(null)
const startSession = async () => {
setError(null)
try {
await session.start()
} catch (reason) {
setError(reason instanceof Error ? reason.message : String(reason))
}
}
const endSession = async () => {
setError(null)
try {
await session.end()
} catch (reason) {
setError(reason instanceof Error ? reason.message : String(reason))
}
}
return (
<SessionProvider session={session}>
<main className="studio-shell" data-lk-theme="default">
<header className="hero">
<p className="eyebrow">Voice Agent Studio</p>
<h1>LiveKit Voice Foundations</h1>
<p>
Publish your microphone to a LiveKit room and hear a Python agent
answer through a real STT to LLM to TTS pipeline.
</p>
</header>
<section className="connection-card">
<div>
<p className="eyebrow">Room connection</p>
<strong>{session.connectionState}</strong>
</div>
<button
type="button"
onClick={session.isConnected ? endSession : startSession}
disabled={session.connectionState === 'connecting'}
>
{session.isConnected ? 'End session' : 'Start session'}
</button>
</section>
{session.isConnected && (
<section className="controls-card">
<p className="eyebrow">Microphone</p>
<ControlBar
controls={{
microphone: true,
camera: false,
screenShare: false,
}}
/>
</section>
)}
{(error || session.connectionState === 'disconnected') && (
<p className={error ? 'error-message' : 'helper-message'}>
{error ?? 'Start a session when the Python agent is waiting in my-test-room.'}
</p>
)}
<RoomAudioRenderer />
</main>
</SessionProvider>
)
}
How Does the Room Client Work?
- The token source uses your enabled development server to request temporary room access.
- The session joins my-test-room with the participant identity browser-user.
- The session actions connect or disconnect the browser while preserving visible error feedback.
- The control bar publishes microphone audio. RoomAudioRenderer plays subscribed room audio through the page.
:root {
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
"Segoe UI", sans-serif;
color: #eef2ff;
background: #070b17;
font-synthesis: none;
text-rendering: optimizeLegibility;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-width: 320px;
min-height: 100vh;
background:
radial-gradient(circle at 20% 0%, rgba(86, 104, 255, 0.22), transparent 34rem),
#070b17;
}
button {
border: 0;
border-radius: 999px;
padding: 0.75rem 1.15rem;
color: #07101f;
background: #78f0cf;
font: inherit;
font-weight: 750;
cursor: pointer;
}
button:disabled {
cursor: wait;
opacity: 0.55;
}
.studio-shell {
width: min(760px, calc(100% - 2rem));
margin: 0 auto;
padding: 4rem 0;
}
.hero,
.connection-card,
.agent-card,
.controls-card {
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 1.25rem;
background: rgba(14, 22, 42, 0.82);
box-shadow: 0 1.5rem 5rem rgba(0, 0, 0, 0.28);
}
.hero {
padding: clamp(1.5rem, 5vw, 3rem);
}
.hero h1 {
margin: 0.3rem 0 0.8rem;
font-size: clamp(2rem, 7vw, 4rem);
line-height: 0.98;
}
.hero p:last-child {
max-width: 60ch;
color: #aeb8d4;
}
.connection-card,
.controls-card {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin-top: 1rem;
padding: 1rem 1.25rem;
}
.agent-card {
margin-top: 1rem;
padding: 1.5rem;
}
.agent-card__header {
display: flex;
align-items: center;
gap: 0.85rem;
}
.agent-card h2 {
margin: 0;
text-transform: capitalize;
}
.eyebrow {
margin: 0 0 0.2rem;
color: #8d9abb;
font-size: 0.75rem;
font-weight: 800;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.status-dot {
width: 0.8rem;
height: 0.8rem;
border-radius: 50%;
background: #f4b860;
box-shadow: 0 0 1rem currentColor;
}
.status-dot[data-state="listening"] {
background: #78f0cf;
}
.status-dot[data-state="thinking"] {
background: #f4b860;
}
.status-dot[data-state="speaking"] {
background: #9ca7ff;
}
.status-dot[data-state="failed"] {
background: #ff7f96;
}
.audio-bars {
height: 9rem;
margin-top: 1.5rem;
}
.helper-message,
.error-message {
margin: 1rem 0 0;
border-radius: 0.8rem;
padding: 0.9rem 1rem;
}
.helper-message {
color: #aeb8d4;
background: rgba(255, 255, 255, 0.05);
}
.error-message {
color: #ffd9df;
background: rgba(255, 81, 118, 0.16);
}
@media (max-width: 560px) {
.studio-shell {
padding: 1.25rem 0;
}
.connection-card,
.controls-card {
align-items: stretch;
flex-direction: column;
}
}
What Does the Stylesheet Add?
- The shell and cards create a focused dark interface for the room controls.
- The button states show when a connection attempt is in progress.
- The helper and error styles keep connection feedback visible.
- The responsive rule stacks the controls on narrow screens.
- Save every frontend file by pressing Cmd+S on macOS or Ctrl+S on Windows.
- Confirm the Explorer shows the four files inside frontend and the three files inside frontend/src.
Vite compiles the TypeScript client while it runs. The npm manifest pins the exact packages that provide the room session and controls.
- Create a second integrated terminal rooted in frontend using your editor's terminal controls.
- Install the pinned frontend dependencies by running this command:
npm install
What Does This Command Do?
- The command reads the pinned packages from package.json.
- It downloads the dependencies required by the React client.
- It records a reproducible dependency tree for future installs.
The terminal returns to its prompt after the dependencies finish installing. Your frontend now has the session hooks and media components it needs.
Dependency Install Not Completing?
- Confirm the integrated terminal is rooted in the frontend folder that contains package.json.
- Check your network connection if package downloads stop progressing.
- Review the first dependency error in the terminal before retrying the install.
Use the error text without sharing credentials. Help me diagnose why npm install is failing in this pinned React frontend.
Start the browser conversation
Safari joins the same named room as the waiting Python agent. Vite keeps the current terminal occupied while it serves the client.
Before you run the client, do you expect the page to join the room immediately or wait for you to start the session?
- Start the Vite development server from the frontend terminal by running this command:
npm run dev
What Does This Command Do?
- The command starts the local Vite development server.
- The terminal displays the local URL for the React page.
- The server keeps running so saved frontend changes can reload in the browser.
- Record the displayed local URL here: http://localhost:5173.
- Keep the Vite terminal running during the voice test.
- Press Cmd+Space on macOS or the Windows key on Windows to open system search.
- Type Safari into system search.
- Press Enter to open Safari.
- Enter http://localhost:5173 in the Safari address bar.
- Press Enter to load the page.
You see the Voice Agent Studio page with a disconnected room state. The helper message points to the waiting my-test-room agent.
Keep the Voice Test Brief
Your $0 Build plan includes $2.50 in inference credits. Real speech recognition, language-model, and speech-synthesis calls consume those credits.
A few short spoken turns are enough to prove the complete room path. You can end the session as soon as the microphone and agent audio both work.
Before you start the session, do you think the browser microphone and the remote agent audio can travel through the same room?
- Click Start session on the connection card.
- Choose the browser option that allows microphone access when Safari asks.
- Speak one short question about realtime voice AI.
You see the connection state change to connected. You hear the Python agent answer through the page.
What Just Crossed the Room?
The browser-user participant published your microphone track to my-test-room. RoomIO delivered that track to the waiting agent session.
The agent published synthesized audio back to the room. RoomAudioRenderer subscribed to that remote track and played it through Safari.
- Select the microphone control in the LiveKit control bar to mute your track.
The control bar shows that your microphone is muted. The browser stops publishing new microphone audio.
- Select the microphone control again to unmute your track.
- Speak one more short question to confirm the agent can hear you again.
You hear another agent response after unmuting. The browser-to-agent WebRTC conversation now works in both directions.
Can't Hear the Agent?
- Confirm the backend terminal still shows the Python agent connected to my-test-room.
- Confirm Safari has microphone access for the local page.
- Confirm the connection card shows connected before speaking.
- Confirm the system output volume is audible.
Share only non-secret browser or terminal errors when asking for help. Help me trace why my LiveKit browser participant cannot hear the Python agent.
Your browser and Python agent now share a real room with live audio flowing in both directions. Next, you will expose the agent's listening, thinking, and speaking lifecycle on the page.
Make the Realtime Lifecycle Visible
Your browser and Python agent now exchange live audio through a WebRTC room. The conversation works, but audio alone hides the source of each delay.
Your React app needs a visible lifecycle for the agent. The useAgent hook exposes the signals that make this possible.
In this step, get ready to:
- Add a panel that displays the agent lifecycle state.
- Drive nine audio bars from the agent's remote audio track.
- Observe a spoken turn plus an interruption through visible state changes.
Add the agent lifecycle panel
The agent panel turns hidden session signals into visible feedback. Its audio bars move from the remote audio track's signal.
- Return to frontend/src/App.tsx in your editor from earlier.
- Locate the LiveKit component import near the top of the file.
- Update the component imports by replacing that block with this code:
import {
BarVisualizer,
ControlBar,
RoomAudioRenderer,
SessionProvider,
useAgent,
useSession,
} from '@livekit/components-react'
What do these imports provide?
- The useAgent hook provides the current lifecycle state plus the agent's remote microphone track.
- The BarVisualizer component converts that track's live signal into moving bars.
- The existing imports keep your session controls plus remote audio playback unchanged.
- Find the existing tokenSource declaration near the top of frontend/src/App.tsx.
- Add AgentPanel immediately below that declaration by pasting this code:
function AgentPanel() {
const agent = useAgent()
const failureMessage =
agent.state === 'failed' ? agent.failureReasons.join(', ') : null
return (
<section className="agent-card" aria-live="polite">
<div className="agent-card__header">
<span className="status-dot" data-state={agent.state} />
<div>
<p className="eyebrow">Agent state</p>
<h2>{agent.state}</h2>
</div>
</div>
<BarVisualizer
className="audio-bars"
state={agent.state}
track={agent.microphoneTrack}
barCount={9}
/>
{failureMessage && <p className="error-message">{failureMessage}</p>}
</section>
)
}
How does the panel expose the lifecycle?
- The useAgent hook keeps the component synchronized with the active agent.
- The state heading displays phases such as listening, thinking, or speaking.
- The status dot uses the same state to select its color.
- The visualizer combines agent.state with agent.microphoneTrack to show real remote audio activity.
- The failure message joins the available reasons when the agent reaches the failed state.
- Save frontend/src/App.tsx.
- Check the Vite terminal from earlier for a successful refresh without a compilation error.
Seeing a compilation error?
Check that BarVisualizer plus useAgent sit inside the existing component import braces.
Confirm that AgentPanel appears outside the App component. Help me diagnose the AgentPanel compilation error.
The component now compiles independently. Rendering it inside the connected session gives its hook access to the active agent.
- Locate the existing controls-card section inside the App component.
- Add the connected AgentPanel line immediately above that section by pasting this code:
{session.isConnected && <AgentPanel />}
Why render the panel here?
The connection check only renders AgentPanel while the browser has an active room session. This keeps agent feedback tied to a real participant connection.
- Save frontend/src/App.tsx.
- Return to the Safari page from earlier.
You should see an agent card above the microphone controls. The card shows a lifecycle label plus nine audio bars.
That is the hidden voice session made visible. You can now distinguish agent activity from the audio you hear.
Don't see the agent card?
Check that the room connection is active because the panel only renders while session.isConnected is true.
Confirm that the new AgentPanel line sits above the existing controls section. Help me find why AgentPanel is not rendering.
✔️ Awesome, I've got everything!
Great. Double-check that you saved frontend/src/App.tsx while keeping your configured development token server ID.
ⓧ I'd like to double check the full code
Compare this structure with your file. Keep the development token server ID that you configured earlier in place of the example value.
import { useState } from 'react'
import {
BarVisualizer,
ControlBar,
RoomAudioRenderer,
SessionProvider,
useAgent,
useSession,
} from '@livekit/components-react'
import '@livekit/components-styles'
import { TokenSource } from 'livekit-client'
const tokenSource = TokenSource.developmentTokenServer(
'your-development-token-server-id',
)
function AgentPanel() {
const agent = useAgent()
const failureMessage =
agent.state === 'failed' ? agent.failureReasons.join(', ') : null
return (
<section className="agent-card" aria-live="polite">
<div className="agent-card__header">
<span className="status-dot" data-state={agent.state} />
<div>
<p className="eyebrow">Agent state</p>
<h2>{agent.state}</h2>
</div>
</div>
<BarVisualizer
className="audio-bars"
state={agent.state}
track={agent.microphoneTrack}
barCount={9}
/>
{failureMessage && <p className="error-message">{failureMessage}</p>}
</section>
)
}
export default function App() {
const session = useSession(tokenSource, {
roomName: 'my-test-room',
participantIdentity: 'browser-user',
})
const [error, setError] = useState<string | null>(null)
const startSession = async () => {
setError(null)
try {
await session.start()
} catch (reason) {
setError(reason instanceof Error ? reason.message : String(reason))
}
}
const endSession = async () => {
setError(null)
try {
await session.end()
} catch (reason) {
setError(reason instanceof Error ? reason.message : String(reason))
}
}
return (
<SessionProvider session={session}>
<main className="studio-shell" data-lk-theme="default">
<header className="hero">
<p className="eyebrow">Voice Agent Studio</p>
<h1>LiveKit Voice Foundations</h1>
<p>
Publish your microphone to a LiveKit room and hear a Python agent
answer through a real STT to LLM to TTS pipeline.
</p>
</header>
<section className="connection-card">
<div>
<p className="eyebrow">Room connection</p>
<strong>{session.connectionState}</strong>
</div>
<button
type="button"
onClick={session.isConnected ? endSession : startSession}
disabled={session.connectionState === 'connecting'}
>
{session.isConnected ? 'End session' : 'Start session'}
</button>
</section>
{session.isConnected && <AgentPanel />}
{session.isConnected && (
<section className="controls-card">
<p className="eyebrow">Microphone</p>
<ControlBar
controls={{
microphone: true,
camera: false,
screenShare: false,
}}
/>
</section>
)}
{(error || session.connectionState === 'disconnected') && (
<p className={error ? 'error-message' : 'helper-message'}>
{error ?? 'Start a session when the Python agent is waiting in my-test-room.'}
</p>
)}
<RoomAudioRenderer />
</main>
</SessionProvider>
)
}
Test the lifecycle and interruption
The state label identifies the agent's current phase. The visualizer confirms when the subscribed agent track carries an audio signal.
- Return to the Safari page from earlier.
- Click Start session if the room connection reads disconnected.
- Wait for the agent state to settle on listening.
Before you speak, predict which state should follow after the agent detects the end of your turn.
- Ask the agent a short question about realtime voice AI.
- Pause after your question.
You should see the label move from listening to thinking. It then moves to speaking while the nine bars react to the agent's audio.
Before the next test, predict how the lifecycle should react when your voice interrupts the agent.
- Ask the agent to explain one benefit of realtime voice interfaces.
- Speak a follow-up while the state label reads speaking.
The agent stops its current reply. The state moves back toward listening as your new turn arrives.
State or bars not changing?
Confirm that the Python agent process from earlier is still connected to my-test-room. The browser needs that remote participant before useAgent can expose its lifecycle.
Check that the visualizer receives agent.microphoneTrack plus agent.state. Help me troubleshoot the agent state or static bars.
You have made the full voice lifecycle observable. Connection state, reasoning phases, synthesized speech, failures, plus interruptions now have visible signals.
Secret mission
Add a Second Live Audio Visualizer
Add a second signal-driven visualizer for the user's local microphone. You will see browser audio published toward the room separately from agent audio subscribed back from it.
Clean Up Your Resources
Clean Up Your Resources
Choose whether to keep your resources running, pause them for another session, or remove them from your Mac. Your LiveKit Cloud project stays on the $0/mo Build plan while usage remains within its included credits.
Resources you used:
- An enabled development Token server for browser authentication.
- The running voice-agent-studio Python agent.
- The running Vite development server.
- The local voice-agent-studio-foundation/ folder containing the Python agent, React client, and backend/.env.local credentials file.
Keep everything running
No action is needed while you are actively testing. Your cloud project remains ready for the next voice session.
- Leave the Token server enabled in your LiveKit Cloud project.
- Keep the voice-agent-studio-foundation/ folder on your Mac.
- Return to the Safari tab when you finish testing.
- Click End session to disconnect browser-user.
- Switch back to the terminal running the Python agent.
- Press Ctrl+C to stop the agent.
- Switch back to the terminal running the frontend.
- Press Ctrl+C to stop the Vite server.
Your test session is safely parked. The cloud configuration and local files remain ready for your return.
Pause - I'll come back to this later
Pausing keeps every project file while closing the active room connections. Disabling development authentication also prevents new browser sessions until you return.
- Return to the Safari tab containing Voice Agent Studio.
- Click End session to disconnect browser-user.
- Return to the Settings page for your LiveKit Cloud project.
- Turn off the Token server setting.
- Switch back to the terminal running the Python agent.
- Press Ctrl+C to disconnect the agent from my-test-room.
- Switch back to the terminal running the frontend.
- Press Ctrl+C to stop the Vite server.
Your development environment is now paused. Re-enable the Token server before your next browser test.
Delete - I don't want to use this again
Complete removal clears the local credentials and project files from your Mac. The cloud project can remain unused on its Build plan.
Before You Delete Local Files
The commands below permanently remove your local project files and generated credentials. Your LiveKit Cloud project stays available on its $0/mo Build plan.
- Return to the Safari tab containing Voice Agent Studio.
- Click End session to disconnect browser-user.
- Return to the Settings page for your LiveKit Cloud project.
- Turn off the Token server setting.
- Switch back to the terminal running the Python agent.
- Press Ctrl+C to disconnect the agent from my-test-room.
- Switch back to the terminal running the frontend.
- Press Ctrl+C to stop the Vite server.
- Remove backend/.env.local and the entire voice-agent-studio-foundation/ folder by running these commands from the backend terminal:
rm -f .env.local
cd ../..
rm -rf voice-agent-studio-foundation
What Do These Commands Remove?
- The rm -f .env.local command removes the generated LiveKit credentials file.
- The cd ../.. command moves from backend/ to the folder containing your project.
- The rm -rf voice-agent-studio-foundation command removes the complete local project folder.
- Confirm the project folder is gone by running this command:
ls
What Should You See?
The folder listing should no longer include voice-agent-studio-foundation.
Cleanup complete. Your local credentials and project files are removed without deleting the unused LiveKit Cloud project.
Nice Work!
Nice Work!
You did it! Your Python AgentSession now holds a real voice conversation with your React client through a LiveKit Cloud WebRTC room.
You've learned how to:
- Prove a real STT to LLM to TTS pipeline in console mode. Use turn detection to close each user turn before the reply begins.
- Connect the Python agent to the React browser through my-test-room. Publish microphone audio over WebRTC. Play the synthesized agent response through the page.
- Show connection errors in the React UI. Track the agent lifecycle from listening through speaking. Drive the audio bars from the subscribed agent track.
- Secret Mission: Add a User visualizer driven by the local microphone track. Preserve the Agent visualizer's state-aware response to the remote audio track.
Ready to quiz yourself?