LiveKit
The @mastra/livekit package connects Mastra agents to the LiveKit Agents framework. LiveKit runs the audio pipeline (voice activity detection, speech-to-text, turn detection, text-to-speech, barge-in) and the package bridges reply generation to a Mastra agent's stream() call.
See Using LiveKit with Mastra for setup and concepts.
The package has three entry points:
@mastra/livekit: server-side APIs —liveKitConnectionRoute(),dispatchVoiceSession(),pipeAgentReplyToWriter(),serializeSessionMetadata(), andcreateEndCallTool(). Import these from Mastra server code. This entry never loads the LiveKit agents runtime.@mastra/livekit/worker: the worker runtime —createLiveKitWorker(),runLiveKitWorker(),chatContextToMessages(), and the session helpersspeakGreeting(),waitForAgentDoneSpeaking(), andrunEndCall(). Import it only from the worker entry file.@mastra/livekit/plugin: the LLM-component plugin —MastraLLMandcreateRemoteAgentReplyGenerator(). Import it in workers that build their ownvoice.AgentSession.createRemoteAgentReplyGenerator()is also exported from@mastra/livekit/workerbecause it plugs intocreateLiveKitWorker()'sgenerateoption;MastraLLMis plugin-only.
createLiveKitWorker()Direct link to createlivekitworker
Builds a LiveKit agent definition that answers voice sessions with Mastra agents. Use it as the default export of your worker entry file.
import { fileURLToPath } from 'node:url'
import { createLiveKitWorker, runLiveKitWorker } from '@mastra/livekit/worker'
import { mastra } from './index'
export default createLiveKitWorker({
mastra,
agent: 'support',
stt: 'deepgram/nova-3',
tts: 'cartesia/sonic-3',
turnDetection: 'multilingual',
})
if (process.argv[1] === fileURLToPath(import.meta.url)) {
runLiveKitWorker({ entry: import.meta.url, agentName: 'mastra-voice' })
}
OptionsDirect link to Options
mastra:
agent?:
workflow?:
workflowInput?:
replyStep?:
resultText?:
generate?:
stt?:
tts?:
vad?:
turnDetection?:
turnHandling?:
sessionOptions?:
memory?:
toolFeedback?:
onTurnComplete?:
configuration?:
greeting?:
consentPolicy?:
endCall?:
stt?:
tts?:
greeting?:
persistGreeting?:
observability?:
voice call span per session: every turn's agent run nests under it, LiveKit's STT, TTS, end-of-utterance, VAD, and LLM latency metrics become child spans, and the span closes with a per-model usage roll-up. Pass false to disable.inputOptions?:
outputOptions?:
onSessionStart?:
runLiveKitWorker()Direct link to runlivekitworker
Starts the LiveKit worker CLI (dev, start, and connect subcommands) for a worker entry file. Call it from the file that default-exports the worker definition, guarded so it only runs when executed directly (the worker spawns a child process per session that re-imports the same file). Using this helper instead of cli.runApp from @livekit/agents guarantees the worker runtime and the bridge share one copy of the LiveKit SDK.
OptionsDirect link to Options
entry:
agentName?:
serverOptions?:
pipeAgentReplyToWriter()Direct link to pipeagentreplytowriter
Streams a Mastra agent's reply into a workflow step's writer on the workflow reply path. It forwards the agent's text deltas, so text-to-speech starts before the full reply is ready, and its tool-call chunks, so toolFeedback fires and onTurnComplete sees the tool list. Piping only stream.textStream silently drops tool calls. Pass the step's abortSignal to agent.stream() so barge-in stops generation promptly.
import { pipeAgentReplyToWriter } from '@mastra/livekit'
const generateResponse = createStep({
id: 'generateResponse',
// input and output schemas omitted
execute: async ({ inputData, mastra, writer, abortSignal }) => {
const stream = await mastra.getAgent('support').stream(inputData.turn, { abortSignal })
const reply = await pipeAgentReplyToWriter(stream, writer)
return { reply }
},
})
Returns: Promise<string>, the accumulated reply text.
ParametersDirect link to Parameters
agentStream:
writer:
chatContextToMessages()Direct link to chatcontexttomessages
Converts a LiveKit chat context into plain messages accepted by agent.stream(), excluding instructions and function calls. Use it in workflowInput to pass the full transcript into a stateless workflow.
import { createLiveKitWorker, chatContextToMessages } from '@mastra/livekit/worker'
export default createLiveKitWorker({
mastra,
workflow: 'phoneConversation',
workflowInput: ({ chatCtx }) => ({ history: chatContextToMessages(chatCtx) }),
})
Returns: VoiceTurnMessage[], where each entry is { role: 'system' | 'user' | 'assistant'; content: string; id?: string }.
MastraLLMDirect link to mastrallm
A standard LiveKit LLM plugin (llm.LLM) backed by a Mastra agent. Use it when you build the voice.AgentSession yourself and want Mastra in the llm slot; createLiveKitWorker() is the managed alternative. See Use Mastra as the LLM component for how to choose.
With remote, the plugin streams each turn from your Mastra server over HTTP using Server-Sent Events (SSE). The agent loop, tools, and memory run server-side, and interrupting the agent aborts the server-side generation.
import { voice } from '@livekit/agents'
import { MastraLLM } from '@mastra/livekit/plugin'
const session = new voice.AgentSession({
llm: new MastraLLM({
remote: { baseUrl: process.env.MASTRA_URL!, agentId: 'support' },
memory: { thread: callId, resource: userId },
}),
stt: 'deepgram/nova-3',
tts: 'cartesia/sonic-3',
// Required with `memory`: LiveKit enables preemptive generation by default.
turnHandling: { preemptiveGeneration: { enabled: false } },
})
The plugin reports provider as mastra and model as the agent id, so LiveKit metrics and fallback adapters identify it like any other LLM.
Constructor optionsDirect link to Constructor options
Provide exactly one reply source: remote, agent, or generate.
remote?:
agent?:
generate?:
memory?:
requestContext?:
toolFeedback?:
onToolCall?:
onTurnComplete?:
Don't combine memory with the session's preemptiveGeneration option, which LiveKit enables by default in sessions you build yourself. A speculative turn that completes before LiveKit discards it persists a user message and a never-spoken reply to the thread. Set turnHandling: { preemptiveGeneration: { enabled: false } } on the session. Stateless mode (no memory) works with preemptive generation.
Tools run on the Mastra agentDirect link to Tools run on the Mastra agent
Tools are defined and executed server-side on the Mastra agent. The plugin never forwards LiveKit tool definitions: if the session passes a non-empty toolCtx, it logs a one-time warning naming the ignored tools. Every tool must complete server-side — a tool that requires approval or client-side execution fails the turn with a descriptive error instead of hanging the call.
Tool activity reaches the worker through toolFeedback, onToolCall, and onTurnComplete.
InstructionsDirect link to Instructions
LiveKit injects your voice.Agent's instructions into the chat context of every request. The plugin drops them: the server-side Mastra agent's own instructions are authoritative. To change the prompt, change the Mastra agent.
Interrupted turnsDirect link to Interrupted turns
When the user interrupts a reply:
- The plugin cancels the stream. The server aborts generation and persists nothing from that turn.
- LiveKit records the part the user actually heard in its chat context, flagged as interrupted.
- On the next turn, the plugin re-sends that heard-only fragment, ordered before the new user message, so the memory thread backfills to match the call. Messages carry LiveKit's message ids and the server deduplicates by id, so retries and re-sends stay idempotent.
A user who hangs up immediately after interrupting leaves that final fragment unrecorded. When the transcript must capture it, reconcile immediately from the session event; the shared message id means the next turn's re-send upserts instead of duplicating:
import { voice } from '@livekit/agents'
import { MastraClient } from '@mastra/client-js'
const client = new MastraClient({ baseUrl: process.env.MASTRA_URL! })
session.on(voice.AgentSessionEventTypes.ConversationItemAdded, ({ item }) => {
if (item.type !== 'message' || item.role !== 'assistant' || !item.interrupted) return
void client.saveMessageToMemory({
agentId: 'support',
messages: [
{
id: item.id,
threadId: callId,
resourceId: userId,
role: 'assistant',
content: item.textContent ?? '',
type: 'text',
createdAt: new Date(),
},
],
})
})
Usage metricsDirect link to Usage metrics
When the server reports token usage for a turn, the plugin feeds it to LiveKit, so the session's metrics_collected events carry time-to-first-token, duration, and token counts like any LLM plugin. The same usage object (promptTokens, completionTokens, promptCachedTokens, totalTokens) arrives on onTurnComplete as result.usage.
Errors and timeoutsDirect link to Errors and timeouts
The transport throws LiveKit's APIError types (APIStatusError, APIConnectionError, APITimeoutError), so the session's retry policy (connOptions.maxRetry) and FallbackAdapter failover work unchanged. A turn is never retried after its first token — a voice reply is better failed fast than replayed half-heard.
A connect and first-token watchdog uses the session's connOptions.timeoutMs (10 seconds by default), so a server that accepts the connection but never streams can't cause indefinite dead air.
If the Mastra server goes down mid-call, each reply attempt fails with a typed error after its retries, and LiveKit closes the session after several consecutive failed replies. Restore the server before that budget runs out and the call recovers on the next turn.
Message contentDirect link to Message content
Message extraction is text-only: image content is dropped, and audio content is included only through its transcript. Voice pipelines aren't affected, but items you inject into the chat context yourself must carry text.
createRemoteAgentReplyGenerator()Direct link to createremoteagentreplygenerator
Builds a reply generator that runs the agent loop on a remote Mastra server over HTTP/SSE. MastraLLM's remote mode uses it internally. Use it directly through createLiveKitWorker's generate option to run the batteries-included worker against a remote server:
import { createLiveKitWorker, createRemoteAgentReplyGenerator } from '@mastra/livekit/worker'
import { mastra } from './index'
export default createLiveKitWorker({
mastra, // local instance for logger and worker config; replies come from the remote server
generate: createRemoteAgentReplyGenerator({
baseUrl: process.env.MASTRA_URL!,
agentId: 'support',
}),
memory: ({ metadata, roomName }) => ({ thread: metadata.threadId ?? roomName }),
stt: 'deepgram/nova-3',
tts: 'cartesia/sonic-3',
})
On the generate path the worker-level toolFeedback and onTurnComplete options don't apply, and the worker's end-call detection doesn't fire; pass the hooks to the generator instead.
Cancelling a turn (barge-in) tears down the HTTP request, which aborts generation on the server. Errors are thrown as LiveKit APIError types; retries applies to the initial connection only — a turn is never retried after its first chunk.
Returns: VoiceReplyGenerator.
OptionsDirect link to Options
baseUrl:
agentId:
apiPrefix?:
headers?:
fetch?:
timeoutMs?:
retries?:
body?:
toolFeedback?:
onToolCall?:
onTurnComplete?:
speakGreeting()Direct link to speakgreeting
Speaks an opening greeting on a session you own, honoring interruption and playout options. Returns the LiveKit SpeechHandle, or undefined when there's no greeting text. createLiveKitWorker() uses it internally for its greeting configuration.
import { speakGreeting } from '@mastra/livekit/worker'
await speakGreeting(session, {
text: "You've reached support. You're speaking with an AI assistant.",
allowInterruptions: false,
awaitPlayout: true,
})
ParametersDirect link to Parameters
session:
greeting:
waitForAgentDoneSpeaking()Direct link to waitforagentdonespeaking
Resolves once the agent is no longer producing or playing a reply — its state has left thinking and speaking. Resolves immediately when the agent is already idle, and always resolves within maxWaitMs (30 seconds by default) as a safety cap. Use it before tearing a session down so closing words play out instead of being cut off.
import { waitForAgentDoneSpeaking } from '@mastra/livekit/worker'
await waitForAgentDoneSpeaking(session)
runEndCall()Direct link to runendcall
Ends the call after the agent asked to: waits for the agent's closing words to finish, speaks an optional final message non-interruptibly, then deletes the room (hanging up the caller, SIP included) and shuts the job down, which runs registered shutdown callbacks.
Pair it with MastraLLM's onToolCall and an end-call tool on the server-side agent to rebuild agent-initiated hang-up on a session you own:
import { MastraLLM } from '@mastra/livekit/plugin'
import { DEFAULT_END_CALL_TOOL, runEndCall } from '@mastra/livekit/worker'
let ending = false
const llm = new MastraLLM({
remote: { baseUrl: process.env.MASTRA_URL!, agentId: 'support' },
onToolCall: ({ toolName }) => {
if (toolName !== DEFAULT_END_CALL_TOOL || ending) return
ending = true
void runEndCall(session, ctx, {}, console)
},
})
The exported constants DEFAULT_END_CALL_TOOL ('endCall'), DEFAULT_END_CALL_REASON, and DEFAULT_END_CALL_MAX_WAIT_MS (30000) hold the defaults.
ParametersDirect link to Parameters
session:
ctx:
config:
logger:
createEndCallTool()Direct link to createendcalltool
Builds the Mastra tool an agent calls to end the call itself: say goodbye, then hang up. The tool only signals intent (and runs optional bookkeeping) — the worker owns the actual hang-up. It lives on the server-safe root entry, so add it to agents defined in server code.
import { Agent } from '@mastra/core/agent'
import { createEndCallTool } from '@mastra/livekit'
const supportAgent = new Agent({
id: 'support',
name: 'Support',
instructions:
'Help the caller. When everything is wrapped up, say goodbye and call endCall as your final action.',
model: 'openai/gpt-5-mini',
tools: { endCall: createEndCallTool() },
})
With createLiveKitWorker(), set configuration: { endCall: {} } and the worker watches for the tool and hangs up. On a session you own, rebuild the hang-up with runEndCall().
OptionsDirect link to Options
id?:
description?:
onEndCall?:
liveKitConnectionRoute()Direct link to livekitconnectionroute
Returns an API route that mints a LiveKit access token with the voice agent dispatched into the room. Frontends call it to join a session.
import { Mastra } from '@mastra/core/mastra'
import { liveKitConnectionRoute } from '@mastra/livekit'
export const mastra = new Mastra({
server: {
apiRoutes: [liveKitConnectionRoute({ agentName: 'mastra-voice' })],
},
})
The route accepts a JSON body with optional agentId, threadId, and resourceId fields and responds with { serverUrl, roomName, participantName, participantToken }. The threadId defaults to the generated room name.
OptionsDirect link to Options
path?:
serverUrl?:
apiKey?:
apiSecret?:
agentName?:
ttl?:
requiresAuth?:
roomName?:
participantIdentity?:
metadata?:
dispatchVoiceSession()Direct link to dispatchvoicesession
Dispatches a Mastra voice agent into a LiveKit room programmatically — for server-initiated sessions such as outbound calls.
import { dispatchVoiceSession } from '@mastra/livekit'
await dispatchVoiceSession({
roomName: 'support-call-42',
agentName: 'mastra-voice',
metadata: { agentId: 'support', threadId: 'thread-42' },
})
OptionsDirect link to Options
roomName:
agentName?:
metadata?:
serverUrl?:
apiKey?:
apiSecret?:
LiveKitSessionMetadataDirect link to livekitsessionmetadata
The metadata passed from the Mastra server to the worker through LiveKit job dispatch.
agentId?:
threadId?:
resourceId?:
requestContext?:
The metadata travels as a JSON string. liveKitConnectionRoute() and dispatchVoiceSession() serialize it for you; use serializeSessionMetadata(metadata) when dispatching through your own code, or write the JSON directly in LiveKit-side configuration such as a SIP dispatch rule. Entries in requestContext reach the agent's dynamic instructions, tools, and input processors on every turn of the call.