メインコンテンツへ移動

リアルタイム Voice

リアルタイム Voice は、Mastra Agent を、ブラウザまたは電話でユーザーが割り込んで話せるライブ通話へ変えます。Mastra は、リアルタイムの音声と映像に対応するオープンソース WebRTC プラットフォーム LiveKit を基盤にしています。

@mastra/livekit パッケージは、Mastra Agent を LiveKit Agents framework に接続します。LiveKit は、音声区間検出、ストリーミング Speech-to-Text、意味に基づくターン検出、割り込み、Text-to-Speech などの音声ループを担います。Mastra Agent は、独自のモデル、Tools、Memory を使って各応答を生成します。

低遅延で割り込み可能な音声会話が必要な場合は、リアルタイム Voice を使用してください。LiveKit を使わない Provider ベースの Speech-to-Speech については、Speech-to-Speechを参照してください。

クイックスタート
クイックスタートへの直接リンク

ここでは、空のプロジェクトから会話できる Voice Agent を構築します。Voice セッションは 2 つの要素で構成されます。アクセストークンを発行する Mastra サーバー上の API ルートと、音声パイプラインを実行して各ターンで Agent を呼び出す独立したワーカープロセスです。

  1. 統合パッケージと、音声区間検出およびターン検出用の LiveKit プラグインをインストールします。

    npm install @mastra/livekit @livekit/agents @livekit/agents-plugin-silero @livekit/agents-plugin-livekit
  2. LiveKit の認証情報を .env ファイルに設定します。LiveKit Cloud で無料プロジェクトを作成するか、livekit-server --dev でローカルサーバーを実行してください。

    .env
    LIVEKIT_URL=wss://your-project.livekit.cloud
    LIVEKIT_API_KEY=your-api-key
    LIVEKIT_API_SECRET=your-api-secret
  3. Mastra インスタンスに Voice Agent を追加し、接続ルートを公開します。liveKitConnectionRoute() ヘルパーは、LiveKit トークンを発行して Agent をルームへディスパッチする POST /voice/livekit/connection-details エンドポイントを追加します。

    src/mastra/index.ts
    import { 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' })],
    },
    })
  4. ワーカーを作成します。ワーカーは独立したプロセスとして実行され、LiveKit セッションに応答し、各ターンで Agent を呼び出します。Worker API は @mastra/livekit/worker エントリポイントにあるため、Mastra サーバーが LiveKit Agents ランタイムを読み込むことはありません。この例では、Speech-to-Text と Text-to-Speech に LiveKit Inference のモデル文字列を使うため、Provider プラグインは不要です。

    src/mastra/voice-worker.ts
    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',
    greeting: 'Hi! How can I help you today?',
    })

    if (process.argv[1] === fileURLToPath(import.meta.url)) {
    runLiveKitWorker({ entry: import.meta.url, agentName: 'mastra-voice' })
    }

    agent オプションは、各セッションに応答する Mastra Agent を選択します。例のように固定キーを渡すか、省略してディスパッチメタデータの agentId を使用すると、1 つのワーカーで Mastra インスタンス上のすべての Agent に対応できます。

  5. ターン検出モデルと音声区間検出モデルを一度ダウンロードします。次に、一方のターミナルでワーカーを、もう一方で Mastra サーバーを実行します。

    npx livekit-agents download-files
    npx tsx src/mastra/voice-worker.ts dev
    npm run dev

    ワーカーは LiveKit サーバーに登録してセッションを待ち、mastra dev は接続ルートを提供します。

  6. Agent と会話します。ホスト型の LiveKit Agents Playground を開いてプロジェクトに接続すると、フロントエンドを構築せずに通話を開始できます。

    独自のアプリを接続する場合は、接続ルートを呼び出してトークンを取得します。POST /voice/livekit/connection-details は、リクエストボディで任意の agentIdthreadIdresourceId フィールドを受け取り、次を返します。

    {
    "serverUrl": "wss://your-project.livekit.cloud",
    "roomName": "mastra-voice-a1b2c3d4",
    "participantName": "user-1",
    "participantToken": "eyJhbGci..."
    }

    このレスポンスは LiveKit のフロントエンドスターターが使用する仕様に準拠しているため、agent-starter-react または LiveKit React components で構築したアプリは変更なしで動作します。

ターン検出と割り込み
ターン検出と割り込みへの直接リンク

LiveKit は、ユーザーが話し終えたタイミングと Agent が割り込まれたタイミングを判断します。デフォルト設定で十分に動作しますが、turnHandling で調整できます。

