Voix en temps réel
La voix en temps réel transforme un Agent Mastra en appel en direct auquel un utilisateur peut participer, dans le navigateur ou par téléphone. Mastra s’appuie sur LiveKit, une plateforme WebRTC open source pour l’audio et la vidéo en temps réel.
Le package @mastra/livekit connecte les Agents Mastra au framework LiveKit Agents : LiveKit gère la boucle audio, notamment la détection d’activité vocale, la transcription en continu, la détection sémantique des tours de parole, l’interruption et la synthèse vocale. Votre Agent Mastra génère chaque réponse avec son propre modèle, ses Tools et sa mémoire.
Utilisez la voix en temps réel lorsque vous avez besoin de conversations vocales interrompables et à faible latence. Pour de la parole à parole fondée sur un fournisseur sans LiveKit, consultez Parole à parole.
Démarrage rapideLien direct vers Démarrage rapide
Ces étapes vous font passer d’un projet vide à un Agent vocal avec lequel vous pouvez parler. Une session vocale comprend deux éléments à configurer ici : une route API sur votre serveur Mastra qui fournit les jetons d’accès, et un processus worker distinct qui exécute le pipeline audio et appelle votre Agent à chaque tour.
Installez le package d’intégration ainsi que les plugins LiveKit de détection d’activité vocale et de détection des tours de parole :
- npm
- pnpm
- Yarn
- Bun
npm install @mastra/livekit @livekit/agents @livekit/agents-plugin-silero @livekit/agents-plugin-livekitpnpm add @mastra/livekit @livekit/agents @livekit/agents-plugin-silero @livekit/agents-plugin-livekityarn add @mastra/livekit @livekit/agents @livekit/agents-plugin-silero @livekit/agents-plugin-livekitbun add @mastra/livekit @livekit/agents @livekit/agents-plugin-silero @livekit/agents-plugin-livekitDéfinissez vos identifiants LiveKit dans un fichier
.env. Créez un projet gratuit sur LiveKit Cloud, ou exécutez un serveur local aveclivekit-server --dev:.envLIVEKIT_URL=wss://your-project.livekit.cloudLIVEKIT_API_KEY=your-api-keyLIVEKIT_API_SECRET=your-api-secretAjoutez un Agent vocal à votre instance Mastra et exposez une route de connexion. L’assistant
liveKitConnectionRoute()ajoute un point de terminaisonPOST /voice/livekit/connection-detailsqui crée un jeton LiveKit et répartit votre Agent dans une salle :src/mastra/index.tsimport { Mastra } from '@mastra/core/mastra'import { Agent } from '@mastra/core/agent'import { liveKitConnectionRoute } from '@mastra/livekit'const supportAgent = new Agent({id: 'support',name: 'Support',instructions: 'You are a friendly phone support agent. Keep replies short and conversational.',model: 'openai/gpt-5-mini',})export const mastra = new Mastra({agents: { support: supportAgent },server: {apiRoutes: [liveKitConnectionRoute({ agentName: 'mastra-voice' })],},})Créez le worker. Il s’exécute comme processus distinct, répond aux sessions LiveKit et appelle votre Agent à chaque tour. Les API de worker se trouvent au point d’entrée
@mastra/livekit/worker, de sorte que le serveur Mastra ne charge jamais le runtime LiveKit Agents. Cet exemple utilise les chaînes de modèles LiveKit Inference pour la transcription et la synthèse vocale ; aucun plugin de fournisseur n’est donc requis :src/mastra/voice-worker.tsimport { 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',greeting: 'Hi! How can I help you today?',})if (process.argv[1] === fileURLToPath(import.meta.url)) {runLiveKitWorker({ entry: import.meta.url, agentName: 'mastra-voice' })}L’option
agentsélectionne l’Agent Mastra qui répond à chaque session. Transmettez une clé fixe, comme dans l’exemple, ou omettez-la pour utiliser l’agentIddes métadonnées de répartition ; un seul worker peut ainsi servir tous les Agents de votre instance Mastra.Téléchargez une fois les modèles de détection des tours de parole et d’activité vocale. Exécutez ensuite le worker dans un terminal et votre serveur Mastra dans un autre :
npx livekit-agents download-filesnpx tsx src/mastra/voice-worker.ts dev- npm
- pnpm
- Yarn
- Bun
npm run devpnpm run devyarn devbun run devLe worker s’enregistre auprès de votre serveur LiveKit et attend des sessions, tandis que
mastra devsert la route de connexion.Parlez à votre Agent. Ouvrez le LiveKit Agents Playground hébergé, puis connectez-le à votre projet pour démarrer un appel sans créer de frontend.
Pour connecter votre propre application, appelez plutôt la route de connexion pour obtenir un jeton.
POST /voice/livekit/connection-detailsaccepte les champs facultatifsagentId,threadIdetresourceIddans le corps de requête, puis renvoie :{"serverUrl": "wss://your-project.livekit.cloud","roomName": "mastra-voice-a1b2c3d4","participantName": "user-1","participantToken": "eyJhbGci..."}Cette réponse correspond au contrat employé par les kits de démarrage frontend de LiveKit ; les applications créées depuis agent-starter-react ou les composants LiveKit React fonctionnent donc sans modification.
Détection des tours de parole et interruptionsLien direct vers Détection des tours de parole et interruptions
LiveKit détermine quand l’utilisateur a fini de parler et quand l’Agent a été interrompu. Les valeurs par défaut fonctionnent bien ; ajustez-les avec turnHandling :
export default createLiveKitWorker({
mastra,
agent: 'support',
stt: 'deepgram/nova-3',
tts: 'cartesia/sonic-3',
turnDetection: 'multilingual',
turnHandling: {
endpointing: { mode: 'dynamic', minDelay: 300, maxDelay: 3000 },
interruption: { minDuration: 500, resumeFalseInterruption: true },
},
})
turnDetection: 'multilingual': exécute localement sur le processeur le modèle sémantique LiveKit de fin de tour. Il lit la transcription en direct afin d’éviter de couper l’utilisateur au milieu de sa pensée. Utilisez plutôt'vad'ou'stt'pour une détection de fin fondée sur le silence.endpointing: limite le temps d’attente de l’Agent après que l’utilisateur a cessé de parler.interruption: contrôle l’interruption. Lorsque l’utilisateur parle en même temps que l’Agent, LiveKit arrête la lecture et annule le flux Mastra en cours ; la génération de tokens s’arrête donc elle aussi.preemptiveGeneration: démarre la réponse de l’Agent Mastra alors que l’utilisateur finit encore de parler, masquant le délai avant le premier token. Le worker la désactive par défaut : chaque tentative anticipée exécute l’Agent Mastra sur une transcription provisoire, et chaque exécution conserve le message de l’utilisateur, ce qui duplique les messages dans le thread. Réactivez-la avecpreemptiveGeneration: { enabled: true }si la latence importe davantage que l’exactitude de l’historique du thread.
Consultez la documentation LiveKit sur la détection des tours pour toutes les options.
Voix et transcription par appelLien direct vers Voix et transcription par appel
Les options stt et tts de premier niveau s’appliquent à chaque appel. Pour les choisir par appel, avec une voix ou une langue par locataire, définissez plutôt les résolveurs configuration.stt et configuration.tts. Chaque résolveur s’exécute une fois par appel avec les métadonnées de répartition, le contexte de requête, le nom de la salle et le contexte du job, puis renvoie une valeur acceptée par l’option de premier niveau correspondante. Cette valeur est une instance de plugin ou une chaîne de modèle d’inférence. Renvoyez undefined pour revenir à l’option de premier niveau.
L’exemple suivant attribue à chaque locataire sa propre voix de synthèse vocale, à partir de l’entrée tenant dans les métadonnées de répartition :
import * as cartesia from '@livekit/agents-plugin-cartesia'
// One voice id per tenant, resolved from the dispatch metadata on each call.
const tenantVoices: Record<string, string> = {
meridian: 'your-cartesia-voice-id-1',
coastal: 'your-cartesia-voice-id-2',
}
// The resolver runs during call setup, so cache plugin instances across calls.
const ttsByVoice = new Map<string, cartesia.TTS>()
export default createLiveKitWorker({
mastra,
agent: 'support',
stt: 'deepgram/nova-3',
tts: 'cartesia/sonic-3',
configuration: {
tts: ({ requestContext }) => {
const voice = tenantVoices[requestContext?.tenant as string]
if (!voice) return undefined // fall back to the top-level `tts`
let tts = ttsByVoice.get(voice)
if (!tts) {
tts = new cartesia.TTS({ voice })
ttsByVoice.set(voice, tts)
}
return tts
},
},
})
configuration.stt fonctionne de la même manière pour la transcription par appel, par exemple avec un modèle de transcription ou une langue différente par locataire. Le message d’accueil possède une forme équivalente par appel : configuration.greeting.text accepte un résolveur ayant le même contexte d’appel, ce qui permet à un worker de commencer avec la formulation propre à chaque locataire.
Mémoire et threadsLien direct vers Mémoire et threads
Lorsque l’Agent Mastra résolu possède une mémoire configurée, chaque appel devient un thread de mémoire :
threadprend par défaut lethreadIddes métadonnées de répartition, puis le nom de la salle.resourceprend par défaut leresourceIddes métadonnées de répartition, puis le thread. Transmettez ici l’ID de votre utilisateur final afin de regrouper les appels sous le bon utilisateur. Mastra Studio envoie l’ID de l’Agent, conformément à la manière dont sa barre latérale liste les threads.- Lorsque le thread n’existe pas encore, le worker le crée avec le titre « Voice call » et les métadonnées
{ source: 'livekit' }. Le message d’accueil parlé est enregistré comme premier message de l’assistant afin que le thread constitue une transcription complète de l’appel ; désactivez cela avecpersistGreeting: false.
Chaque tour envoie uniquement la nouvelle entrée de l’utilisateur ; Mastra Memory fournit l’historique, le rappel sémantique et la mémoire de travail. Épinglez une session à un thread existant en transmettant threadId dans le corps de la requête de connexion ; c’est utile pour poursuivre une conversation texte par la voix. Dans Studio, démarrer un appel depuis une discussion ouverte lie l’appel à ce thread, et la transcription est ajoutée à la discussion après chaque échange.
Lorsqu’un utilisateur interrompt l’Agent, la génération en cours est abandonnée et aucun élément de ce tour n’est alors conservé. LiveKit garde dans sa transcription la partie que l’utilisateur a réellement entendue et, au tour suivant, le worker renvoie ce fragment entendu afin de compléter le thread pour qu’il corresponde à l’appel. Un utilisateur qui raccroche juste après l’interruption laisse ce dernier fragment non enregistré. Consultez les tours interrompus pour les détails et une méthode de réconciliation.
Parler pendant l’exécution des ToolsLien direct vers Parler pendant l’exécution des Tools
Les conversations vocales ne peuvent pas rester silencieuses lorsqu’un Tool lent s’exécute. Utilisez toolFeedback pour prononcer une courte phrase lorsque l’Agent Mastra démarre un appel de Tool :
export default createLiveKitWorker({
mastra,
agent: 'support',
stt: 'deepgram/nova-3',
tts: 'cartesia/sonic-3',
toolFeedback: ({ toolName }) =>
toolName === 'searchOrders' ? 'Let me look that up.' : undefined,
})
La phrase est prononcée dans la réponse et enregistrée dans la transcription.
Générer des réponses avec un WorkflowLien direct vers Générer des réponses avec un Workflow
Par défaut, le worker génère chaque réponse avec un Agent Mastra. Pour exécuter une logique en plusieurs étapes à chaque tour, par exemple classifier l’intention, router, appeler des Tools en séquence puis composer une réponse, générez plutôt les réponses avec un Workflow Mastra. Définissez workflow à la place de agent.
LiveKit gère toujours la boucle audio et appelle Mastra une fois par tour : le Workflow s’exécute donc jusqu’à son terme à chaque tour. Le Workflow ne peut ni être suspendu ni reprendre, et aucun état de conversation n’est conservé entre les tours. Transmettez la transcription via workflowInput afin que le Workflow reste sans état :
import { createLiveKitWorker, chatContextToMessages } from '@mastra/livekit/worker'
import { mastra } from './index'
export default createLiveKitWorker({
mastra,
workflow: 'phoneConversation',
workflowInput: ({ chatCtx }) => ({ history: chatContextToMessages(chatCtx) }),
replyStep: 'generateResponse',
stt: 'deepgram/nova-3',
tts: 'cartesia/sonic-3',
turnDetection: 'multilingual',
})
Un Workflow diffuse des événements d’étape structurés, et non du texte. Pour prononcer les tokens au fur et à mesure de leur génération, l’étape de réponse transfère le texte de son Agent vers le writer de l’étape :
const generateResponse = createStep({
id: 'generateResponse',
// input and output schemas omitted
execute: async ({ inputData, mastra, writer, abortSignal }) => {
const stream = await mastra.getAgent('voice').stream(inputData.history, { abortSignal })
await stream.textStream.pipeTo(writer)
return { assistantMessage: await stream.text }
},
})
replyStep: limite la sortie prononcée à une étape. Omettez-le pour prononcer chaque étape qui écrit dans sonwriter.resultText: solution de repli qui dérive la réponse du résultat final de l’exécution lorsqu’aucune étape ne diffuse de texte. La diffusion viawriterréduit le délai avant le premier token ; privilégiez-la.abortSignal: transmettez l’abortSignalde l’étape àagent.stream()afin que l’interruption arrête rapidement la génération. Lorsque l’utilisateur interrompt, le worker annule l’exécution.generate: pour un contrôle total, transmettez plutôt une fonctiongenerate. Il peut s’agir de tout générateur de réponses qui transforme un tour en flux texte.
Avec un Workflow, le worker ne conserve pas automatiquement les tours comme le fait stream() d’un Agent. Conservez l’historique de conversation dans le Workflow, ou gardez la transcription LiveKit comme source de vérité et transmettez-la à chaque tour.
Utiliser Mastra comme composant LLMLien direct vers Utiliser Mastra comme composant LLM
createLiveKitWorker() gère la session LiveKit pour vous. Pour gérer vous-même la session, utilisez plutôt MastraLLM : un plugin LLM LiveKit standard qui place un Agent Mastra dans l’emplacement llm de votre propre voice.AgentSession. L’application Mastra, la boucle d’Agent, les Tools, la mémoire et l’observabilité s’exécutent sur votre serveur Mastra, auquel le worker accède via HTTP. Le processus worker n’a besoin ni d’application Mastra, ni de base de données, ni de clés de fournisseur de modèles.
import { fileURLToPath } from 'node:url'
import { defineAgent, voice } from '@livekit/agents'
import * as silero from '@livekit/agents-plugin-silero'
import { MastraLLM } from '@mastra/livekit/plugin'
import { runLiveKitWorker } from '@mastra/livekit/worker'
export default defineAgent({
entry: async ctx => {
await ctx.connect()
const session = new voice.AgentSession({
llm: new MastraLLM({
remote: { baseUrl: process.env.MASTRA_URL!, agentId: 'support' },
memory: { thread: ctx.room.name!, resource: 'user-7' },
}),
stt: 'deepgram/nova-3',
tts: 'cartesia/sonic-3',
vad: await silero.VAD.load(),
// Required with `memory`: LiveKit enables preemptive generation by default.
turnHandling: { preemptiveGeneration: { enabled: false } },
})
await session.start({
// These instructions never reach the Mastra agent; its own instructions apply.
agent: new voice.Agent({ instructions: 'Replies come from the Mastra agent.' }),
room: ctx.room,
})
session.say('Hi! How can I help you today?')
},
})
if (process.argv[1] === fileURLToPath(import.meta.url)) {
runLiveKitWorker({ entry: import.meta.url, agentName: 'mastra-voice' })
}
Les deux approches partagent le même pipeline de réponse ; choisissez selon qui doit gérer la session :
createLiveKitWorker() | MastraLLM | |
|---|---|---|
| Gestion de session | L’assistant worker crée et gère l’AgentSession | Votre code crée la session ; toutes les options et tous les hooks LiveKit vous appartiennent |
| Où l’application Mastra s’exécute | Dans le processus worker | Sur votre serveur Mastra, accessible via HTTP ou dans le processus via agent |
| Besoins du processus worker | Votre application Mastra, votre stockage et les clés de fournisseur de modèles | Uniquement le SDK LiveKit et un accès réseau à votre serveur |
| Commodités intégrées | Message d’accueil, contrôle du consentement, raccrochage déclenché par l’Agent, initialisation de thread, agrégation de l’observabilité | Recréez ce dont vous avez besoin avec les assistants de session |
| Idéal pour | Le chemin le plus rapide vers un Agent vocal fonctionnel ; le mode vocal Studio | Applications LiveKit existantes et contrôle total de la session |
Les Tools restent sur l’Agent Mastra et s’exécutent sur le serveur. Les Tools côté LiveKit transmis à la session sont ignorés. L’activité des Tools parvient au worker via toolFeedback (formule d’attente prononcée), onToolCall (se déclenche au début de chaque appel de Tool) et onTurnComplete (se déclenche après chaque réponse avec le texte, les appels de Tools et l’usage de tokens). Le raccrochage déclenché par l’Agent ne prend que quelques lignes : associez onToolCall à runEndCall().
Ne combinez pas l’option memory avec preemptiveGeneration de LiveKit, que LiveKit active par défaut dans les sessions que vous créez vous-même. Un tour spéculatif qui se termine avant que LiveKit ne l’écarte conserve dans le thread un message utilisateur et une réponse jamais prononcée. Définissez turnHandling: { preemptiveGeneration: { enabled: false } }, ou exécutez sans memory et transmettez la transcription complète à chaque tour.
MastraLLM accepte également une instance agent Mastra dans le processus, la gestion de session sans second déploiement, ou une fonction generate personnalisée. Le transport distant est disponible de façon autonome sous la forme de createRemoteAgentReplyGenerator(), qui se branche aussi sur l’option generate de createLiveKitWorker pour exécuter le worker complet sur un serveur distant.
Sessions initiées par le serveurLien direct vers Sessions initiées par le serveur
Utilisez dispatchVoiceSession() pour ajouter un Agent vocal à une salle depuis votre propre code, par exemple afin de rejoindre une salle existante ou de lancer un appel SIP sortant :
import { dispatchVoiceSession } from '@mastra/livekit'
await dispatchVoiceSession({
roomName: 'support-call-42',
agentName: 'mastra-voice',
metadata: { agentId: 'support', threadId: 'thread-42', resourceId: 'user-7' },
})
ObservabilitéLien direct vers Observabilité
Lorsque l’observabilité est configurée sur l’instance Mastra, le worker trace chaque appel. Il ouvre une span voice call par session et y imbrique tous les éléments :
- L’exécution de l’Agent Mastra à chaque tour, avec la génération du modèle, les appels de Tools et les opérations de mémoire, exactement comme ils sont consignés par une discussion texte.
- Une span enfant pour chaque métrique du pipeline LiveKit : transcription, synthèse vocale, fin d’énoncé (détection de tour), détection d’activité vocale et délai du modèle avant le premier token. Elles contiennent les mesures de latence et d’audio que les traces texte ne peuvent pas montrer.
- Une agrégation d’utilisation par modèle, totaux de tokens, caractères et audio pour tout l’appel, écrite dans la span à la fin de la session.
Le worker est un processus distinct : pointez donc le stockage vers un backend qui accepte les écritures simultanées du serveur et du worker. LibSQL, basé sur SQLite, convient ; les stockages à un seul écrivain ne conviennent pas. Les traces, la mémoire et les threads peuvent partager un même stockage :
import { Mastra } from '@mastra/core/mastra'
import { LibSQLStore } from '@mastra/libsql'
import { Observability, MastraStorageExporter } from '@mastra/observability'
export const mastra = new Mastra({
storage: new LibSQLStore({ id: 'voice-agent-storage', url: 'file:./voice-agent.db' }),
observability: new Observability({
configs: {
default: {
serviceName: 'voice-agent',
exporters: [new MastraStorageExporter()],
},
},
}),
})
Le traçage est activé par défaut. Transmettez observability: false à createLiveKitWorker pour le désactiver.
DéploiementLien direct vers Déploiement
Le worker est un processus distinct de votre serveur Mastra ; mastra build doit donc le produire avec son propre point d’entrée. Ajoutez-le à bundler.entries :
import { Mastra } from '@mastra/core'
export const mastra = new Mastra({
bundler: {
entries: { 'voice-worker': './voice-worker.ts' },
// Keep LiveKit's native modules out of the bundle. `mastra build` only applies
// this default when you set no other bundler options, so set it explicitly here.
externals: true,
},
})
mastra build écrit désormais les deux processus dans .mastra/output, avec un même package.json et une seule installation des dépendances :
.mastra/output/
index.mjs # Mastra server
voice-worker.mjs # LiveKit worker
Déployez ce répertoire comme un artefact unique et démarrez chaque processus avec sa propre commande :
node .mastra/output/index.mjs # server
node .mastra/output/voice-worker.mjs start # worker
Le worker a besoin des mêmes variables d’environnement que le serveur, auxquelles s’ajoutent LIVEKIT_URL, LIVEKIT_API_KEY et LIVEKIT_API_SECRET.
Les recommandations de LiveKit sur le dimensionnement, l’arrêt gracieux et l’hébergement s’appliquent telles quelles. Consultez Deploying agents. Les workers se connectent à LiveKit en sortie et n’ont donc pas besoin de ports entrants.
FonctionnementLien direct vers Fonctionnement
Une session vocale LiveKit comporte trois éléments :
- Votre serveur Mastra crée un jeton d’accès LiveKit et répartit votre Agent dans une salle. La répartition transporte des métadonnées telles que l’ID d’Agent Mastra, le thread de mémoire et la ressource.
- Un worker d’Agent LiveKit, processus distinct de longue durée, reçoit le job et exécute le pipeline audio. L’audio circule entre le navigateur et le worker via WebRTC et ne passe jamais par votre serveur HTTP Mastra.
- Chaque fois que l’utilisateur termine un tour, le worker appelle
stream()de l’Agent Mastra avec la nouvelle entrée et prononce le texte diffusé. Lorsque l’utilisateur interrompt, LiveKit annule le flux et Mastra cesse de générer.
L’historique de conversation se trouve dans Mastra Memory ; les sessions vocales et les discussions texte peuvent donc partager un même thread.