How it works
- The main agent gets its tools, plus one tool per task and group.
- A task tool starts a run in the core. The run hands out one step at a time.
- Each step is a LiveKit
AgentTask, awaited inside the tool call. It opens on the history the core’s rule gives it. - When the run ends, the main agent gets its own history back. A caller turn a step answered for it comes too.
- Every model request goes through one
llm_node. It puts the prompt of the agent or step into the request as it reads now. - With no
model, or aLiveKitModelplugin, the library fills the prompt from the call state and the plugin answers. Its output passes through unchanged. - With a
ModelCall, the request goes to it in OpenAI format, with the prompt site (the place where the prompt is used) and the variables. A native call fills the prompt. The SLNG router fills it on its side. - Session events become spans. The names are in the Telemetry reference.
UnmuteAgent
UnmuteAgent is a LiveKit Agent. spec is the only positional argument. The others are keyword only.
AgentSpec
required
The agent: its prompt, tools or tasks, models, language and handoff tools. Its
name is the LiveKit id a handoff event shows. With no name, the id is the call’s agent_id. With no name and no state, LiveKit derives the id from the class name. Every field is on AgentSpec.CallState | None
default:"None"
The call state that prompts and tools read. None takes the state of
call, so the call keeps one state and one session_id. The spec’s tasks and save_values need one.CallHooks | None
default:"None"
The call’s hooks: tools on events, the failure line and the end of the call. Each model request waits for the tools on events. Give every agent of one call the same object. See The call’s life.
Telemetry | None
default:"None"
The call’s telemetry, new for this call. Spans start when the agent enters. It closes when the session closes.
Sequence[llm.Tool | llm.Toolset]
default:"()"
LiveKit tools the main agent gets as they are, like
EndCallTool. A @function_tool method of a subclass is one too. A step gets the ones its task lists in handoffs or native_tools.Mapping[str, ThenTarget] | None
default:"None"
The agents a group goes to when its
then is transfer, by then_target. Each one returns agent.hand_off(...). See Handoffs.tts.TTS | None
default:"None"
The agent’s voice. Its steps speak with it too. None uses the session’s TTS.
Any
LiveKit’s own
Agent options, passed on as they are. For example chat_ctx, stt, vad, turn_handling and allow_interruptions.AgentSpec refuses tools beside tasks, and a task model that task_models does not name. UnmuteAgent raises ValueError in these cases:
- The spec has
tasksorsave_values, and there is no state. - The spec’s timezone is unknown.
- A task lists a handoff the agent has no tool for.
- A group transfers to an agent
then_targetslacks. - The spec’s
handoffsnames a tool the agent does not have. - A task’s tool input names a prefetch lookup the call’s
prefetchlacks. - A placeholder makes a key longer than the model call takes. The router takes 64 characters. A native model has no limit.
- A task or group has the same name as another tool of the agent, like
end_call.
UnmuteAgent attributes
AgentSpec
The spec the agent was built with.
CallState | None
The call state the agent was built with. With none, it is the
state of the agent’s call hooks. state.call holds what the call carries.CallTasks | None
The call’s task runs, built from the spec. None for an agent without
tasks.tuple[str, ...]
The names of the agent’s handoff tools, from the spec.
UnmuteAgent.hand_off
A handoff tool returnsawait self.hand_off(UnmuteAgent(other_spec, state=self.state), announce=..., context=...). Build the next agent on this agent’s CallState, and without telemetry: it records into this agent’s spans. The model, voice, history and announce rules are on Handoffs. An agent built on another state raises ValueError. For a plain Agent, hand_off raises TypeError. Return the plain agent from the tool instead, built with shape_chat_ctx.
The model
With nomodel in the spec, the agent runs on the session’s LLM. To give the agent its own plugin, pass AgentSpec(model=LiveKitModel(plugin)). A LiveKit plugin answers as it does on a plain agent. The library fills the prompt and passes the plugin’s chunks through unchanged. A value reaches the model whole.
To use another provider, give the spec a ModelCall. OpenAIModelCall reaches any OpenAI-compatible endpoint. See Model call. The SLNG Context Router is one more ModelCall. Leave llm out of the session when you give one. Each router request carries your own router config, so the model runs on your own OpenAI key. See SLNG for the router.
- A
ModelCallis retried as the session retries its own LLM. It uses the session’sconn_options. It retries only before the first event, and only when the call does not retry itself (retries_itself). - When the session closes, the agent closes its
ModelCalls, like the router’s HTTP client. A LiveKit LLM plugin stays open. LiveKit owns it. - A model that fails after its reply began does not fail the call. The reply ends as it stands, and the next turn runs.
ModelCallLLM
ModelCallLLM runs a ModelCall as the session’s LLM. Use it on a plain agent.
ModelCall
required
The model call that answers.
CallState | None
default:"None"
The call state the prompt’s placeholders read.
SessionTrace | None
default:"None"
The session’s trace from
trace_session. Each request is then an llm span in its turn.str
default:"\"\""
The prompt site of each request.
"" is the main prompt, and task:<name> is a step.CallHooks | None
default:"None"
The call’s hooks. Each request waits for the tools on events and ends with their notes. None uses the trace’s hooks.
filled_llm
filled_llm is for a plain Agent that keeps its own LLM plugin. Call it from the agent’s llm_node. It fills the prompt from the call state on each request. It needs no Telemetry.
Agent
required
The agent whose session LLM answers.
llm.ChatContext
required
The chat LiveKit sends.
list[llm.Tool]
required
The tools LiveKit offers.
ModelSettings
required
LiveKit’s settings for this reply.
CallState | None
required
The state the prompt’s
{{placeholders}} read. Pass it by keyword.CallHooks | None
default:"None"
The call’s hooks. The request then waits for the background prefetch, ends with the tools’ notes, and runs the
prompt, before_model and after_model hooks.bool
default:"False"
End the request with the current values block, for
values_tool.traced_llm does the same and adds an llm span. See On a plain agent.
When the call has model hooks and the agent keeps LiveKit’s default llm_node with a plain plugin, the hooks cannot run. attach_call logs one warning at the first reply. See Hooks.
LiveKitModel
LiveKitModel(plugin) turns a LiveKit LLM plugin into a ModelCall. Its llm attribute is the plugin. Use it wherever a model goes: the spec’s model, summarizer and task_models, HandoffContext(summarizer=...) or a FallbackModelCall. Inside an UnmuteAgent, LiveKit holds the plugin itself. The plugin keeps its own client, settings and retries. Outside an UnmuteAgent, it sends the request through the plugin’s chat. A tool call’s provider data, like a Gemini thought signature, comes back as ToolCall.extra. The plugin sends it back when the call is replayed. So a FallbackModelCall of Gemini plugins keeps its signatures.
How the call opens and ends
Without aCallHooks, open the call step by step:
CallHooks, open_call opens the whole call:
open_callattaches the hooks and turns the caller’s audio off.- It runs the
before_callhooks and the prefetch, and starts the background lookups. - It greets. A failed check says the failure line and hangs up instead.
- The caller’s audio comes back when the agent starts to speak. With
interruptible=False, it comes back when the greeting ends. Either way it comes back afterGREETING_HOLD_SECONDS(20 seconds) at the latest, ormax_hold_s, so a greeting that never starts cannot hold the caller for ever. - A greeting the model words, on a realtime model, waits for the background lookups and for the instructions they change. That is at most the prefetch’s background budget for the lookups (
BACKGROUND_BUDGET_S, 10 seconds by default), then at mostSETTLE_WAIT_SECONDS(5 seconds) for the instructions. - If the caller leaves while the checks or the prefetch run,
open_callreturns None and greets no one. idle_nudges(session, config, call=call)andSipLine(session, call=call)take the hooks too. The idle hang-up is then noted asidle. A dial that ends on a voicemail is noted asagent_ended.- The caller leaving closes the session with LiveKit’s reason
participant_disconnected. The call’s end readscaller_hung_up. A tool that ends the call, a group withthen: endand a step’s ending tool readagent_ended. See Why the call ended.
sip_call_info
rtc.RemoteParticipant | None
required
The SIP participant, or None before it has joined.
Mapping[str, object]
required
The job’s metadata, parsed. Its
direction, when it says inbound or outbound, sets the direction.str | None
default:"None"
The agent’s own number, when the participant does not report it. Pass it by keyword.
inbound | outbound | None
default:"None"
The direction, when your dispatch knows it. It wins over everything else. Pass it by keyword.
str | None
default:"None"
The agent’s id, the same on every framework. Pass it by keyword.
str | None
default:"None"
The call’s id, when your dispatch gives one. A new one otherwise. Pass it by keyword.
CallInfo: the direction, the two numbers and the SIP call id as carrier_call_id. The room name is not stored. The direction is direction, else the metadata’s direction. Else a SIP participant with the sip.ruleID attribute came in through a dispatch rule, so it is inbound. Else a job that names phone_number dials out. Any other call is inbound. A participant without sip.ruleID does not mean outbound by itself. A contradiction between these is no longer refused.
greet
greet(session, greeting, state) says the line, has the model word a greeting, or waits for the caller. It returns the greeting’s SpeechHandle, or None when the caller speaks first. Call it after the session has started and prefetch has run.
AgentSession
required
The call’s session.
Greeting
required
How the call opens.
CallState | None
required
The call state the line’s values come from.
open_call
await open_call(session, greeting, call) returns the greeting’s SpeechHandle, or None when the caller speaks first or a check failed.
AgentSession
required
The call’s session, already started.
Greeting
required
How the call opens.
CallHooks
required
The call’s hooks, with the checks to pass.
float
default:"GREETING_HOLD_SECONDS"
The longest the caller’s audio stays off once the greeting is queued, in case its speech never starts. Pass it by keyword.
attach_call
attach_call(session, call) does only the attaching, once per call. It binds say, shutdown and the transcript. The first unrecoverable error event fails the call. LiveKit counts these errors per part and closes the session on its own only at the fourth of one part, so this is sooner. A TTS error is only logged, and the call stays open. LiveKit still closes the session at the fourth unrecoverable TTS error. The session’s close runs call.end, and the job’s shutdown waits for it.
AgentSession
required
The call’s session.
CallHooks
required
The call’s hooks.
max_duration
max_duration(session, seconds, call=call) closes the session after that many seconds, with no closing line. It stops counting when the session closes first. Give it call=call and the call’s end reads max_duration. It returns the running CallLimit.
AgentSession
required
The call’s session.
float
required
The longest the call may run. It must be above zero.
CallHooks | None
default:"None"
The call’s hooks. With them, the call’s end reads
max_duration.At the end of the call
When the session closes, the agent settles the steps’ held saves and closes its models before the telemetry closes. Inside a LiveKit job, the job’s shutdown waits for all of it. The library stops waiting after 15 seconds. LiveKit kills the job process aftershutdown_process_timeout, which is 10 seconds by default. So raise it in WorkerOptions if the saves or the telemetry need longer. See Tasks.
Phone legs
SipLine is a SIP call’s phone leg. It transfers the caller and dials out. See Phone calls.
Native tools and MCP
native_tools gives the main agent LiveKit tools as they are. A LiveKit MCP toolset is one. A step gets only the toolsets its task lists, by id. It borrows the tools inside a toolset and never the toolset itself. So a step that ends cannot close the main agent’s toolset.
Rules to know
- A subclass that overrides
on_entermust callsuper().on_enter(), or no span starts. - A subclass that overrides
tts_nodemust callsuper().tts_node(...), or thettsspans stop. - Build one
AgentSpec, oneUnmuteAgentand oneTelemetryper call. Build theModelCallper call too, after the state. The agent closes the models it was given when the call ends, after the call’s end has finished, so aRouterModelCallorOpenAIModelCallshared across calls is closed under the others. - Give the spec
toolsortasks, not both. The task plan holds every tool. - A
ModelCallthat raises insideUnmuteAgentfails the call. LiveKit alone only logs a failedllm_node. A model that fails after its reply began does not fail the call. - Gemini’s OpenAI-compatible endpoint keeps only the last system message.
OpenAIModelCallfolds the library’s several into one for a Gemini model. A native OpenAI-compatible plugin pointed at that endpoint sends them as they are, so use the native Gemini plugin there. See Gemini and several system messages. - With LiveKit preemptive generation,
{{call.first_user_message}}can be a partial transcript. - LiveKit’s own spans stay off. The adapter does not call
set_tracer_provider. - A request a
ModelCallanswers throughUnmuteAgentemits LiveKit’sLLMMetrics, with the tokens and the time to the first token.metrics_collectedandsession.usagecount it as they count an LLM plugin. - LiveKit’s AEC warm-up (
AgentSession(aec_warmup_duration=...), 3 seconds by default, off on an outbound SIP call) turns the caller’s audio into silence while the agent speaks during that time. The caller cannot cut in on the greeting then, even thoughopen_callturns their audio back on when the agent starts to speak. EndCallToolasks the model for a goodbye reply, then ends the call. Withdelete_room=True, its default, it deletes the room, which drops a phone caller. AToolSpec(effect="ends_conversation")says only itsannounceline and closes the session. Setroom_io.RoomOptions(delete_room_on_close=True)so the phone caller is dropped then too.- LiveKit always lets a running tool finish. Tool fields and webhooks are in Tools.
- A tool that ends the call, beside another tool in the same reply, makes no second reply. The library cancels the reply for all of them.
- A line a tool waits for (
announce_wait=True) and the lines aSipLinesays on a transfer cannot be cut off by the caller. - In
recordmode, a reply that spoke and called nothing can be followed by a short finish check of up to 8 seconds. See Tasks.
Where to go next
LiveKit
Run the library in your own LiveKit agent.
AgentSpec
Every field of the agent, the same on both frameworks.
Telemetry reference
Every span and attribute the adapter writes.
Pipecat adapter reference
The same detail for Pipecat.