src/mastra/voice-worker.ts
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': LiveKit の意味に基づくターン終了モデルをローカルの CPU で実行します。ライブ文字起こしを読み、ユーザーの発話途中で打ち切らないようにします。無音に基づくエンドポイント検出には、代わりに 'vad' または 'stt' を使用します。
  • endpointing: ユーザーが話し終えてから Agent が待つ時間の範囲を設定します。
  • interruption: 割り込みを制御します。ユーザーが Agent の発話中に話すと、LiveKit は再生を停止し、進行中の Mastra ストリームをキャンセルするため、トークン生成も停止します。
  • preemptiveGeneration: ユーザーが話し終える前に Mastra Agent の応答生成を開始し、最初のトークンまでの待ち時間を見えにくくします。ワーカーではデフォルトで無効です。先行生成のたびに暫定的な文字起こしで Mastra Agent が実行され、各実行でユーザーメッセージが永続化されるため、スレッド内でメッセージが重複します。正確なスレッド履歴よりレイテンシを優先する場合は、preemptiveGeneration: { enabled: true } で再度有効にしてください。

すべてのオプションについては、LiveKit のターン検出ドキュメントを参照してください。

通話ごとの Voice と文字起こし
通話ごとの Voice と文字起こしへの直接リンク

トップレベルの stttts オプションは、すべての通話に適用されます。通話ごと、またはテナントごとに Voice や言語を選ぶには、代わりに configuration.sttconfiguration.tts のリゾルバーを設定します。各リゾルバーは、ディスパッチメタデータ、RequestContext、ルーム名、ジョブコンテキストを受け取り、通話ごとに一度実行されます。戻り値は対応するトップレベルオプションで使用できる値で、プラグインインスタンスまたは推論モデル文字列です。undefined を返すとトップレベルオプションにフォールバックします。

次の例では、ディスパッチメタデータの tenant エントリをキーとして、各テナントに固有の Text-to-Speech Voice を割り当てます。

src/mastra/voice-worker.ts
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 も同様に、テナントごとに異なる文字起こしモデルや言語を使うなど、通話ごとの文字起こしに利用できます。挨拶にも通話ごとの形式があります。configuration.greeting.text は同じ通話コンテキストを受け取るリゾルバーに対応しているため、1 つのワーカーでテナントごとに異なる挨拶を使用できます。

Memory とスレッド
Memory とスレッドへの直接リンク

解決された Mastra Agent に Memory が設定されている場合、各通話が 1 つの Memory スレッドになります。

  • thread は、デフォルトでディスパッチメタデータの threadId を使用し、なければルーム名を使用します。
  • resource は、デフォルトでディスパッチメタデータの resourceId を使用し、なければスレッドを使用します。エンドユーザーの ID をここに渡すと、通話を適切なユーザーのもとにまとめられます。Mastra Studio は Agent ID を渡し、サイドバーのスレッド一覧と対応させます。
  • スレッドがまだ存在しない場合、ワーカーはタイトルを「Voice call」、メタデータを { source: 'livekit' } として作成します。発話した挨拶は最初の assistant メッセージとして保存されるため、スレッドを完全な通話記録として読めます(persistGreeting: false で無効化できます)。

各ターンでは新しいユーザー入力だけを送信し、Mastra Memory が履歴、セマンティックリコール、ワーキングメモリを提供します。接続リクエストボディで threadId を渡すと、セッションを既存スレッドに固定できます。テキストでの会話を音声で続ける場合に便利です。Studio では、開いているチャットから通話を開始すると、そのスレッドに通話が関連付けられ、やり取りのたびに文字起こしがチャットへ追加されます。

ユーザーが Agent に割り込むと、進行中の生成が中止され、その時点ではそのターンの内容は永続化されません。LiveKit はユーザーが実際に聞いた部分を文字起こしに保持し、次のターンでワーカーがその部分だけを再送して、スレッドを通話内容に合わせて補完します。割り込み直後にユーザーが通話を終了した場合、最後の断片は記録されません。詳細と整合方法については、割り込まれたターンを参照してください。

Tool の実行中に発話する
Tool の実行中に発話するへの直接リンク

時間のかかる Tool の実行中に、音声会話が無音になることは避ける必要があります。Mastra Agent が Tool 呼び出しを開始したときに短いフレーズを発話するには、toolFeedback を使用します。

