Overview
Agents move through a lifecycle of states: connecting, listening, thinking, speaking, and eventually disconnecting or failing. Your frontend can read that state to show the right UI -- for example, when a user can talk, when an agent is busy, or if something has gone wrong. The agent publishes its state as the lk.agent.state participant attribute.
For most UI decisions you should use state getters, which are booleans like canListen and isFinished, rather than raw state, so your UI stays correct as the SDK evolves.
Agent states
State comes from two places. The agent SDK reports what the agent is doing (for example, listening or speaking), and the client reports connection and terminal outcomes. The following table defines each state.
| State | Description |
|---|---|
connecting | The client is connecting to the room. The agent is not yet in the loop. |
pre-connect-buffering | The app is connecting and may buffer user input before the connection is ready. Used for instant connect. Can be disabled so the client goes straight to the active states. |
initializing | The agent is connecting and setting up. |
idle | The agent is connected but idle (waiting for user input). |
listening | The agent is actively listening to the user. |
thinking | The agent is processing input or performing actions. |
speaking | The agent is producing audio output. |
disconnected | The user disconnected cleanly. This is the normal success outcome after ending a session. |
failed | The session entered an error state. Check the platform's failure API such as failureReasons for details. |
Lifecycles
The following diagrams show the lifecycle of an agent with or without instant connect enabled. The flow can end in failed from any state, such as if the agent never connects or leaves the room unexpectedly during the conversation.
With instant connect
In this flow, the agent connects, buffers input, initializes, and then can listen, think, speak, and eventually disconnect.
Loading diagram…
Without instant-connect
In this flow, the agent connects, initializes, and then can listen, think, speak, and eventually disconnect. It doesn't buffer input before the connection is ready.
Loading diagram…
Disconnected vs failed
The disconnected state is the successful status of a call ending. It means the user connected, then disconnected, and everything shut down correctly.
The failed state is the error status of a call ending. It means the state machine hit an error, such as connection or agent errors. When state is failed, use the platform's failure property like failureReasons to surface or handle the issues.
Using state getters
Use state getters for UI decisions to accommodate new states as they're added over time. State getters are booleans over raw state so your UI stays correct as the SDK evolves.
Only use raw states when you need to show a specific state in the UI, such as "Pre-connect buffering…" or "Connecting…" messages.
State getters and their associated descriptions and states can be found in the following table.
| Getter | Description | Associated states |
|---|---|---|
canListen | The user can speak. Input is accepted (including while the agent is thinking or speaking). | pre-connect-buffering, listening, thinking, speaking |
isConnected | The session is connected to the room. | listening, thinking, speaking |
isPending | The agent is in a transitional phase (connecting or setting up). | connecting, initializing, idle |
isFinished | The session has reached a terminal outcome. | disconnected, failed |
Accessing agent state
The following examples show how to read state and getters for each platform.
Use the useAgent hook to access the agent's state and getters:
import { useAgent } from '@livekit/components-react';function AgentStatus() {const agent = useAgent();return (<>{agent.canListen && (<div><p>Agent ready!</p><p>Agent is in state {agent.state}</p>{/* Show chat panel or other agent specific ui elements here */}</div>)}{agent.isFinished && (agent.failureReasons?.length > 0 ? (<p>Agent failed: {agent.failureReasons.join(', ')}</p>) : (<p>Agent disconnected.</p>))}</>);}
Access the agent from the session and use its state getters. The session's agent property exposes canListen, isFinished, agentState, and error:
@EnvironmentObject var session: Sessionvar body: some View {let agent = session.agentif agent.canListen {VStack {Text("Agent ready!")Text("Agent is in state \(String(describing: agent.agentState ?? .initializing))")// Show chat panel or other agent-specific UI here}}if agent.isFinished {if let error = agent.error {Text("Agent failed: \(error.localizedDescription)")} else {Text("Agent disconnected.")}}}
Use the rememberAgent composable to access the agent's state and getters.
@Composablefun AgentStatus() {val agent = rememberAgent()if (agent.canListen) {Column {Text("Agent ready!")Text("Agent is in state ${agent.agentState.name}")// Show chat panel or other agent-specific UI here}}if (agent.isFinished) {if (agent.failureReasons.isNotEmpty()) {Text("Agent failed: ${agent.failureReasons.joinToString(", ")}")} else {Text("Agent disconnected.")}}}
Custom state
The built-in agent states cover the agent's lifecycle, but your application may need to share additional state between the frontend and agent. For example, you might want to display which tool the agent is currently using, show a list of items the agent has found, or let the user toggle agent behavior from the UI.
For this kind of application-specific state, use LiveKit's state synchronization and RPC features. State synchronization keeps key-value data in sync across participants, while RPC lets you call methods on the agent or frontend from the other side.