src/mastra/voice-worker.ts
export default createLiveKitWorker({
mastra,
agent: 'support',
stt: 'deepgram/nova-3',
tts: 'cartesia/sonic-3',
toolFeedback: ({ toolName }) =>
toolName === 'searchOrders' ? 'Let me look that up.' : undefined,
})

このフレーズは応答の一部として発話され、文字起こしに記録されます。

Workflow で応答を生成する
Workflow で応答を生成するへの直接リンク

デフォルトでは、ワーカーは Mastra Agent で各応答を生成します。ターンごとに複数ステップのロジックを実行する場合(たとえば、意図の分類、ルーティング、Tool の順次呼び出し、応答の作成)は、代わりに Mastra Workflow で応答を生成します。agent の代わりに workflow を設定してください。

音声ループは引き続き LiveKit が担い、ターンごとに一度 Mastra を呼び出すため、Workflow は各ターンで完了まで実行されます。Workflow は一時停止や再開ができず、会話状態はターン間で引き継がれません。Workflow をステートレスに保つため、workflowInput から文字起こしを渡します。

src/mastra/voice-worker.ts
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',
})

Workflow はテキストではなく、構造化されたステップイベントをストリーミングします。生成されたトークンを順次発話するには、応答ステップで Agent のテキストをステップの writer にパイプします。

src/mastra/workflows/phone-conversation.ts
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: 発話出力を 1 つのステップに限定します。省略すると、writer に書き込むすべてのステップを発話します。
  • resultText: どのステップもテキストをストリーミングしない場合に、最終実行結果から応答を導出するフォールバックです。writer 経由のストリーミングは最初のトークンまでの時間を短縮できるため、そちらを優先してください。
  • abortSignal: 割り込み時に生成をすぐ停止できるよう、ステップの abortSignalagent.stream() に渡します。ユーザーが割り込むと、ワーカーは実行をキャンセルします。
  • generate: 完全に制御するには、代わりに generate 関数を渡します。ターンをテキストストリームに変換する任意の応答ジェネレーターを使用できます。

Workflow を使う場合、Agent の stream() のようにワーカーがターンを自動的に永続化することはありません。会話履歴を Workflow 内で永続化するか、LiveKit の文字起こしを信頼できる唯一の情報源として、各ターンで渡してください。

Mastra を LLM コンポーネントとして使用する
Mastra を LLM コンポーネントとして使用するへの直接リンク

createLiveKitWorker() は LiveKit セッションを管理します。セッションを自分で管理するには、代わりに MastraLLM を使用します。これは標準の LiveKit LLM プラグインで、独自の voice.AgentSessionllm スロットに Mastra Agent を配置します。Mastra アプリ、Agent ループ、Tools、Memory、Observability は Mastra サーバー上で動作し、ワーカーは HTTP 経由で接続します。ワーカープロセスに Mastra アプリ、データベース、モデル Provider のキーは不要です。

src/mastra/voice-worker-plugin.ts
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' })
}

どちらも内部では同じ応答パイプラインを共有しています。セッションをどちらが管理するかで選択してください。

createLiveKitWorker()MastraLLM
セッションの管理ワーカーヘルパーが AgentSession を構築して管理独自のコードでセッションを構築し、LiveKit のすべてのオプションとフックを制御
Mastra アプリの実行場所ワーカープロセス内HTTP 経由で接続する Mastra サーバー上(または agent による同一プロセス内)
ワーカープロセスに必要なものMastra アプリ、ストレージ、モデル Provider のキーLiveKit SDK とサーバーへのネットワークアクセスのみ
組み込み機能挨拶、同意ゲート、Agent による通話終了、スレッドの初期化、Observability の集計セッションヘルパーで必要な機能を構築
適した用途動作する Voice Agent をすばやく構築、Studio Voice モード既存の LiveKit アプリとセッションの完全な制御

Tools は Mastra Agent に保持され、サーバー上で実行されます。セッションに渡した LiveKit 側の Tools は無視されます。Tool の動作は、toolFeedback(つなぎの発話)、onToolCall(各 Tool 呼び出しの開始時に実行)、onTurnComplete(各応答の後にテキスト、Tool 呼び出し、トークン使用量とともに実行)を通じてワーカーに伝わります。Agent による通話終了は数行で実装できます。onToolCallrunEndCall() を組み合わせてください。

警告

memory オプションを、独自に構築したセッションで LiveKit がデフォルトで有効にする preemptiveGeneration と組み合わせないでください。LiveKit が破棄する前に先行生成ターンが完了すると、ユーザーメッセージと発話されなかった応答がスレッドに永続化されます。turnHandling: { preemptiveGeneration: { enabled: false } } を設定するか、memory を使わずに各ターンで完全な文字起こしを渡してください。

MastraLLM は、同一プロセス内の Mastra agent インスタンス、別のデプロイを必要としないセッション管理、カスタム generate 関数にも対応します。リモート転送は createRemoteAgentReplyGenerator() として単独でも利用でき、createLiveKitWorkergenerate オプションに接続して、機能一式を備えたワーカーをリモートサーバーに対して実行できます。

サーバーから開始するセッション
サーバーから開始するセッションへの直接リンク

dispatchVoiceSession() を使うと、独自のコードから Voice Agent をルームに追加できます。たとえば、既存のルームへの参加や、発信 SIP 通話の実行に使用します。

import { dispatchVoiceSession } from '@mastra/livekit'

await dispatchVoiceSession({
roomName: 'support-call-42',
agentName: 'mastra-voice',
metadata: { agentId: 'support', threadId: 'thread-42', resourceId: 'user-7' },
})

Observability
Observabilityへの直接リンク

Mastra インスタンスに Observability が設定されていると、ワーカーは各通話を Trace します。セッションごとに 1 つの voice call Span を開き、その配下にすべてをネストします。

  • 各ターンの Mastra Agent 実行。モデル生成、Tool 呼び出し、Memory 操作を含み、テキストチャットと同様に記録されます。
  • LiveKit パイプラインの各メトリクスに対する子 Span。Speech-to-Text、Text-to-Speech、発話終了(ターン検出)、音声区間検出、モデルの最初のトークンまでの時間が含まれます。テキストの Trace では確認できないレイテンシと音声の測定値を保持します。
  • モデルごとの使用量集計。通話全体のトークン数、文字数、音声量の合計が、セッション終了時に Span へ書き込まれます。

ワーカーは独立したプロセスなので、サーバーとワーカーの両方からの同時書き込みに対応するバックエンドをストレージに指定してください。SQLite ベースの LibSQL は利用できますが、単一 Writer のストアは利用できません。Traces、Memory、スレッドは 1 つのストアを共有できます。

src/mastra/index.ts
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()],
},
},
}),
})

Tracing はデフォルトで有効です。無効にするには、createLiveKitWorkerobservability: false を渡します。

デプロイ
デプロイへの直接リンク

ワーカーは Mastra サーバーとは別のプロセスなので、mastra build で独立したエントリとして出力する必要があります。bundler.entries に追加してください。

src/mastra/index.ts
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 は、1 つの package.json と依存関係のインストールを共有する 2 つのプロセスを .mastra/output に出力します。

.mastra/output/
index.mjs # Mastra server
voice-worker.mjs # LiveKit worker

このディレクトリを単一のアーティファクトとしてデプロイし、それぞれのコマンドで各プロセスを起動します。

node .mastra/output/index.mjs # server
node .mastra/output/voice-worker.mjs start # worker

ワーカーには、サーバーと同じ環境変数に加えて、LIVEKIT_URLLIVEKIT_API_KEYLIVEKIT_API_SECRET が必要です。

サイズ設定、グレースフルシャットダウン、ホスティングに関する LiveKit のガイドラインはそのまま適用されます。Agent のデプロイを参照してください。ワーカーは LiveKit へ送信方向で接続するため、受信ポートは不要です。

仕組み
仕組みへの直接リンク

LiveKit Voice セッションは、3 つの要素で構成されます。

  1. Mastra サーバーが LiveKit アクセストークンを発行し、Agent をルームへディスパッチします。ディスパッチには、Mastra Agent ID、Memory スレッド、リソースなどのメタデータが含まれます。
  2. LiveKit Agent ワーカー(独立して常時稼働するプロセス)がジョブを受け取り、音声パイプラインを実行します。音声は WebRTC を介してブラウザとワーカー間を流れ、Mastra HTTP サーバーを通過しません。
  3. ユーザーがターンを終えるたびに、ワーカーは新しい入力で Mastra Agent の stream() を呼び出し、ストリーミングされたテキストを発話します。ユーザーが割り込むと、LiveKit はストリームをキャンセルし、Mastra は生成を停止します。

会話履歴は Mastra Memory に保持されるため、Voice セッションとテキストチャットで 1 つのスレッドを共有できます。