@mastra/livekit 0.3.1-alpha.0 → 0.3.1-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -419
- package/dist/plugin-entry.cjs +1 -1
- package/dist/plugin-entry.js +1 -1
- package/dist/{remote-D0Y5P6e4.cjs → remote-BZ7eyB1q.cjs} +7 -1
- package/dist/{remote-D0Y5P6e4.cjs.map → remote-BZ7eyB1q.cjs.map} +1 -1
- package/dist/{remote-C9K3UzKv.js → remote-D7n50m8S.js} +2 -2
- package/dist/{remote-C9K3UzKv.js.map → remote-D7n50m8S.js.map} +1 -1
- package/dist/worker-entry.cjs +3 -1
- package/dist/worker-entry.d.ts +2 -1
- package/dist/worker-entry.d.ts.map +1 -1
- package/dist/worker-entry.js +2 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -3,446 +3,46 @@
|
|
|
3
3
|
Realtime voice for [Mastra](https://mastra.ai) agents and workflows, powered by [LiveKit Agents](https://docs.livekit.io/agents/).
|
|
4
4
|
|
|
5
5
|
LiveKit's agents framework owns the **audio loop** — WebRTC transport, voice activity detection (VAD), streaming speech-to-text (STT), semantic turn detection, barge-in, and text-to-speech (TTS). This package bridges **reply generation** to Mastra, so each detected user turn is answered by a Mastra **agent** (`agent.stream()`) or **workflow** — with your tools, memory, processors, and model routing all running inside Mastra.
|
|
6
|
-
|
|
7
|
-
```
|
|
8
|
-
caller speaks ─▶ VAD ─▶ STT ─▶ turn detection ─▶ [ Mastra agent / workflow ] ─▶ TTS ─▶ caller hears
|
|
9
|
-
(LiveKit owns the audio loop) (this package bridges replies)
|
|
10
|
-
```
|
|
11
|
-
|
|
12
|
-
## What's in the box
|
|
13
|
-
|
|
14
|
-
- **Two reply paths** — answer turns with a Mastra **agent** (the default, richest path) or a Mastra **workflow** (run-to-completion per turn, e.g. deterministic intent routing). A low-level `generate` escape hatch accepts any custom reply generator.
|
|
15
|
-
- **Full speech stack, pluggable** — STT/TTS as LiveKit inference model strings (`'deepgram/nova-3'`, `'cartesia/sonic-3'`) or your own plugin instances; Silero VAD and LiveKit multilingual/English turn detection; barge-in cancels in-flight generation automatically.
|
|
16
|
-
- **Memory, scoped to the call** — `thread` = call, `resource` = caller, so a returning caller is recognized across calls. Up-front thread creation and greeting persistence keep the saved thread a faithful transcript. Works on the agent path and the workflow path (via `memoryInstance`).
|
|
17
|
-
- **Lifecycle hooks** — `toolFeedback` (speak filler while a tool runs), `onTurnComplete` (post-turn, fire-and-forget, off the audio path), and `onCallEnd` (end-of-call, awaited within LiveKit's shutdown window — the place to summarize the finished call with `memory.summarizeThread()`).
|
|
18
|
-
- **Compliance controls, grouped under `configuration`** — AI-disclosure greeting that can't be barged over (with a per-tenant resolver), periodic re-disclosure on long calls (`repeatEvery`), an extensible named consent model (`consentPolicy` declared on the worker, captured at runtime with `createConsentTool`), and agent-initiated hang-up (`endCall` + `createEndCallTool`) that waits for the goodbye to play out before disconnecting.
|
|
19
|
-
- **Observability** — one `voice call` trace per session with LiveKit pipeline metrics and every Mastra run nested under it.
|
|
20
|
-
- **Connection + dispatch helpers** — `liveKitConnectionRoute` mints tokens and dispatches the worker so a frontend can join.
|
|
21
|
-
|
|
22
6
|
## Installation
|
|
23
7
|
|
|
24
8
|
```bash
|
|
25
|
-
npm install @mastra/livekit
|
|
9
|
+
npm install @mastra/livekit
|
|
26
10
|
```
|
|
27
11
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
| Package | Needed for |
|
|
31
|
-
| -------------------------------- | -------------------------------------------- |
|
|
32
|
-
| `@mastra/core` | the Mastra agent/workflow you bridge to |
|
|
33
|
-
| `@livekit/agents` | the audio loop runtime |
|
|
34
|
-
| `@livekit/agents-plugin-silero` | the default `vad: 'silero'` |
|
|
35
|
-
| `@livekit/agents-plugin-livekit` | `turnDetection: 'multilingual' \| 'english'` |
|
|
36
|
-
|
|
37
|
-
The package has three entry points:
|
|
38
|
-
|
|
39
|
-
- `@mastra/livekit` — server-side helpers (`liveKitConnectionRoute`, `dispatchVoiceSession`, `pipeAgentReplyToWriter`) and the agent tool factories (`createConsentTool`, `createEndCallTool` — they go on agents defined in server/shared code). Safe to import from Mastra server code; never loads the `@livekit/agents` runtime.
|
|
40
|
-
- `@mastra/livekit/worker` — the worker runtime (`createLiveKitWorker`, `runLiveKitWorker`). Import it only from the worker entry file.
|
|
41
|
-
- `@mastra/livekit/plugin` — the `MastraLLM` plugin, for customers who own their `voice.AgentSession` and want a Mastra agent in the `llm` slot (a standard LiveKit `llm.LLM`; in-process agent, remote Mastra server, or custom generator). Also loads the `@livekit/agents` runtime — keep it out of Mastra server code.
|
|
42
|
-
|
|
43
|
-
## Quick start
|
|
44
|
-
|
|
45
|
-
A worker is a standalone Node process that connects to LiveKit and answers sessions. Define it with `createLiveKitWorker` and run it with `runLiveKitWorker`:
|
|
46
|
-
|
|
47
|
-
```typescript
|
|
48
|
-
// src/mastra/voice-worker.ts
|
|
49
|
-
import { fileURLToPath } from 'node:url';
|
|
50
|
-
import { createLiveKitWorker, runLiveKitWorker } from '@mastra/livekit/worker';
|
|
51
|
-
import { mastra } from './index';
|
|
52
|
-
|
|
53
|
-
export default createLiveKitWorker({
|
|
54
|
-
mastra,
|
|
55
|
-
agent: 'support', // a Mastra agent key/id (or a resolver, or use `workflow` instead)
|
|
56
|
-
stt: 'deepgram/nova-3',
|
|
57
|
-
tts: 'cartesia/sonic-3',
|
|
58
|
-
turnDetection: 'multilingual',
|
|
59
|
-
configuration: {
|
|
60
|
-
greeting: { text: 'Thanks for calling. How can I help?' },
|
|
61
|
-
},
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
65
|
-
runLiveKitWorker({ entry: import.meta.url, agentName: 'mastra-voice' });
|
|
66
|
-
}
|
|
67
|
-
```
|
|
12
|
+
## Usage
|
|
68
13
|
|
|
69
|
-
Add a connection
|
|
14
|
+
Set `LIVEKIT_URL`, `LIVEKIT_API_KEY`, and `LIVEKIT_API_SECRET`. Add a connection route to your Mastra server so clients can receive a room token and dispatch the configured agent.
|
|
70
15
|
|
|
71
16
|
```typescript
|
|
72
|
-
|
|
17
|
+
import { Agent } from '@mastra/core/agent';
|
|
73
18
|
import { Mastra } from '@mastra/core/mastra';
|
|
74
19
|
import { liveKitConnectionRoute } from '@mastra/livekit';
|
|
75
20
|
|
|
21
|
+
const supportAgent = new Agent({
|
|
22
|
+
id: 'support',
|
|
23
|
+
name: 'Support agent',
|
|
24
|
+
instructions: 'Keep voice responses short and conversational.',
|
|
25
|
+
model: 'openai/gpt-5-mini',
|
|
26
|
+
});
|
|
27
|
+
|
|
76
28
|
export const mastra = new Mastra({
|
|
29
|
+
agents: { supportAgent },
|
|
77
30
|
server: {
|
|
78
31
|
apiRoutes: [liveKitConnectionRoute({ agentName: 'mastra-voice' })],
|
|
79
32
|
},
|
|
80
33
|
});
|
|
81
34
|
```
|
|
82
35
|
|
|
83
|
-
Run the worker
|
|
84
|
-
|
|
85
|
-
```bash
|
|
86
|
-
npx livekit-agents download-files # one-time: turn-detection + VAD model files
|
|
87
|
-
npx tsx src/mastra/voice-worker.ts dev
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
The model strings (`deepgram/nova-3`, `cartesia/sonic-3`) route through **LiveKit Cloud inference**, so with a LiveKit Cloud project you don't need separate Deepgram/Cartesia accounts — only your `LIVEKIT_URL` / `LIVEKIT_API_KEY` / `LIVEKIT_API_SECRET` (plus whatever key your Mastra model needs). To bring your own providers, pass plugin instances to `stt` / `tts` instead of strings.
|
|
91
|
-
|
|
92
|
-
## Reply paths
|
|
93
|
-
|
|
94
|
-
### Agent (default)
|
|
95
|
-
|
|
96
|
-
Pass `agent` (a key/id, an `Agent` instance, or a resolver). The agent runs its full loop each turn — model, tools, memory, processors — and streams its text deltas to TTS. Barge-in cancels the in-flight `agent.stream()`.
|
|
97
|
-
|
|
98
|
-
### Workflow
|
|
99
|
-
|
|
100
|
-
Pass `workflow` + `workflowInput` instead of `agent` (mutually exclusive). LiveKit owns the turn boundary, so the workflow runs **once to completion per turn** — no suspend/resume. Use it for deterministic per-turn structure (e.g. classify intent, then reply).
|
|
101
|
-
|
|
102
|
-
```typescript
|
|
103
|
-
import { createLiveKitWorker, chatContextToMessages } from '@mastra/livekit/worker';
|
|
104
|
-
|
|
105
|
-
export default createLiveKitWorker({
|
|
106
|
-
mastra,
|
|
107
|
-
workflow: 'phoneConversation',
|
|
108
|
-
workflowInput: ({ messages, memory }) => ({ turn: messages, memory: memory || undefined }),
|
|
109
|
-
replyStep: 'generateResponse', // only stream text from this step (optional)
|
|
110
|
-
stt: 'deepgram/nova-3',
|
|
111
|
-
tts: 'cartesia/sonic-3',
|
|
112
|
-
turnDetection: 'multilingual',
|
|
113
|
-
});
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
In the reply-producing step, use **`pipeAgentReplyToWriter`** to forward the agent's reply into the step `writer`. It streams text deltas (so TTS starts early) **and** tool-call chunks (so `toolFeedback` fires and `onTurnComplete` sees the tool list) — unlike piping only `.textStream`, which silently drops tool calls:
|
|
117
|
-
|
|
118
|
-
```typescript
|
|
119
|
-
import { pipeAgentReplyToWriter } from '@mastra/livekit';
|
|
120
|
-
|
|
121
|
-
const generateResponse = createStep({
|
|
122
|
-
id: 'generateResponse',
|
|
123
|
-
execute: async ({ inputData, mastra, writer, abortSignal }) => {
|
|
124
|
-
const stream = await mastra.getAgent('support').stream(inputData.turn, {
|
|
125
|
-
memory: inputData.memory, // engages working memory, recall, etc.
|
|
126
|
-
abortSignal, // lets barge-in stop generation promptly
|
|
127
|
-
});
|
|
128
|
-
const reply = await pipeAgentReplyToWriter(stream, writer);
|
|
129
|
-
return { reply };
|
|
130
|
-
},
|
|
131
|
-
});
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
A step that writes no text stays silent unless you pass `resultText` to derive the reply from the final run result.
|
|
135
|
-
|
|
136
|
-
### Custom (`generate`)
|
|
137
|
-
|
|
138
|
-
For full control, pass a `generate` function — any `VoiceReplyGenerator` that turns a turn into a `ReadableStream<string>` (a remote bridge, a bespoke pipeline, …).
|
|
139
|
-
|
|
140
|
-
## `createLiveKitWorker` options
|
|
141
|
-
|
|
142
|
-
| Option | Type | Notes |
|
|
143
|
-
| --------------------------------------------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
144
|
-
| `mastra` | `Mastra` | **Required.** The instance whose agents/workflows answer sessions. |
|
|
145
|
-
| **Reply generation** (pick one) | | |
|
|
146
|
-
| `agent` | `string \| Agent \| (args) => …` | The agent that answers. Defaults to `metadata.agentId`. |
|
|
147
|
-
| `workflow` | `string \| Workflow \| (args) => string` | Answer with a workflow instead. Requires `workflowInput`. |
|
|
148
|
-
| `workflowInput` | `(ctx & { metadata }) => inputData` | Maps a turn into the workflow's `inputData`. |
|
|
149
|
-
| `replyStep` | `string` | Only stream text from this workflow step id. |
|
|
150
|
-
| `resultText` | `(result) => string` | Fallback reply text when the workflow streams nothing. |
|
|
151
|
-
| `generate` | `VoiceReplyGenerator` | Lowest-level escape hatch. |
|
|
152
|
-
| **Speech stack** | | |
|
|
153
|
-
| `stt` | plugin or `'provider/model'` | Speech-to-text. |
|
|
154
|
-
| `tts` | plugin or `'provider/model'` | Text-to-speech. |
|
|
155
|
-
| `vad` | `VAD \| 'silero' \| false` | Voice activity detection. Defaults to `'silero'`. |
|
|
156
|
-
| `turnDetection` | `'multilingual' \| 'english' \| …` | End-of-turn detection. |
|
|
157
|
-
| `turnHandling` | `AgentSessionOptions['turnHandling']` | Endpointing delays, interruption sensitivity, preemptive generation. |
|
|
158
|
-
| `sessionOptions` / `inputOptions` / `outputOptions` | partial LiveKit options | Merged over what the helper builds. |
|
|
159
|
-
| **Memory** | | |
|
|
160
|
-
| `memory` | `false \| (args) => { thread, resource }` | Memory mapping. Defaults to `{ thread: metadata.threadId ?? room, resource: metadata.resourceId ?? thread }` when the agent has memory. |
|
|
161
|
-
| `memoryInstance` | `Memory \| (args) => Memory` | The `Memory` used to bootstrap the thread + persist the greeting on the **workflow/custom** path (no agent to source it from). Mastra storage is injected if the `Memory` has none. |
|
|
162
|
-
| `configuration` | `{ greeting?, consentPolicy?, endCall?, stt?, tts? }` | Grouped conversation & compliance config (see [Configuration](#configuration)). `greeting` (`text` — a string or a per-tenant resolver — plus `allowInterruptions`, `awaitPlayout`, `persist`, `repeatEvery`/`repeatText` for periodic AI re-disclosure), `consentPolicy` (extensible consent set, e.g. `summaryStorage`, surfaced on `onCallEnd`), `endCall` (agent-initiated hang-up; pair with `createEndCallTool`), and per-call `stt` / `tts` resolvers that pick this call's transcriber and voice (fall back to the top-level options). |
|
|
163
|
-
| **Lifecycle hooks** | | |
|
|
164
|
-
| `toolFeedback` | `(toolCall) => string \| void` | Speak filler while a tool runs (agent + workflow). |
|
|
165
|
-
| `onTurnComplete` | `VoiceTurnCompleteHook` | After each turn streams, **fire-and-forget**, off the audio path. |
|
|
166
|
-
| `onCallEnd` | `VoiceCallEndHook` | When the call ends, **awaited** within LiveKit's shutdown window. |
|
|
167
|
-
| `onSessionStart` | `(args) => …` | After the session starts — attach listeners, trigger replies, etc. |
|
|
168
|
-
| **Other** | | |
|
|
169
|
-
| `observability` | `boolean` | Voice-pipeline tracing. Defaults to `true`. |
|
|
170
|
-
|
|
171
|
-
## Configuration
|
|
172
|
-
|
|
173
|
-
`configuration` groups related conversation & compliance knobs in one place, so they don't each
|
|
174
|
-
become a top-level worker option — and it's where further compliance controls land as they ship.
|
|
175
|
-
|
|
176
|
-
```typescript
|
|
177
|
-
createLiveKitWorker({
|
|
178
|
-
mastra,
|
|
179
|
-
agent: 'support',
|
|
180
|
-
configuration: {
|
|
181
|
-
greeting: {
|
|
182
|
-
// Spoken via TTS at call start (no model round-trip). Doubles as a required AI disclosure.
|
|
183
|
-
text: 'You are speaking with an AI assistant. This call may be recorded. How can I help?',
|
|
184
|
-
allowInterruptions: false, // caller can't barge over the disclosure (EU AI Act Art. 50)
|
|
185
|
-
awaitPlayout: true, // hold post-greeting work until the disclosure finishes
|
|
186
|
-
persist: true, // save it to the memory thread (default true)
|
|
187
|
-
// Periodic re-disclosure on long calls: once this interval elapses, the NEXT turn's reply is
|
|
188
|
-
// prefixed with a short "you're speaking with an AI" reminder (spoken at the turn boundary,
|
|
189
|
-
// never mid-turn). Omit to disable.
|
|
190
|
-
repeatEvery: 3 * 60_000, // ~every 3 minutes (California SB 243 and similar)
|
|
191
|
-
repeatText: 'Quick reminder — you are speaking with an AI assistant.', // optional; has a default
|
|
192
|
-
},
|
|
193
|
-
// Consent requirements — a named, extensible set. Each item is independently required and
|
|
194
|
-
// independently granted, so new items are added without one global "consented" flag.
|
|
195
|
-
consentPolicy: {
|
|
196
|
-
summaryStorage: true, // or { required: true, purpose: 'storing a summary of this call' }
|
|
197
|
-
},
|
|
198
|
-
// Let the agent end the call itself. Pair with a `createEndCallTool` tool on the agent; the
|
|
199
|
-
// worker waits for the agent's closing words to play out, then hangs up (running onCallEnd).
|
|
200
|
-
endCall: {
|
|
201
|
-
message: 'Thanks for calling. Goodbye!', // optional non-interruptible sign-off before hangup
|
|
202
|
-
},
|
|
203
|
-
},
|
|
204
|
-
});
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
**Greeting / AI disclosure.** `text` is spoken at call start; `allowInterruptions: false` makes a
|
|
208
|
-
required disclosure play through; `awaitPlayout: true` waits for it before anything else runs;
|
|
209
|
-
`repeatEvery` re-discloses periodically on long calls. Under the EU AI Act (Art. 50) a person must be
|
|
210
|
-
told they're interacting with an AI at the first interaction.
|
|
211
|
-
|
|
212
|
-
**Per-tenant greeting.** `greeting.text` also takes a resolver — a function called once per call
|
|
213
|
-
(post-connect) with the call context (`metadata`, `requestContext`, `roomName`, `ctx`). Return a
|
|
214
|
-
greeting keyed off the dispatch metadata so one multi-tenant agent opens differently per tenant (the
|
|
215
|
-
disclosure options still apply to whatever it returns; return `undefined` for no greeting):
|
|
216
|
-
|
|
217
|
-
```typescript
|
|
218
|
-
configuration: {
|
|
219
|
-
greeting: {
|
|
220
|
-
text: ({ requestContext }) => {
|
|
221
|
-
const tenant = TENANTS[requestContext?.tenantId as string];
|
|
222
|
-
return `Thanks for calling ${tenant?.name ?? 'us'}. You're speaking with an AI assistant.`;
|
|
223
|
-
},
|
|
224
|
-
allowInterruptions: false, // the disclosure still can't be barged over
|
|
225
|
-
},
|
|
226
|
-
},
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
**Consent.** `consentPolicy` _declares_ which consents the call needs (starting with `summaryStorage`
|
|
230
|
-
— consent to store a summary of the call). Capture the caller's decision at runtime with
|
|
231
|
-
**`createConsentTool`** (exported from `@mastra/livekit`) — add it to your agent, and it reads the
|
|
232
|
-
caller identity from the tool context and hands each decision to your store:
|
|
233
|
-
|
|
234
|
-
```typescript
|
|
235
|
-
import { createConsentTool } from '@mastra/livekit';
|
|
236
|
-
|
|
237
|
-
// in your agent's tools:
|
|
238
|
-
recordConsent: createConsentTool({
|
|
239
|
-
items: ['summaryStorage'],
|
|
240
|
-
onGrant: async ({ item, granted, resourceId }) => {
|
|
241
|
-
if (resourceId) await db.saveConsent(resourceId, item, granted); // your system of record
|
|
242
|
-
},
|
|
243
|
-
}),
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
Then _enforce_ it: the requirements are surfaced on the `onCallEnd` hook (`args.configuration`), so
|
|
247
|
-
you only run the consent-gated action — e.g. the `memory.summarizeThread()` call that stores the
|
|
248
|
-
summary — when it isn't required or the caller granted it. Whether to gate at all is your policy
|
|
249
|
-
call: a permissive deployment skips `consentPolicy` entirely and always summarizes, while a
|
|
250
|
-
regulated one declares → captures → enforces (the runnable example ships both flavors). Further
|
|
251
|
-
controls (recording notice, data retention, human handoff) are planned to land here too.
|
|
252
|
-
|
|
253
|
-
**Agent-initiated hang-up.** `endCall` lets the agent end the call itself — say goodbye, then hang up.
|
|
254
|
-
Enable it under `configuration`, and add a matching tool to the agent with **`createEndCallTool`**
|
|
255
|
-
(both default to the tool name `'endCall'`). The tool only _signals_ intent — from inside
|
|
256
|
-
`agent.stream()` it can't reach the room — so the worker owns the hang-up: on each turn it watches for
|
|
257
|
-
the tool, waits for the agent's closing words to finish playing, holds a short drain (`drainMs`,
|
|
258
|
-
default 800ms) so audio still buffered at the caller isn't clipped, then disconnects, running
|
|
259
|
-
`onCallEnd` on the way out exactly as a caller hang-up does. It works on the
|
|
260
|
-
agent and workflow reply paths.
|
|
261
|
-
|
|
262
|
-
```typescript
|
|
263
|
-
import { createEndCallTool } from '@mastra/livekit';
|
|
264
|
-
|
|
265
|
-
// in your agent's tools:
|
|
266
|
-
endCall: createEndCallTool({
|
|
267
|
-
// optional bookkeeping — the tool reads the caller identity from its context
|
|
268
|
-
onEndCall: ({ reason, resourceId }) => log.info('agent ended call', { reason, resourceId }),
|
|
269
|
-
}),
|
|
270
|
-
```
|
|
271
|
-
|
|
272
|
-
Instruct the agent to say its goodbye and then call `endCall` as its final action. `endCall.message`
|
|
273
|
-
adds a guaranteed non-interruptible sign-off spoken right before hanging up; `endCall.reason` sets the
|
|
274
|
-
shutdown reason in LiveKit logs; `endCall.maxWaitMs` caps how long to wait for the closing words
|
|
275
|
-
(default 30s). This is the AI-oversight companion to a future first-class human `handoff`.
|
|
276
|
-
|
|
277
|
-
**Backwards compatibility.** The previous top-level `greeting` (string) and `persistGreeting` options
|
|
278
|
-
still work — they're deprecated aliases for `configuration.greeting.text` and
|
|
279
|
-
`configuration.greeting.persist`. If both are set, `configuration.greeting` wins field-by-field, so
|
|
280
|
-
existing worker configs keep running unchanged while you migrate.
|
|
281
|
-
|
|
282
|
-
## Lifecycle hooks
|
|
283
|
-
|
|
284
|
-
Three hooks let you do work around a turn without adding to the caller's latency:
|
|
285
|
-
|
|
286
|
-
```typescript
|
|
287
|
-
createLiveKitWorker({
|
|
288
|
-
mastra,
|
|
289
|
-
agent: 'support',
|
|
290
|
-
|
|
291
|
-
// 1. In-turn: speak a short phrase while a tool runs, so the caller isn't left in silence.
|
|
292
|
-
toolFeedback: ({ toolName }) => (toolName === 'lookupOrder' ? 'Let me pull that up.' : undefined),
|
|
293
|
-
|
|
294
|
-
// 2. Post-turn: fire-and-forget AFTER the reply has streamed — the worker never awaits it, so it
|
|
295
|
-
// can't delay the caller or the next turn. Carries the produced reply + the memory mapping.
|
|
296
|
-
onTurnComplete: async ({ result, memory }) => {
|
|
297
|
-
if (memory) await crm.logContact(memory.resource, result.text); // result.text/toolCalls/interrupted
|
|
298
|
-
},
|
|
299
|
-
|
|
300
|
-
// 3. End-of-call: runs when the caller hangs up, AWAITED within LiveKit's shutdown grace window
|
|
301
|
-
// (so it finishes before the process exits). The place for end-of-call work — e.g. summarize
|
|
302
|
-
// the finished call into your own records. `memory.summarizeThread()` (@mastra/memory) runs a
|
|
303
|
-
// one-shot summarization + structured extraction over the whole call, outside observational
|
|
304
|
-
// memory's lifecycle — nothing is written back to memory; you decide where the result goes
|
|
305
|
-
// (return value and/or each Extractor's `onExtracted` hook).
|
|
306
|
-
onCallEnd: async ({ memory }) => {
|
|
307
|
-
if (!memory) return;
|
|
308
|
-
await myMemory.summarizeThread({
|
|
309
|
-
// your app's `Memory` (from @mastra/memory)
|
|
310
|
-
model: 'openai/gpt-4.1-mini',
|
|
311
|
-
threadId: memory.thread,
|
|
312
|
-
resourceId: memory.resource,
|
|
313
|
-
instructions: 'Summarize this call for the business owner.',
|
|
314
|
-
});
|
|
315
|
-
},
|
|
316
|
-
});
|
|
317
|
-
```
|
|
318
|
-
|
|
319
|
-
`onTurnComplete` and `toolFeedback` work on the **workflow** path too (the reply step must surface tool calls via `pipeAgentReplyToWriter`).
|
|
320
|
-
|
|
321
|
-
## Joining a call: `liveKitConnectionRoute`
|
|
322
|
-
|
|
323
|
-
Mounts an API route on your Mastra server that mints a LiveKit token and dispatches the worker by `agentName`. Frontends `POST` to it to get connection details.
|
|
324
|
-
|
|
325
|
-
| Option | Default | Notes |
|
|
326
|
-
| ------------------------------------ | -------------------------------------------------------- | --------------------------------------------------- |
|
|
327
|
-
| `path` | `/voice/livekit/connection-details` | Must not start with `/api`. |
|
|
328
|
-
| `serverUrl` / `apiKey` / `apiSecret` | `LIVEKIT_URL` / `LIVEKIT_API_KEY` / `LIVEKIT_API_SECRET` | LiveKit credentials. |
|
|
329
|
-
| `agentName` | — | Must match the worker's `agentName`. |
|
|
330
|
-
| `ttl` | `'15m'` | Token lifetime. |
|
|
331
|
-
| `requiresAuth` | `true` | Mastra custom routes require auth unless opted out. |
|
|
332
|
-
| `roomName` / `participantIdentity` | generated | String or `(args) => string`. |
|
|
333
|
-
| `metadata` | passes `agentId`/`threadId`/`resourceId` | Session metadata delivered to the worker. |
|
|
334
|
-
|
|
335
|
-
For programmatic dispatch (no HTTP), use `dispatchVoiceSession`.
|
|
336
|
-
|
|
337
|
-
## Running the worker: `runLiveKitWorker`
|
|
338
|
-
|
|
339
|
-
Starts the LiveKit agent worker CLI (`dev` / `start` / `connect`) for your entry file.
|
|
340
|
-
|
|
341
|
-
| Option | Default | Notes |
|
|
342
|
-
| --------------- | ---------------- | --------------------------------------------------- |
|
|
343
|
-
| `entry` | — | The worker module; pass `import.meta.url`. |
|
|
344
|
-
| `agentName` | `'mastra-voice'` | Dispatch name; must match `liveKitConnectionRoute`. |
|
|
345
|
-
| `serverOptions` | — | Extra LiveKit `ServerOptions`. |
|
|
346
|
-
|
|
347
|
-
## Observability
|
|
348
|
-
|
|
349
|
-
When the Mastra instance has observability configured, the worker opens one `voice call` span per session, nests every turn's Mastra run under it, and adds child spans for LiveKit pipeline metrics — STT, TTS, end-of-utterance, VAD, and LLM time-to-first-token — closing with a per-model token/character/audio usage roll-up. On by default; pass `observability: false` to disable.
|
|
350
|
-
|
|
351
|
-
## Deployment
|
|
36
|
+
Run a separate LiveKit worker to own the audio pipeline and call the agent for each detected turn. See the quickstart for the worker setup and required LiveKit plugins.
|
|
352
37
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
| Component | What runs it | Notes |
|
|
356
|
-
| ------------------------ | ------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
|
|
357
|
-
| **LiveKit media server** | LiveKit Cloud, or self-hosted `livekit-server` + Redis | WebRTC transport. Both processes below need its `LIVEKIT_URL` / `LIVEKIT_API_KEY` / `LIVEKIT_API_SECRET`. |
|
|
358
|
-
| **Mastra HTTP server** | `mastra build` → `node .mastra/output/index.mjs` | Your agents/workflows + `liveKitConnectionRoute` (mints tokens, dispatches the worker). |
|
|
359
|
-
| **LiveKit worker** | your worker entry → `runLiveKitWorker` (LiveKit Agents CLI `start`) | Connects **outbound** to the media server and answers calls. Not an HTTP server. |
|
|
360
|
-
|
|
361
|
-
The worker is a **separate process**: `mastra build` bundles only your `Mastra` instance and `src/mastra/tools/**` — never the worker entry, because nothing imports it. `liveKitConnectionRoute`, by contrast, is an `apiRoute`, so it ships inside the server build automatically. The server and worker therefore build and run independently and can live on different hosts, as long as they share a LiveKit project and the same `agentName`.
|
|
362
|
-
|
|
363
|
-
> Note: `mastra worker build` is unrelated — it bundles Mastra's own pubsub/scheduler workflow workers (`mastra.startWorkers()`), not this LiveKit worker.
|
|
364
|
-
|
|
365
|
-
### Building the worker
|
|
366
|
-
|
|
367
|
-
The worker imports `@livekit/agents` (plus the optional plugins) and your `Mastra` instance, then runs the LiveKit Agents CLI. Two build-time essentials:
|
|
368
|
-
|
|
369
|
-
- **Run `start`, not `dev`, in production** — `dev` is hot-reload only.
|
|
370
|
-
- **Pre-download the model files** so they're baked into the image instead of fetched on cold start:
|
|
371
|
-
|
|
372
|
-
```bash
|
|
373
|
-
node --import tsx src/mastra/voice-worker.ts download-files # Silero VAD + turn-detector ONNX
|
|
374
|
-
```
|
|
375
|
-
|
|
376
|
-
Running the worker with `tsx` against source avoids bundling LiveKit's native deps (onnxruntime, …). If you do, make `tsx` and the `@livekit/agents*` packages **real** dependencies (not devDependencies) in the deployed image.
|
|
377
|
-
|
|
378
|
-
### Docker
|
|
379
|
-
|
|
380
|
-
Build the server with `mastra build`, bake the model files, then run the two processes. **Recommended: one image, two services** — workers scale by call volume and the HTTP server scales by request volume, so keep them independent:
|
|
381
|
-
|
|
382
|
-
```dockerfile
|
|
383
|
-
FROM node:22-slim AS build
|
|
384
|
-
WORKDIR /app
|
|
385
|
-
COPY package.json package-lock.json ./
|
|
386
|
-
RUN npm ci
|
|
387
|
-
COPY . .
|
|
388
|
-
RUN npx mastra build
|
|
389
|
-
RUN node --import tsx src/mastra/voice-worker.ts download-files
|
|
390
|
-
|
|
391
|
-
FROM node:22-slim
|
|
392
|
-
WORKDIR /app
|
|
393
|
-
COPY --from=build /app /app
|
|
394
|
-
ENV NODE_ENV=production
|
|
395
|
-
EXPOSE 4111
|
|
396
|
-
# server service: CMD ["node", ".mastra/output/index.mjs"]
|
|
397
|
-
# worker service: CMD ["node", "--import", "tsx", "src/mastra/voice-worker.ts", "start"]
|
|
398
|
-
```
|
|
399
|
-
|
|
400
|
-
To run **both in one container** (single-tenant boxes, demos), supervise them with `bash` so the container exits — and is restarted by the orchestrator — if either dies:
|
|
401
|
-
|
|
402
|
-
```bash
|
|
403
|
-
#!/usr/bin/env bash
|
|
404
|
-
set -euo pipefail
|
|
405
|
-
node .mastra/output/index.mjs &
|
|
406
|
-
node --import tsx src/mastra/voice-worker.ts start &
|
|
407
|
-
wait -n
|
|
408
|
-
exit 1
|
|
409
|
-
```
|
|
410
|
-
|
|
411
|
-
This is simpler, but it couples two processes that have opposite scaling curves and no independent autoscaling — prefer the split for anything beyond a demo.
|
|
412
|
-
|
|
413
|
-
### Managed platforms (Mastra Cloud, Railway, Cloud Run, …)
|
|
414
|
-
|
|
415
|
-
Single-process HTTP hosts run the **Mastra server** as-is (`node .mastra/output/index.mjs`). They can't host the worker — it isn't an HTTP server, it isn't in the build output, and it's a long-lived outbound connection. Deploy the **hybrid**: the server on the managed platform (it still mints tokens and dispatches via the bundled connection route), and the worker on any plain process host (a dedicated Railway/Fly/Render service, a VM, a Kubernetes `Deployment`, ECS, …). Point both at the same LiveKit project, keep ≥1 always-on worker instance (no scale-to-zero, so it stays registered), and inject the shared `LIVEKIT_*` env into both.
|
|
416
|
-
|
|
417
|
-
## Runnable example
|
|
418
|
-
|
|
419
|
-
A complete, runnable reference lives in the Mastra monorepo at **[`examples/voice-agent`](https://github.com/mastra-ai/mastra/tree/main/examples/voice-agent)** — a trades-contractor front-desk voice agent that exercises nearly every feature here:
|
|
420
|
-
|
|
421
|
-
- **Three workers, one agent name** (run one at a time): the default **agent** worker (`pnpm worker`) and **workflow** worker (`pnpm worker:workflow`, deterministic intent routing → memory-backed reply) are deliberately **permissive** — no consent friction, the end-of-call summary always runs. The **regulated** worker (`pnpm worker:regulated`, a "Northwind Financial" line) demonstrates every compliance control at once: non-interruptible AI disclosure, 45-second re-disclosure, a four-item runtime consent sweep with an audit ledger, agent-initiated hang-up with a compliance sign-off, and a consent-gated summary (no consent → no stored summary).
|
|
422
|
-
- **End-of-call summarization** — every finished call is distilled into a structured record (summary, sentiment, requested services) via `memory.summarizeThread()` + an `Extractor` whose `onExtracted` hook writes to the app's own store, from the `onCallEnd` hook.
|
|
423
|
-
- **Three memory layers** — working memory, semantic recall, and observational memory, all scoped to the caller.
|
|
424
|
-
- **Tools + deterministic reconciliation**, a tenant-context input processor, the `toolFeedback` / `onTurnComplete` / `onCallEnd` hooks, and full observability.
|
|
425
|
-
|
|
426
|
-
To run it:
|
|
427
|
-
|
|
428
|
-
```bash
|
|
429
|
-
git clone https://github.com/mastra-ai/mastra
|
|
430
|
-
cd mastra && pnpm install && pnpm build:packages # build the workspace packages
|
|
38
|
+
## Documentation
|
|
431
39
|
|
|
432
|
-
|
|
433
|
-
cp .env.example .env # add LiveKit Cloud creds + your model key (e.g. OPENAI_API_KEY)
|
|
434
|
-
pnpm install
|
|
435
|
-
pnpm worker:download-files # one-time model download
|
|
40
|
+
- [@mastra/livekit documentation](https://mastra.ai/integrations/voice/livekit)
|
|
436
41
|
|
|
437
|
-
|
|
438
|
-
pnpm dev # Mastra server + Studio at http://localhost:4111
|
|
439
|
-
pnpm worker # the voice worker (or `pnpm worker:workflow` / `pnpm worker:regulated`)
|
|
440
|
-
```
|
|
42
|
+
## Changelog
|
|
441
43
|
|
|
442
|
-
See the
|
|
44
|
+
See the [package changelog](https://github.com/mastra-ai/mastra/blob/main/integrations/livekit/CHANGELOG.md) for version history and release notes.
|
|
443
45
|
|
|
444
|
-
##
|
|
46
|
+
## Support
|
|
445
47
|
|
|
446
|
-
|
|
447
|
-
- [`@mastra/livekit` reference](https://mastra.ai/reference/voice/livekit)
|
|
448
|
-
- [LiveKit Agents docs](https://docs.livekit.io/agents/)
|
|
48
|
+
We have an [open community Discord](https://discord.gg/mastra-ai). Come and say hello and let us know if you have any questions or need any help getting things running.
|
package/dist/plugin-entry.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_remote = require("./remote-
|
|
2
|
+
const require_remote = require("./remote-BZ7eyB1q.cjs");
|
|
3
3
|
let _livekit_agents = require("@livekit/agents");
|
|
4
4
|
let _mastra_core_request_context = require("@mastra/core/request-context");
|
|
5
5
|
//#region src/llm-plugin.ts
|
package/dist/plugin-entry.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as chatContextToMessages, o as extractNewTurnMessages, r as createAgentReplyGenerator, t as createRemoteAgentReplyGenerator } from "./remote-D7n50m8S.js";
|
|
2
2
|
import { DEFAULT_API_CONNECT_OPTIONS, llm } from "@livekit/agents";
|
|
3
3
|
import { RequestContext } from "@mastra/core/request-context";
|
|
4
4
|
//#region src/llm-plugin.ts
|
|
@@ -624,6 +624,12 @@ function createRemoteAgentReplyGenerator(options) {
|
|
|
624
624
|
};
|
|
625
625
|
}
|
|
626
626
|
//#endregion
|
|
627
|
+
Object.defineProperty(exports, "MastraVoiceAgent", {
|
|
628
|
+
enumerable: true,
|
|
629
|
+
get: function() {
|
|
630
|
+
return MastraVoiceAgent;
|
|
631
|
+
}
|
|
632
|
+
});
|
|
627
633
|
Object.defineProperty(exports, "chatContextToMessages", {
|
|
628
634
|
enumerable: true,
|
|
629
635
|
get: function() {
|
|
@@ -655,4 +661,4 @@ Object.defineProperty(exports, "extractNewTurnMessages", {
|
|
|
655
661
|
}
|
|
656
662
|
});
|
|
657
663
|
|
|
658
|
-
//# sourceMappingURL=remote-
|
|
664
|
+
//# sourceMappingURL=remote-BZ7eyB1q.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"remote-D0Y5P6e4.cjs","names":["ReadableStream","RequestContext","llm","voice","RequestContext","ReadableStream","APIStatusError","APIConnectionError","APITimeoutError","APIError"],"sources":["../src/messages.ts","../src/bridge.ts","../src/remote.ts"],"sourcesContent":["import type { llm } from '@livekit/agents';\n\n/**\n * Fixed id LiveKit gives the customer Agent's instructions when it injects them as a leading\n * `role: 'system'` message into the chat context passed to `chat()` / `llmNode`. We drop this\n * item so the server-side Mastra agent's own system prompt is authoritative.\n */\nexport const LIVEKIT_INSTRUCTIONS_MESSAGE_ID = 'lk.agent_task.instructions';\n\n/**\n * A message bound for `agent.stream(...)` (in-process) or the Mastra server stream route (remote).\n * `id` carries the LiveKit `ChatMessage.id` so the server can dedupe/upsert by id — making\n * base-class retries, preemptive double-sends, and the interrupted-turn reconciliation recipe idempotent.\n */\nexport type VoiceTurnMessage =\n | { role: 'system'; content: string; id?: string }\n | { role: 'user'; content: string; id?: string }\n | { role: 'assistant'; content: string; id?: string };\n\nfunction textOfMessage(message: llm.ChatMessage): string {\n const parts: string[] = [];\n for (const part of message.content) {\n if (typeof part === 'string') {\n parts.push(part);\n } else if (part.type === 'instructions') {\n parts.push(part.value);\n } else if (part.type === 'audio_content' && part.transcript) {\n parts.push(part.transcript);\n }\n }\n return parts.join('\\n').trim();\n}\n\nfunction toVoiceTurnMessage(item: llm.ChatItem): VoiceTurnMessage | undefined {\n if (item.type !== 'message') return undefined;\n const content = textOfMessage(item);\n if (!content) return undefined;\n const id = item.id;\n if (item.role === 'user') return { role: 'user', content, id };\n if (item.role === 'assistant') return { role: 'assistant', content, id };\n // 'system' and 'developer' both map to a Mastra system message.\n return { role: 'system', content, id };\n}\n\n/**\n * Extracts only the messages added since the agent last spoke. Used when Mastra Memory is\n * the source of truth for conversation history: prior turns are already persisted in the\n * thread, so re-sending them would duplicate history.\n *\n * Two extensions over the naive \"slice after the last assistant message\":\n *\n * - **Interrupted-turn self-heal:** when the last assistant message was cut off by barge-in\n * (`interrupted: true`), the server never persisted it — aborted runs skip persistence — so\n * its heard-only text is missing from the thread. Re-send that fragment (ordered first) this\n * turn to backfill it. It stops being \"the last assistant message\" once a full reply lands,\n * so each interrupted fragment is sent exactly once, on the following turn.\n * - **Instructions filter:** LiveKit injects the customer Agent's `instructions` as a\n * leading `system` message ({@link LIVEKIT_INSTRUCTIONS_MESSAGE_ID}); the server-side Mastra\n * agent owns its own system prompt, so drop it (it would otherwise ship on the first turn,\n * before any assistant message).\n */\nexport function extractNewTurnMessages(chatCtx: llm.ChatContext): VoiceTurnMessage[] {\n const items = chatCtx.items;\n let lastAssistantIdx = -1;\n for (let i = items.length - 1; i >= 0; i--) {\n const item = items[i];\n if (item?.type === 'message' && item.role === 'assistant') {\n lastAssistantIdx = i;\n break;\n }\n }\n const lastAssistant = lastAssistantIdx >= 0 ? items[lastAssistantIdx] : undefined;\n const healInterrupted =\n lastAssistant?.type === 'message' && lastAssistant.role === 'assistant' && lastAssistant.interrupted;\n // Include the interrupted fragment by starting the slice AT it, otherwise start strictly after.\n const startIdx = healInterrupted ? lastAssistantIdx : lastAssistantIdx + 1;\n\n const messages: VoiceTurnMessage[] = [];\n for (const item of items.slice(startIdx)) {\n if (item.type === 'message' && item.id === LIVEKIT_INSTRUCTIONS_MESSAGE_ID) continue;\n const message = toVoiceTurnMessage(item);\n if (message) messages.push(message);\n }\n return messages;\n}\n\n/**\n * Converts the full LiveKit chat context to Mastra messages. Used when the bridge runs\n * without Mastra Memory and LiveKit's in-session context is the only history. The agent's\n * LiveKit-level instructions are excluded — the Mastra agent applies its own instructions.\n */\nexport function chatContextToMessages(chatCtx: llm.ChatContext): VoiceTurnMessage[] {\n const withoutInstructions = chatCtx.copy({ excludeInstructions: true, excludeFunctionCall: true });\n const messages: VoiceTurnMessage[] = [];\n for (const item of withoutInstructions.items) {\n const message = toVoiceTurnMessage(item);\n if (message) messages.push(message);\n }\n return messages;\n}\n","import { ReadableStream } from 'node:stream/web';\nimport { llm, voice } from '@livekit/agents';\nimport type { Agent as MastraAgent, AgentExecutionOptionsBase } from '@mastra/core/agent';\nimport type { TracingContext } from '@mastra/core/observability';\nimport { RequestContext } from '@mastra/core/request-context';\nimport { chatContextToMessages, extractNewTurnMessages } from './messages';\nimport type { VoiceTurnMessage } from './messages';\n\nconst DEFAULT_INSTRUCTIONS = 'You are a helpful voice assistant powered by a Mastra agent.';\n\n/** Default spoken text for periodic AI re-disclosure. See {@link MastraVoiceAgentOptions.greetingReminder}. */\nexport const DEFAULT_DISCLOSURE_REMINDER = \"Just a reminder, you're speaking with an AI assistant.\";\n\n/**\n * Tracks periodic AI re-disclosure for a single call. `due()` returns the reminder text once\n * `everyMs` has elapsed since the last disclosure (resetting the clock), otherwise `undefined`.\n * Time is injectable so the interval logic is deterministically testable.\n */\nexport class DisclosureReminder {\n private lastAt: number;\n constructor(\n private readonly everyMs: number,\n private readonly text: string,\n now: number = Date.now(),\n ) {\n this.lastAt = now;\n }\n /** Call once per turn: the reminder text if it's due, else `undefined`. Does not reset the clock —\n * call {@link DisclosureReminder.markDelivered} once the reminder is actually threaded into the\n * outgoing reply, so a reminder that never makes it out isn't silently skipped for a full interval. */\n due(now: number = Date.now()): string | undefined {\n if (now - this.lastAt < this.everyMs) return undefined;\n return this.text;\n }\n /** Resets the clock. Call only once the reminder text from {@link due} was actually emitted. */\n markDelivered(now: number = Date.now()): void {\n this.lastAt = now;\n }\n}\n\n/**\n * Wraps `source` in a stream that emits `text` as a single leading chunk (with a trailing space, so\n * TTS pauses before the reply) before piping the rest of `source` through unchanged. Cancelling the\n * wrapper cancels `source` — so barge-in still aborts the underlying generation.\n */\nexport function prependText(source: ReadableStream<string>, text: string): ReadableStream<string> {\n const prefix = text.endsWith(' ') ? text : `${text} `;\n const reader = source.getReader();\n return new ReadableStream<string>({\n start(controller) {\n controller.enqueue(prefix);\n },\n async pull(controller) {\n try {\n const { done, value } = await reader.read();\n if (done) controller.close();\n else controller.enqueue(value);\n } catch (error) {\n controller.error(error);\n }\n },\n cancel(reason) {\n return reader.cancel(reason);\n },\n });\n}\n\nexport type MastraStreamOptions = Partial<AgentExecutionOptionsBase<unknown>>;\n\nexport interface VoiceToolCall {\n toolCallId: string;\n toolName: string;\n args?: unknown;\n}\n\n/**\n * Token usage for one turn, captured from the model's `finish` chunk. Field names mirror LiveKit's\n * `CompletionUsage` so the plugin can forward it into `metrics_collected` without remapping.\n */\nexport interface VoiceTurnUsage {\n /** Tokens in the prompt (LiveKit `promptTokens`). */\n promptTokens: number;\n /** Tokens in the completion (LiveKit `completionTokens`). */\n completionTokens: number;\n /** Cached prompt tokens (LiveKit `promptCachedTokens`). */\n promptCachedTokens: number;\n /** Total tokens for the turn. */\n totalTokens: number;\n}\n\n/**\n * Maps a Mastra `finish` chunk's usage (`payload.output.usage`, AI-SDK `LanguageModelUsage`) to the\n * LiveKit-shaped {@link VoiceTurnUsage}, or `undefined` when the chunk carries no token counts.\n * Handles both the flat V2 usage shape (`inputTokens`/`outputTokens`) and the nested V3 shape\n * (`inputTokens.total`/`outputTokens.total`).\n */\nexport function mapTurnUsage(usage: unknown): VoiceTurnUsage | undefined {\n if (!usage || typeof usage !== 'object') return undefined;\n const u = usage as Record<string, unknown>;\n // Prefer the flat V2 shape (`inputTokens: number`); fall back to the nested V3 shape\n // (`inputTokens: { total, cacheRead }`).\n const totalOf = (v: unknown): number | undefined => {\n if (typeof v === 'number') return v;\n if (v && typeof v === 'object' && typeof (v as { total?: unknown }).total === 'number') {\n return (v as { total: number }).total;\n }\n return undefined;\n };\n const cacheReadOf = (v: unknown): number | undefined =>\n v && typeof v === 'object' && typeof (v as { cacheRead?: unknown }).cacheRead === 'number'\n ? (v as { cacheRead: number }).cacheRead\n : undefined;\n\n const promptTokens = totalOf(u.inputTokens) ?? 0;\n const completionTokens = totalOf(u.outputTokens) ?? 0;\n const promptCachedTokens =\n (typeof u.cachedInputTokens === 'number' ? u.cachedInputTokens : cacheReadOf(u.inputTokens)) ?? 0;\n const totalTokens = typeof u.totalTokens === 'number' ? u.totalTokens : promptTokens + completionTokens;\n // Nothing was reported at all → treat as no usage rather than emitting an all-zero chunk.\n if (promptTokens === 0 && completionTokens === 0 && totalTokens === 0 && promptCachedTokens === 0) {\n return undefined;\n }\n return { promptTokens, completionTokens, promptCachedTokens, totalTokens };\n}\n\nexport interface MastraVoiceAgentMemory {\n thread: string;\n resource?: string;\n}\n\n/**\n * Per-turn context handed to a {@link VoiceReplyGenerator}. LiveKit calls `llmNode` once per\n * detected user turn; the bridge builds this context and asks the generator for the reply.\n */\nexport interface VoiceTurnContext {\n /**\n * The messages to generate a reply from. With Mastra Memory on, only the messages new since\n * the agent last spoke (history comes from the thread); with memory off, the full session.\n *\n * For a workflow / custom generator: pass these straight to a memory-backed `agent.stream(...,\n * { memory })` inside a step so the agent backfills history from the thread (no duplication). A\n * stateless workflow that wants the entire transcript every turn should read `chatCtx` instead\n * (e.g. `chatContextToMessages(chatCtx)`), since there is no thread to backfill from.\n */\n messages: VoiceTurnMessage[];\n /** The raw LiveKit chat context, for generators that want the full transcript or message parts. */\n chatCtx: llm.ChatContext;\n /** Resolved memory mapping for the call, or `false` when memory is disabled. */\n memory: MastraVoiceAgentMemory | false;\n /** Request context forwarded to generation. */\n requestContext?: RequestContext;\n /** Voice-call span context, so each turn's generation nests under the call trace. */\n tracingContext?: TracingContext;\n /**\n * Internal, per-turn side channel for token usage. A generator invokes this once, when the\n * `finish` chunk carries usage, so the caller (e.g. `MastraLLMStream`) can attribute usage to\n * exactly this turn — kept on the context (not on generator options) so overlapping turns from\n * preemptive generation can't misattribute usage. Fire-and-forget; the generator does not await it.\n */\n onUsage?: (usage: VoiceTurnUsage) => void;\n}\n\n/**\n * What a turn produced, handed to {@link VoiceTurnCompleteHook} after the reply finishes.\n */\nexport interface VoiceTurnResult {\n /** The assistant reply text streamed this turn, accumulated from the model's text deltas. */\n text: string;\n /** Tool calls the agent made during the turn, in order. */\n toolCalls: VoiceToolCall[];\n /** True when barge-in cut the turn short before it finished streaming. */\n interrupted: boolean;\n /** Token usage for the turn when the model reported it in its `finish` chunk. */\n usage?: VoiceTurnUsage;\n}\n\n/** {@link VoiceTurnContext} plus the reply it produced. Passed to {@link VoiceTurnCompleteHook}. */\nexport interface VoiceTurnCompleteContext extends VoiceTurnContext {\n /** The reply the agent produced this turn. */\n result: VoiceTurnResult;\n}\n\n/**\n * Called once per turn AFTER the reply has finished streaming to text-to-speech — off the audio\n * path. It runs fire-and-forget: the turn does not await it, so post-turn work (memory\n * maintenance, CRM writes, analytics) never delays what the caller hears or the next turn. A\n * thrown error or rejected promise is logged, not propagated. Because the resolved `memory`\n * mapping (`thread`/`resource`) is on the context, this is the place for a truly non-blocking\n * `memory.updateWorkingMemory(...)`. See {@link MastraVoiceAgentOptions.onTurnComplete}.\n */\nexport type VoiceTurnCompleteHook = (ctx: VoiceTurnCompleteContext) => void | Promise<void>;\n\n/**\n * Produces a stream of text deltas for one conversational turn, or `null` to stay silent.\n * Cancelling the returned stream (LiveKit does this on barge-in) must abort the underlying\n * generation. Built-in implementations: {@link createAgentReplyGenerator} (a Mastra agent) and\n * `createWorkflowReplyGenerator` (a Mastra workflow).\n */\nexport type VoiceReplyGenerator = (\n ctx: VoiceTurnContext,\n) => ReadableStream<string> | null | Promise<ReadableStream<string> | null>;\n\nexport interface AgentReplyGeneratorOptions {\n /** The Mastra agent that generates replies. Tools and memory run inside this agent. */\n agent: MastraAgent;\n /** Extra options merged into every `agent.stream()` call (e.g. `tracingContext`). */\n streamOptions?: MastraStreamOptions;\n /** Speak a short phrase while a tool call runs. See {@link MastraVoiceAgentOptions.toolFeedback}. */\n toolFeedback?: (toolCall: VoiceToolCall) => string | undefined | void;\n /** Notified as each tool-call chunk arrives, mid-stream. See {@link MastraVoiceAgentOptions.onToolCall}. */\n onToolCall?: (toolCall: VoiceToolCall) => void;\n /** Fired off the audio path after the reply streams. See {@link MastraVoiceAgentOptions.onTurnComplete}. */\n onTurnComplete?: VoiceTurnCompleteHook;\n}\n\n/**\n * A {@link VoiceReplyGenerator} backed by a Mastra agent: runs the agent's full loop (model,\n * tools, memory) and streams its text deltas. On barge-in the returned stream is cancelled,\n * which aborts the in-flight `agent.stream()`.\n */\nexport function createAgentReplyGenerator(options: AgentReplyGeneratorOptions): VoiceReplyGenerator {\n const { agent, streamOptions, toolFeedback, onToolCall, onTurnComplete } = options;\n return ctx => {\n if (ctx.messages.length === 0) return null;\n\n const abortController = new AbortController();\n const mergedOptions: MastraStreamOptions = {\n ...streamOptions,\n abortSignal: abortController.signal,\n };\n if (ctx.memory) mergedOptions.memory = ctx.memory;\n if (ctx.requestContext) mergedOptions.requestContext = ctx.requestContext;\n\n let cancelled = false;\n // Accumulated as the turn streams so the post-turn hook can see what was actually produced.\n let replyText = '';\n const toolCalls: VoiceToolCall[] = [];\n let usage: VoiceTurnUsage | undefined;\n\n // Fire-and-forget after the reply has streamed: off the audio path (the caller already heard\n // the text), and not awaited, so it never delays the next turn. Errors are logged, not thrown.\n const emitTurnComplete = (interrupted: boolean) => {\n if (!onTurnComplete) return;\n const completeCtx: VoiceTurnCompleteContext = {\n ...ctx,\n result: { text: replyText, toolCalls, interrupted, usage },\n };\n Promise.resolve()\n .then(() => onTurnComplete(completeCtx))\n .catch(error => {\n console.warn('@mastra/livekit: onTurnComplete hook threw', error);\n });\n };\n\n return new ReadableStream<string>({\n start: async controller => {\n try {\n const result = await agent.stream(ctx.messages, mergedOptions);\n for await (const chunk of result.fullStream) {\n if (cancelled) break;\n if (chunk.type === 'text-delta') {\n if (chunk.payload.text) {\n replyText += chunk.payload.text;\n controller.enqueue(chunk.payload.text);\n }\n } else if (chunk.type === 'tool-call') {\n const toolCall: VoiceToolCall = {\n toolCallId: chunk.payload.toolCallId,\n toolName: chunk.payload.toolName,\n args: chunk.payload.args,\n };\n toolCalls.push(toolCall);\n // Observer hooks are customer code: a throw must not tear down an otherwise healthy\n // reply stream (same isolation as onTurnComplete).\n try {\n onToolCall?.(toolCall);\n } catch (error) {\n console.warn('@mastra/livekit: onToolCall hook threw', error);\n }\n if (toolFeedback) {\n let filler: string | undefined | void;\n try {\n filler = toolFeedback(toolCall);\n } catch (error) {\n console.warn('@mastra/livekit: toolFeedback hook threw', error);\n }\n if (filler) controller.enqueue(filler.endsWith(' ') ? filler : `${filler} `);\n }\n } else if (chunk.type === 'finish') {\n // Usage is dropped from the spoken stream but surfaced via the per-turn side channel\n // and on the turn result, so the plugin and onTurnComplete consumers can read it.\n const output = (chunk.payload as { output?: { usage?: unknown } }).output;\n const turnUsage = mapTurnUsage(output?.usage);\n if (turnUsage) {\n usage = turnUsage;\n try {\n ctx.onUsage?.(turnUsage);\n } catch (error) {\n console.warn('@mastra/livekit: onUsage hook threw', error);\n }\n }\n } else if (chunk.type === 'error') {\n const error = chunk.payload.error;\n throw error instanceof Error ? error : new Error(String(error));\n }\n }\n if (!cancelled) controller.close();\n // Success, or a clean barge-in break out of the loop: the turn is done either way.\n emitTurnComplete(cancelled);\n } catch (error) {\n // Barge-in cancels the stream and aborts generation; that's not a failure — the turn\n // still completed (interrupted), so the hook still fires for memory reconciliation.\n if (cancelled || abortController.signal.aborted) {\n emitTurnComplete(true);\n return;\n }\n controller.error(error);\n }\n },\n cancel: () => {\n cancelled = true;\n abortController.abort();\n },\n });\n };\n}\n\nexport interface MastraVoiceAgentOptions {\n /**\n * The Mastra agent that generates replies. Tools and memory run inside this agent. Provide\n * either this or {@link MastraVoiceAgentOptions.generate}.\n */\n agent?: MastraAgent;\n /**\n * A lower-level reply generator (e.g. from `createWorkflowReplyGenerator`). Use instead of\n * `agent` to drive replies with a workflow or any custom generator.\n */\n generate?: VoiceReplyGenerator;\n /**\n * Conversation persistence. When set, only messages new since the agent last spoke are\n * sent each turn and Mastra Memory supplies history. When `false`, the full LiveKit\n * in-session context is sent on every turn instead.\n */\n memory?: MastraVoiceAgentMemory | false;\n /** Request context entries forwarded to generation. */\n requestContext?: RequestContext | Record<string, unknown>;\n /**\n * Called when the Mastra agent starts a tool call mid-reply. Return a short phrase (e.g. \"Let\n * me look that up.\") to speak it while the tool runs; it also appears in the transcript. Return\n * nothing to stay silent. Applies to the agent generator built here; the workflow generator\n * takes its own equivalent via `createWorkflowReplyGenerator`.\n */\n toolFeedback?: (toolCall: VoiceToolCall) => string | undefined | void;\n /**\n * Called as each tool call starts mid-reply (before the tool result is known), the building block\n * for tool-driven side effects — analytics, agent-initiated hang-up — without waiting for the turn\n * to finish. Runs synchronously on the stream; keep it cheap and non-throwing. Applies to the agent\n * generator built here; the workflow generator surfaces tool calls via `onTurnComplete` instead.\n */\n onToolCall?: (toolCall: VoiceToolCall) => void;\n /**\n * Called once per turn after the reply has finished streaming to text-to-speech. Runs off the\n * audio path and fire-and-forget — the turn does not await it — so post-turn memory\n * maintenance, CRM writes, or analytics never delay the caller or the next turn. The context\n * carries the produced reply ({@link VoiceTurnResult}) and the resolved `memory` mapping, so\n * this is where a truly non-blocking `memory.updateWorkingMemory(...)` belongs. A thrown error\n * or rejected promise is logged, not propagated. Applies to the agent generator built here; the\n * workflow generator takes its own via `createWorkflowReplyGenerator`.\n */\n onTurnComplete?: VoiceTurnCompleteHook;\n /**\n * Periodic AI re-disclosure. When set, once `everyMs` has elapsed since the last disclosure the\n * NEXT turn's reply is prefixed with `text` (spoken at the turn boundary, never mid-turn), so long\n * calls keep re-disclosing the AI status. Applies to the agent and workflow/custom generators. The\n * worker derives this from `configuration.greeting.repeatEvery` / `repeatText`; `text` defaults to\n * {@link DEFAULT_DISCLOSURE_REMINDER}.\n */\n greetingReminder?: { everyMs: number; text?: string };\n /** Extra options merged into every `agent.stream()` call (agent generator only). */\n streamOptions?: MastraStreamOptions;\n /** LiveKit agent instructions. Unused for reply generation (the Mastra agent/workflow applies its own). */\n instructions?: string;\n id?: voice.AgentOptions<unknown>['id'];\n stt?: voice.AgentOptions<unknown>['stt'];\n vad?: voice.AgentOptions<unknown>['vad'];\n tts?: voice.AgentOptions<unknown>['tts'];\n turnHandling?: voice.AgentOptions<unknown>['turnHandling'];\n}\n\nfunction toRequestContext(value: RequestContext | Record<string, unknown> | undefined): RequestContext | undefined {\n if (!value) return undefined;\n if (value instanceof RequestContext) return value;\n return new RequestContext<unknown>(Object.entries(value));\n}\n\n/**\n * The session only runs its cascaded reply pipeline when an `llm` instance is present —\n * `llmNode` replaces the inference step, but the gate checks `llm instanceof LLM`. This\n * placeholder satisfies the gate; the Mastra agent/workflow does the actual generation.\n */\nclass MastraPlaceholderLLM extends llm.LLM {\n label(): string {\n return 'mastra.MastraVoiceAgent';\n }\n\n override get model(): string {\n return 'mastra-agent';\n }\n\n override get provider(): string {\n return 'mastra';\n }\n\n chat(): llm.LLMStream {\n throw new Error(\n '@mastra/livekit: reply generation runs through the Mastra agent via llmNode; the placeholder LLM cannot be used for inference.',\n );\n }\n}\n\n/**\n * A LiveKit `voice.Agent` whose replies come from a Mastra agent or workflow.\n *\n * LiveKit keeps ownership of the audio loop (VAD, STT, turn detection, TTS, barge-in) and calls\n * `llmNode` once per detected user turn; the node delegates to a {@link VoiceReplyGenerator}\n * which streams text deltas back. On barge-in LiveKit cancels the returned stream, which aborts\n * the in-flight generation.\n */\nexport class MastraVoiceAgent extends voice.Agent {\n readonly mastraAgent?: MastraAgent;\n readonly memory: MastraVoiceAgentMemory | false;\n readonly requestContext?: RequestContext;\n readonly streamOptions?: MastraStreamOptions;\n private readonly replyGenerator: VoiceReplyGenerator;\n private readonly reminder?: DisclosureReminder;\n\n constructor(options: MastraVoiceAgentOptions) {\n if (options.agent && options.generate) {\n throw new Error(\n '@mastra/livekit: MastraVoiceAgent requires `agent` or `generate`, not both — they are mutually exclusive reply sources.',\n );\n }\n super({\n id: options.id,\n instructions: options.instructions ?? DEFAULT_INSTRUCTIONS,\n stt: options.stt,\n vad: options.vad,\n llm: new MastraPlaceholderLLM(),\n tts: options.tts,\n turnHandling: options.turnHandling,\n });\n this.memory = options.memory ?? false;\n this.requestContext = toRequestContext(options.requestContext);\n this.streamOptions = options.streamOptions;\n if (options.greetingReminder) {\n this.reminder = new DisclosureReminder(\n options.greetingReminder.everyMs,\n options.greetingReminder.text?.trim() || DEFAULT_DISCLOSURE_REMINDER,\n );\n }\n\n if (options.generate) {\n this.replyGenerator = options.generate;\n } else if (options.agent) {\n this.mastraAgent = options.agent;\n this.replyGenerator = createAgentReplyGenerator({\n agent: options.agent,\n streamOptions: options.streamOptions,\n toolFeedback: options.toolFeedback,\n onToolCall: options.onToolCall,\n onTurnComplete: options.onTurnComplete,\n });\n } else {\n throw new Error('@mastra/livekit: MastraVoiceAgent requires `agent` or `generate`.');\n }\n }\n\n override async llmNode(\n chatCtx: llm.ChatContext,\n _toolCtx: llm.ToolContext,\n _modelSettings: voice.ModelSettings,\n ): Promise<ReadableStream<llm.ChatChunk | string> | null> {\n const messages: VoiceTurnMessage[] =\n this.memory === false ? chatContextToMessages(chatCtx) : extractNewTurnMessages(chatCtx);\n if (messages.length === 0) return null;\n\n const reply = await this.replyGenerator({\n messages,\n chatCtx,\n memory: this.memory,\n requestContext: this.requestContext,\n tracingContext: this.streamOptions?.tracingContext,\n });\n if (!reply) return null;\n\n // Periodic AI re-disclosure: when the interval has elapsed, prefix this turn's spoken reply with\n // the reminder. Done at the turn boundary (never mid-turn), riding the same stream so barge-in\n // cancellation still propagates to the underlying generation. The clock only resets once the\n // reminder is actually threaded into the outgoing reply below, not just because it was due.\n // KNOWN LIMIT: \"threaded into the reply\" is stream-build time, not playout. Under LiveKit's\n // preemptive generation a discarded speculative reply still resets the clock, so the next real\n // turn can miss its reminder — hence the documented repeatEvery/preemptiveGeneration\n // incompatibility. A playout-accurate reset needs a confirmed-turn signal llmNode doesn't have.\n const reminder = this.reminder?.due();\n if (!reminder) return reply;\n this.reminder?.markDelivered();\n return prependText(reply, reminder);\n }\n}\n\nexport function createMastraVoiceAgent(options: MastraVoiceAgentOptions): MastraVoiceAgent {\n return new MastraVoiceAgent(options);\n}\n","import { ReadableStream } from 'node:stream/web';\nimport { APIConnectionError, APIError, APIStatusError, APITimeoutError } from '@livekit/agents';\nimport { RequestContext } from '@mastra/core/request-context';\nimport { mapTurnUsage } from './bridge';\nimport type {\n VoiceReplyGenerator,\n VoiceToolCall,\n VoiceTurnCompleteContext,\n VoiceTurnCompleteHook,\n VoiceTurnUsage,\n} from './bridge';\n\nconst DEFAULT_API_PREFIX = '/api';\n/** Connect + first-token budget when not overridden. Plugin mode passes `connOptions.timeoutMs`. */\nexport const DEFAULT_REMOTE_TIMEOUT_MS = 10_000;\n/** Standalone initial-connection retry attempts. Plugin mode forces this to 0 (base class owns retries). */\nexport const DEFAULT_REMOTE_RETRIES = 2;\n\n/** Thrown (as a plain, non-retryable error) when the server emits a chunk that needs client action. */\nconst HITL_UNSUPPORTED_MESSAGE =\n '@mastra/livekit: the agent requested tool approval or suspended a tool call; human-in-the-loop ' +\n 'flows (approve-tool-call / resume-stream) are not supported on the voice path. Remove requireApproval ' +\n 'or suspend from the tools this agent uses on voice calls.';\n\n/**\n * Options for the remote Mastra transport. Shape mirrors the in-process `AgentReplyGeneratorOptions`\n * so `MastraLLM` can accept either source interchangeably.\n */\nexport interface RemoteMastraAgentOptions {\n /** Base URL of the remote Mastra server, e.g. `https://my-app.mastra.cloud`. */\n baseUrl: string;\n /** Agent key in the Mastra config's `agents`. */\n agentId: string;\n /** Path prefix for the Mastra API. Defaults to `'/api'`. */\n apiPrefix?: string;\n /** Static headers, or a (possibly async) resolver invoked per turn — e.g. to mint a fresh token. */\n headers?: Record<string, string> | (() => Record<string, string> | Promise<Record<string, string>>);\n /** Injectable `fetch` for tests/proxies. Defaults to `globalThis.fetch`. */\n fetch?: typeof globalThis.fetch;\n /**\n * Connect + first-token timeout in ms. Plugin mode default: LiveKit's `connOptions.timeoutMs` (10s).\n * Standalone default: {@link DEFAULT_REMOTE_TIMEOUT_MS}.\n */\n timeoutMs?: number;\n /**\n * Initial-connection retry attempts (before the first chunk only). Standalone default:\n * {@link DEFAULT_REMOTE_RETRIES}. In plugin mode the LiveKit base class owns retries and this is\n * forced to 0.\n */\n retries?: number;\n /** Extra fields merged into each stream request body (advanced). */\n body?: Record<string, unknown>;\n}\n\n/** {@link RemoteMastraAgentOptions} plus the per-turn observer hooks the generator threads through. */\nexport interface RemoteAgentReplyGeneratorOptions extends RemoteMastraAgentOptions {\n /** Speak a short phrase while a tool runs. See {@link MastraVoiceAgentOptions.toolFeedback}. */\n toolFeedback?: (toolCall: VoiceToolCall) => string | undefined | void;\n /** Notified as each tool-call chunk arrives, mid-stream. See {@link MastraVoiceAgentOptions.onToolCall}. */\n onToolCall?: (toolCall: VoiceToolCall) => void;\n /** Fired off the audio path after the reply streams. See {@link MastraVoiceAgentOptions.onTurnComplete}. */\n onTurnComplete?: VoiceTurnCompleteHook;\n}\n\ntype RawChunk = { type?: string; payload?: Record<string, unknown> };\n\nfunction trimTrailingSlash(url: string): string {\n return url.endsWith('/') ? url.slice(0, -1) : url;\n}\n\nfunction toMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nasync function resolveHeaders(headers: RemoteMastraAgentOptions['headers']): Promise<Record<string, string>> {\n if (!headers) return {};\n if (typeof headers === 'function') return (await headers()) ?? {};\n return headers;\n}\n\nfunction serializeRequestContext(\n requestContext: RequestContext | Record<string, unknown> | undefined,\n): Record<string, unknown> | undefined {\n if (!requestContext) return undefined;\n // Mirror client-js `parseClientRequestContext`.\n if (requestContext instanceof RequestContext) return Object.fromEntries(requestContext.entries());\n return requestContext;\n}\n\nasync function safeReadBody(response: Response): Promise<object | null> {\n try {\n const text = await response.text();\n if (!text) return null;\n try {\n const parsed: unknown = JSON.parse(text);\n return parsed && typeof parsed === 'object' ? (parsed as object) : { message: String(parsed) };\n } catch {\n return { message: text };\n }\n } catch {\n return null;\n }\n}\n\n/**\n * Reads a Mastra SSE stream and yields each event's parsed JSON. Framing matches the server's\n * `processMastraStream` (buffer, split on `\\n\\n`, strip `data: `, stop on `[DONE]`); undecodable\n * `data:` lines are skipped. Aborting `signal` cancels the underlying reader.\n */\nexport async function* readMastraSSE(\n body: globalThis.ReadableStream<Uint8Array>,\n signal: AbortSignal,\n): AsyncGenerator<RawChunk> {\n const reader = body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n const onAbort = () => void reader.cancel().catch(() => {});\n if (signal.aborted) {\n void reader.cancel().catch(() => {});\n return;\n }\n signal.addEventListener('abort', onAbort, { once: true });\n try {\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n const events = buffer.split('\\n\\n');\n buffer = events.pop() ?? '';\n for (const event of events) {\n if (!event.startsWith('data:')) continue;\n const data = event.slice(event.startsWith('data: ') ? 6 : 5).trim();\n if (data === '[DONE]') return;\n if (!data) continue;\n let json: unknown;\n try {\n json = JSON.parse(data);\n } catch {\n continue; // tolerate a stray non-JSON line\n }\n if (json && typeof json === 'object') yield json as RawChunk;\n }\n }\n } finally {\n signal.removeEventListener('abort', onAbort);\n try {\n reader.releaseLock();\n } catch {\n // A read may still be pending when the fetch was aborted; the stream is torn down anyway.\n }\n }\n}\n\n/**\n * A {@link VoiceReplyGenerator} that runs the Mastra agent loop on a **remote** Mastra server over\n * HTTP/SSE. Shaped exactly like the in-process `createAgentReplyGenerator`: it consumes the same\n * chunk vocabulary, drives the same `toolFeedback` / `onToolCall` / `onTurnComplete` seams, and\n * cancelling the returned stream (LiveKit does this on barge-in) tears down the HTTP request so the\n * server aborts generation.\n *\n * Usable standalone via the worker's `generate:` hatch (a minimum-viable remote worker mode), and as\n * the transport `MastraLLM` wraps. Errors are thrown as LiveKit `APIError` subclasses so the plugin's\n * base-class retry loop and `FallbackAdapter` behave; a connect + first-token watchdog prevents\n * indefinite dead air.\n */\nexport function createRemoteAgentReplyGenerator(options: RemoteAgentReplyGeneratorOptions): VoiceReplyGenerator {\n const {\n baseUrl,\n agentId,\n apiPrefix = DEFAULT_API_PREFIX,\n headers,\n fetch: fetchImpl = globalThis.fetch,\n timeoutMs = DEFAULT_REMOTE_TIMEOUT_MS,\n retries = DEFAULT_REMOTE_RETRIES,\n body: extraBody,\n toolFeedback,\n onToolCall,\n onTurnComplete,\n } = options;\n\n if (!fetchImpl) {\n throw new Error('@mastra/livekit: no fetch implementation available; pass `fetch` or run on Node ≥ 22.');\n }\n const url = `${trimTrailingSlash(baseUrl)}${apiPrefix}/agents/${agentId}/stream`;\n\n return ctx => {\n if (ctx.messages.length === 0) return null;\n\n // Reassigned per retry attempt (see the loop below) so a watchdog abort on one attempt can't\n // poison the next; `cancel()` always aborts whichever attempt is currently in flight.\n let currentAbortController: AbortController | undefined;\n let cancelled = false;\n // Accumulated as the turn streams so the post-turn hook sees what was actually produced.\n let replyText = '';\n const toolCalls: VoiceToolCall[] = [];\n let usage: VoiceTurnUsage | undefined;\n\n const emitTurnComplete = (interrupted: boolean) => {\n if (!onTurnComplete) return;\n const completeCtx: VoiceTurnCompleteContext = {\n ...ctx,\n result: { text: replyText, toolCalls, interrupted, usage },\n };\n Promise.resolve()\n .then(() => onTurnComplete(completeCtx))\n .catch(error => {\n console.warn('@mastra/livekit: onTurnComplete hook threw', error);\n });\n };\n\n const requestBody: Record<string, unknown> = {\n messages: ctx.messages,\n // The server schema requires a resource when memory is present; default it to the thread id,\n // matching the worker's own thread bootstrap.\n memory: ctx.memory\n ? { thread: ctx.memory.thread, resource: ctx.memory.resource ?? ctx.memory.thread }\n : undefined,\n requestContext: serializeRequestContext(ctx.requestContext),\n ...extraBody,\n };\n\n return new ReadableStream<string>({\n start: async controller => {\n // `retryable` is the LiveKit contract flag: true only before the first chunk is emitted, so a\n // voice turn is never replayed half-heard. It also gates the standalone connect-retry.\n let retryable = true;\n try {\n for (let attempt = 0; ; attempt++) {\n // A fresh controller per attempt: reusing one across retries meant a watchdog abort on an\n // earlier attempt left every subsequent attempt's fetch already-aborted before it started.\n const abortController = new AbortController();\n currentAbortController = abortController;\n let timedOut = false;\n let watchdog: ReturnType<typeof setTimeout> | undefined;\n const clearWatchdog = () => {\n if (watchdog) {\n clearTimeout(watchdog);\n watchdog = undefined;\n }\n };\n try {\n watchdog = setTimeout(() => {\n timedOut = true;\n abortController.abort();\n }, timeoutMs);\n (watchdog as { unref?: () => void }).unref?.();\n\n const resolvedHeaders = await resolveHeaders(headers);\n if (cancelled) {\n clearWatchdog();\n break;\n }\n const response = await fetchImpl(url, {\n method: 'POST',\n headers: { 'content-type': 'application/json', accept: 'text/event-stream', ...resolvedHeaders },\n body: JSON.stringify(requestBody),\n signal: abortController.signal,\n });\n if (!response.ok) {\n const errorBody = await safeReadBody(response);\n throw new APIStatusError({\n message: `@mastra/livekit: Mastra agent stream request failed with status ${response.status}`,\n options: { statusCode: response.status, body: errorBody, retryable },\n });\n }\n if (!response.body) {\n throw new APIConnectionError({\n message: '@mastra/livekit: Mastra agent stream returned an empty response body',\n options: { retryable },\n });\n }\n\n for await (const chunk of readMastraSSE(\n response.body as unknown as globalThis.ReadableStream<Uint8Array>,\n abortController.signal,\n )) {\n if (cancelled) break;\n // First chunk: the server has committed to this generation — forbid any further\n // retry so the turn can't be replayed mid-stream. The watchdog is NOT cleared here:\n // lifecycle metadata (step-start, text-start, ...) isn't proof the model is\n // producing anything, and disarming on it would turn a post-metadata stall into\n // indefinite dead air. It disarms on the first sign of model output below.\n retryable = false;\n const payload = chunk.payload ?? {};\n switch (chunk.type) {\n case 'text-delta': {\n const text = payload.text;\n if (typeof text === 'string' && text) {\n clearWatchdog();\n replyText += text;\n controller.enqueue(text);\n }\n break;\n }\n case 'tool-call': {\n // A tool call is first-token progress too — the model committed to a tool run,\n // which may legitimately outlast the connect budget before any text streams.\n clearWatchdog();\n const toolCall: VoiceToolCall = {\n toolCallId: String(payload.toolCallId ?? ''),\n toolName: String(payload.toolName ?? ''),\n args: payload.args,\n };\n toolCalls.push(toolCall);\n // Observer hooks are customer code: a throw must not tear down an otherwise\n // healthy reply stream (same isolation as onTurnComplete).\n try {\n onToolCall?.(toolCall);\n } catch (error) {\n console.warn('@mastra/livekit: onToolCall hook threw', error);\n }\n if (toolFeedback) {\n let filler: string | undefined | void;\n try {\n filler = toolFeedback(toolCall);\n } catch (error) {\n console.warn('@mastra/livekit: toolFeedback hook threw', error);\n }\n if (filler) controller.enqueue(filler.endsWith(' ') ? filler : `${filler} `);\n }\n break;\n }\n case 'finish': {\n clearWatchdog();\n const output = payload.output as { usage?: unknown } | undefined;\n const turnUsage = mapTurnUsage(output?.usage);\n if (turnUsage) {\n usage = turnUsage;\n try {\n ctx.onUsage?.(turnUsage);\n } catch (error) {\n console.warn('@mastra/livekit: onUsage hook threw', error);\n }\n }\n break;\n }\n case 'tool-call-approval':\n case 'tool-call-suspended':\n throw new Error(HITL_UNSUPPORTED_MESSAGE);\n case 'error': {\n const error = payload.error;\n throw error instanceof Error ? error : new Error(String(error));\n }\n default:\n break; // ignore everything else (text-start, step-start, tool-result, ...)\n }\n }\n clearWatchdog();\n break; // success, or a clean barge-in break out of the SSE loop\n } catch (error) {\n clearWatchdog();\n if (cancelled) break; // barge-in: not a failure, emit interrupted below\n\n // Classify into the LiveKit error vocabulary. Application errors thrown after streaming\n // began (an `error` chunk, a HITL chunk) are not connection failures — propagate as-is.\n let typed: APIError;\n if (timedOut) {\n typed = new APITimeoutError({ options: { retryable } });\n } else if (error instanceof APIError) {\n typed = error;\n } else if (retryable) {\n typed = new APIConnectionError({ message: toMessage(error), options: { retryable } });\n } else {\n throw error;\n }\n if (typed.retryable && attempt < retries) continue;\n throw typed;\n }\n }\n if (!cancelled) controller.close();\n // Success or clean barge-in: the turn is done either way.\n emitTurnComplete(cancelled);\n } catch (error) {\n // Barge-in never reaches here (handled above); a real failure errors the stream and does\n // not fire onTurnComplete — same contract as the in-process generator.\n if (cancelled) {\n emitTurnComplete(true);\n return;\n }\n controller.error(error);\n }\n },\n cancel: () => {\n cancelled = true;\n currentAbortController?.abort();\n },\n });\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"remote-BZ7eyB1q.cjs","names":["ReadableStream","RequestContext","llm","voice","RequestContext","ReadableStream","APIStatusError","APIConnectionError","APITimeoutError","APIError"],"sources":["../src/messages.ts","../src/bridge.ts","../src/remote.ts"],"sourcesContent":["import type { llm } from '@livekit/agents';\n\n/**\n * Fixed id LiveKit gives the customer Agent's instructions when it injects them as a leading\n * `role: 'system'` message into the chat context passed to `chat()` / `llmNode`. We drop this\n * item so the server-side Mastra agent's own system prompt is authoritative.\n */\nexport const LIVEKIT_INSTRUCTIONS_MESSAGE_ID = 'lk.agent_task.instructions';\n\n/**\n * A message bound for `agent.stream(...)` (in-process) or the Mastra server stream route (remote).\n * `id` carries the LiveKit `ChatMessage.id` so the server can dedupe/upsert by id — making\n * base-class retries, preemptive double-sends, and the interrupted-turn reconciliation recipe idempotent.\n */\nexport type VoiceTurnMessage =\n | { role: 'system'; content: string; id?: string }\n | { role: 'user'; content: string; id?: string }\n | { role: 'assistant'; content: string; id?: string };\n\nfunction textOfMessage(message: llm.ChatMessage): string {\n const parts: string[] = [];\n for (const part of message.content) {\n if (typeof part === 'string') {\n parts.push(part);\n } else if (part.type === 'instructions') {\n parts.push(part.value);\n } else if (part.type === 'audio_content' && part.transcript) {\n parts.push(part.transcript);\n }\n }\n return parts.join('\\n').trim();\n}\n\nfunction toVoiceTurnMessage(item: llm.ChatItem): VoiceTurnMessage | undefined {\n if (item.type !== 'message') return undefined;\n const content = textOfMessage(item);\n if (!content) return undefined;\n const id = item.id;\n if (item.role === 'user') return { role: 'user', content, id };\n if (item.role === 'assistant') return { role: 'assistant', content, id };\n // 'system' and 'developer' both map to a Mastra system message.\n return { role: 'system', content, id };\n}\n\n/**\n * Extracts only the messages added since the agent last spoke. Used when Mastra Memory is\n * the source of truth for conversation history: prior turns are already persisted in the\n * thread, so re-sending them would duplicate history.\n *\n * Two extensions over the naive \"slice after the last assistant message\":\n *\n * - **Interrupted-turn self-heal:** when the last assistant message was cut off by barge-in\n * (`interrupted: true`), the server never persisted it — aborted runs skip persistence — so\n * its heard-only text is missing from the thread. Re-send that fragment (ordered first) this\n * turn to backfill it. It stops being \"the last assistant message\" once a full reply lands,\n * so each interrupted fragment is sent exactly once, on the following turn.\n * - **Instructions filter:** LiveKit injects the customer Agent's `instructions` as a\n * leading `system` message ({@link LIVEKIT_INSTRUCTIONS_MESSAGE_ID}); the server-side Mastra\n * agent owns its own system prompt, so drop it (it would otherwise ship on the first turn,\n * before any assistant message).\n */\nexport function extractNewTurnMessages(chatCtx: llm.ChatContext): VoiceTurnMessage[] {\n const items = chatCtx.items;\n let lastAssistantIdx = -1;\n for (let i = items.length - 1; i >= 0; i--) {\n const item = items[i];\n if (item?.type === 'message' && item.role === 'assistant') {\n lastAssistantIdx = i;\n break;\n }\n }\n const lastAssistant = lastAssistantIdx >= 0 ? items[lastAssistantIdx] : undefined;\n const healInterrupted =\n lastAssistant?.type === 'message' && lastAssistant.role === 'assistant' && lastAssistant.interrupted;\n // Include the interrupted fragment by starting the slice AT it, otherwise start strictly after.\n const startIdx = healInterrupted ? lastAssistantIdx : lastAssistantIdx + 1;\n\n const messages: VoiceTurnMessage[] = [];\n for (const item of items.slice(startIdx)) {\n if (item.type === 'message' && item.id === LIVEKIT_INSTRUCTIONS_MESSAGE_ID) continue;\n const message = toVoiceTurnMessage(item);\n if (message) messages.push(message);\n }\n return messages;\n}\n\n/**\n * Converts the full LiveKit chat context to Mastra messages. Used when the bridge runs\n * without Mastra Memory and LiveKit's in-session context is the only history. The agent's\n * LiveKit-level instructions are excluded — the Mastra agent applies its own instructions.\n */\nexport function chatContextToMessages(chatCtx: llm.ChatContext): VoiceTurnMessage[] {\n const withoutInstructions = chatCtx.copy({ excludeInstructions: true, excludeFunctionCall: true });\n const messages: VoiceTurnMessage[] = [];\n for (const item of withoutInstructions.items) {\n const message = toVoiceTurnMessage(item);\n if (message) messages.push(message);\n }\n return messages;\n}\n","import { ReadableStream } from 'node:stream/web';\nimport { llm, voice } from '@livekit/agents';\nimport type { Agent as MastraAgent, AgentExecutionOptionsBase } from '@mastra/core/agent';\nimport type { TracingContext } from '@mastra/core/observability';\nimport { RequestContext } from '@mastra/core/request-context';\nimport { chatContextToMessages, extractNewTurnMessages } from './messages';\nimport type { VoiceTurnMessage } from './messages';\n\nconst DEFAULT_INSTRUCTIONS = 'You are a helpful voice assistant powered by a Mastra agent.';\n\n/** Default spoken text for periodic AI re-disclosure. See {@link MastraVoiceAgentOptions.greetingReminder}. */\nexport const DEFAULT_DISCLOSURE_REMINDER = \"Just a reminder, you're speaking with an AI assistant.\";\n\n/**\n * Tracks periodic AI re-disclosure for a single call. `due()` returns the reminder text once\n * `everyMs` has elapsed since the last disclosure (resetting the clock), otherwise `undefined`.\n * Time is injectable so the interval logic is deterministically testable.\n */\nexport class DisclosureReminder {\n private lastAt: number;\n constructor(\n private readonly everyMs: number,\n private readonly text: string,\n now: number = Date.now(),\n ) {\n this.lastAt = now;\n }\n /** Call once per turn: the reminder text if it's due, else `undefined`. Does not reset the clock —\n * call {@link DisclosureReminder.markDelivered} once the reminder is actually threaded into the\n * outgoing reply, so a reminder that never makes it out isn't silently skipped for a full interval. */\n due(now: number = Date.now()): string | undefined {\n if (now - this.lastAt < this.everyMs) return undefined;\n return this.text;\n }\n /** Resets the clock. Call only once the reminder text from {@link due} was actually emitted. */\n markDelivered(now: number = Date.now()): void {\n this.lastAt = now;\n }\n}\n\n/**\n * Wraps `source` in a stream that emits `text` as a single leading chunk (with a trailing space, so\n * TTS pauses before the reply) before piping the rest of `source` through unchanged. Cancelling the\n * wrapper cancels `source` — so barge-in still aborts the underlying generation.\n */\nexport function prependText(source: ReadableStream<string>, text: string): ReadableStream<string> {\n const prefix = text.endsWith(' ') ? text : `${text} `;\n const reader = source.getReader();\n return new ReadableStream<string>({\n start(controller) {\n controller.enqueue(prefix);\n },\n async pull(controller) {\n try {\n const { done, value } = await reader.read();\n if (done) controller.close();\n else controller.enqueue(value);\n } catch (error) {\n controller.error(error);\n }\n },\n cancel(reason) {\n return reader.cancel(reason);\n },\n });\n}\n\nexport type MastraStreamOptions = Partial<AgentExecutionOptionsBase<unknown>>;\n\nexport interface VoiceToolCall {\n toolCallId: string;\n toolName: string;\n args?: unknown;\n}\n\n/**\n * Token usage for one turn, captured from the model's `finish` chunk. Field names mirror LiveKit's\n * `CompletionUsage` so the plugin can forward it into `metrics_collected` without remapping.\n */\nexport interface VoiceTurnUsage {\n /** Tokens in the prompt (LiveKit `promptTokens`). */\n promptTokens: number;\n /** Tokens in the completion (LiveKit `completionTokens`). */\n completionTokens: number;\n /** Cached prompt tokens (LiveKit `promptCachedTokens`). */\n promptCachedTokens: number;\n /** Total tokens for the turn. */\n totalTokens: number;\n}\n\n/**\n * Maps a Mastra `finish` chunk's usage (`payload.output.usage`, AI-SDK `LanguageModelUsage`) to the\n * LiveKit-shaped {@link VoiceTurnUsage}, or `undefined` when the chunk carries no token counts.\n * Handles both the flat V2 usage shape (`inputTokens`/`outputTokens`) and the nested V3 shape\n * (`inputTokens.total`/`outputTokens.total`).\n */\nexport function mapTurnUsage(usage: unknown): VoiceTurnUsage | undefined {\n if (!usage || typeof usage !== 'object') return undefined;\n const u = usage as Record<string, unknown>;\n // Prefer the flat V2 shape (`inputTokens: number`); fall back to the nested V3 shape\n // (`inputTokens: { total, cacheRead }`).\n const totalOf = (v: unknown): number | undefined => {\n if (typeof v === 'number') return v;\n if (v && typeof v === 'object' && typeof (v as { total?: unknown }).total === 'number') {\n return (v as { total: number }).total;\n }\n return undefined;\n };\n const cacheReadOf = (v: unknown): number | undefined =>\n v && typeof v === 'object' && typeof (v as { cacheRead?: unknown }).cacheRead === 'number'\n ? (v as { cacheRead: number }).cacheRead\n : undefined;\n\n const promptTokens = totalOf(u.inputTokens) ?? 0;\n const completionTokens = totalOf(u.outputTokens) ?? 0;\n const promptCachedTokens =\n (typeof u.cachedInputTokens === 'number' ? u.cachedInputTokens : cacheReadOf(u.inputTokens)) ?? 0;\n const totalTokens = typeof u.totalTokens === 'number' ? u.totalTokens : promptTokens + completionTokens;\n // Nothing was reported at all → treat as no usage rather than emitting an all-zero chunk.\n if (promptTokens === 0 && completionTokens === 0 && totalTokens === 0 && promptCachedTokens === 0) {\n return undefined;\n }\n return { promptTokens, completionTokens, promptCachedTokens, totalTokens };\n}\n\nexport interface MastraVoiceAgentMemory {\n thread: string;\n resource?: string;\n}\n\n/**\n * Per-turn context handed to a {@link VoiceReplyGenerator}. LiveKit calls `llmNode` once per\n * detected user turn; the bridge builds this context and asks the generator for the reply.\n */\nexport interface VoiceTurnContext {\n /**\n * The messages to generate a reply from. With Mastra Memory on, only the messages new since\n * the agent last spoke (history comes from the thread); with memory off, the full session.\n *\n * For a workflow / custom generator: pass these straight to a memory-backed `agent.stream(...,\n * { memory })` inside a step so the agent backfills history from the thread (no duplication). A\n * stateless workflow that wants the entire transcript every turn should read `chatCtx` instead\n * (e.g. `chatContextToMessages(chatCtx)`), since there is no thread to backfill from.\n */\n messages: VoiceTurnMessage[];\n /** The raw LiveKit chat context, for generators that want the full transcript or message parts. */\n chatCtx: llm.ChatContext;\n /** Resolved memory mapping for the call, or `false` when memory is disabled. */\n memory: MastraVoiceAgentMemory | false;\n /** Request context forwarded to generation. */\n requestContext?: RequestContext;\n /** Voice-call span context, so each turn's generation nests under the call trace. */\n tracingContext?: TracingContext;\n /**\n * Internal, per-turn side channel for token usage. A generator invokes this once, when the\n * `finish` chunk carries usage, so the caller (e.g. `MastraLLMStream`) can attribute usage to\n * exactly this turn — kept on the context (not on generator options) so overlapping turns from\n * preemptive generation can't misattribute usage. Fire-and-forget; the generator does not await it.\n */\n onUsage?: (usage: VoiceTurnUsage) => void;\n}\n\n/**\n * What a turn produced, handed to {@link VoiceTurnCompleteHook} after the reply finishes.\n */\nexport interface VoiceTurnResult {\n /** The assistant reply text streamed this turn, accumulated from the model's text deltas. */\n text: string;\n /** Tool calls the agent made during the turn, in order. */\n toolCalls: VoiceToolCall[];\n /** True when barge-in cut the turn short before it finished streaming. */\n interrupted: boolean;\n /** Token usage for the turn when the model reported it in its `finish` chunk. */\n usage?: VoiceTurnUsage;\n}\n\n/** {@link VoiceTurnContext} plus the reply it produced. Passed to {@link VoiceTurnCompleteHook}. */\nexport interface VoiceTurnCompleteContext extends VoiceTurnContext {\n /** The reply the agent produced this turn. */\n result: VoiceTurnResult;\n}\n\n/**\n * Called once per turn AFTER the reply has finished streaming to text-to-speech — off the audio\n * path. It runs fire-and-forget: the turn does not await it, so post-turn work (memory\n * maintenance, CRM writes, analytics) never delays what the caller hears or the next turn. A\n * thrown error or rejected promise is logged, not propagated. Because the resolved `memory`\n * mapping (`thread`/`resource`) is on the context, this is the place for a truly non-blocking\n * `memory.updateWorkingMemory(...)`. See {@link MastraVoiceAgentOptions.onTurnComplete}.\n */\nexport type VoiceTurnCompleteHook = (ctx: VoiceTurnCompleteContext) => void | Promise<void>;\n\n/**\n * Produces a stream of text deltas for one conversational turn, or `null` to stay silent.\n * Cancelling the returned stream (LiveKit does this on barge-in) must abort the underlying\n * generation. Built-in implementations: {@link createAgentReplyGenerator} (a Mastra agent) and\n * `createWorkflowReplyGenerator` (a Mastra workflow).\n */\nexport type VoiceReplyGenerator = (\n ctx: VoiceTurnContext,\n) => ReadableStream<string> | null | Promise<ReadableStream<string> | null>;\n\nexport interface AgentReplyGeneratorOptions {\n /** The Mastra agent that generates replies. Tools and memory run inside this agent. */\n agent: MastraAgent;\n /** Extra options merged into every `agent.stream()` call (e.g. `tracingContext`). */\n streamOptions?: MastraStreamOptions;\n /** Speak a short phrase while a tool call runs. See {@link MastraVoiceAgentOptions.toolFeedback}. */\n toolFeedback?: (toolCall: VoiceToolCall) => string | undefined | void;\n /** Notified as each tool-call chunk arrives, mid-stream. See {@link MastraVoiceAgentOptions.onToolCall}. */\n onToolCall?: (toolCall: VoiceToolCall) => void;\n /** Fired off the audio path after the reply streams. See {@link MastraVoiceAgentOptions.onTurnComplete}. */\n onTurnComplete?: VoiceTurnCompleteHook;\n}\n\n/**\n * A {@link VoiceReplyGenerator} backed by a Mastra agent: runs the agent's full loop (model,\n * tools, memory) and streams its text deltas. On barge-in the returned stream is cancelled,\n * which aborts the in-flight `agent.stream()`.\n */\nexport function createAgentReplyGenerator(options: AgentReplyGeneratorOptions): VoiceReplyGenerator {\n const { agent, streamOptions, toolFeedback, onToolCall, onTurnComplete } = options;\n return ctx => {\n if (ctx.messages.length === 0) return null;\n\n const abortController = new AbortController();\n const mergedOptions: MastraStreamOptions = {\n ...streamOptions,\n abortSignal: abortController.signal,\n };\n if (ctx.memory) mergedOptions.memory = ctx.memory;\n if (ctx.requestContext) mergedOptions.requestContext = ctx.requestContext;\n\n let cancelled = false;\n // Accumulated as the turn streams so the post-turn hook can see what was actually produced.\n let replyText = '';\n const toolCalls: VoiceToolCall[] = [];\n let usage: VoiceTurnUsage | undefined;\n\n // Fire-and-forget after the reply has streamed: off the audio path (the caller already heard\n // the text), and not awaited, so it never delays the next turn. Errors are logged, not thrown.\n const emitTurnComplete = (interrupted: boolean) => {\n if (!onTurnComplete) return;\n const completeCtx: VoiceTurnCompleteContext = {\n ...ctx,\n result: { text: replyText, toolCalls, interrupted, usage },\n };\n Promise.resolve()\n .then(() => onTurnComplete(completeCtx))\n .catch(error => {\n console.warn('@mastra/livekit: onTurnComplete hook threw', error);\n });\n };\n\n return new ReadableStream<string>({\n start: async controller => {\n try {\n const result = await agent.stream(ctx.messages, mergedOptions);\n for await (const chunk of result.fullStream) {\n if (cancelled) break;\n if (chunk.type === 'text-delta') {\n if (chunk.payload.text) {\n replyText += chunk.payload.text;\n controller.enqueue(chunk.payload.text);\n }\n } else if (chunk.type === 'tool-call') {\n const toolCall: VoiceToolCall = {\n toolCallId: chunk.payload.toolCallId,\n toolName: chunk.payload.toolName,\n args: chunk.payload.args,\n };\n toolCalls.push(toolCall);\n // Observer hooks are customer code: a throw must not tear down an otherwise healthy\n // reply stream (same isolation as onTurnComplete).\n try {\n onToolCall?.(toolCall);\n } catch (error) {\n console.warn('@mastra/livekit: onToolCall hook threw', error);\n }\n if (toolFeedback) {\n let filler: string | undefined | void;\n try {\n filler = toolFeedback(toolCall);\n } catch (error) {\n console.warn('@mastra/livekit: toolFeedback hook threw', error);\n }\n if (filler) controller.enqueue(filler.endsWith(' ') ? filler : `${filler} `);\n }\n } else if (chunk.type === 'finish') {\n // Usage is dropped from the spoken stream but surfaced via the per-turn side channel\n // and on the turn result, so the plugin and onTurnComplete consumers can read it.\n const output = (chunk.payload as { output?: { usage?: unknown } }).output;\n const turnUsage = mapTurnUsage(output?.usage);\n if (turnUsage) {\n usage = turnUsage;\n try {\n ctx.onUsage?.(turnUsage);\n } catch (error) {\n console.warn('@mastra/livekit: onUsage hook threw', error);\n }\n }\n } else if (chunk.type === 'error') {\n const error = chunk.payload.error;\n throw error instanceof Error ? error : new Error(String(error));\n }\n }\n if (!cancelled) controller.close();\n // Success, or a clean barge-in break out of the loop: the turn is done either way.\n emitTurnComplete(cancelled);\n } catch (error) {\n // Barge-in cancels the stream and aborts generation; that's not a failure — the turn\n // still completed (interrupted), so the hook still fires for memory reconciliation.\n if (cancelled || abortController.signal.aborted) {\n emitTurnComplete(true);\n return;\n }\n controller.error(error);\n }\n },\n cancel: () => {\n cancelled = true;\n abortController.abort();\n },\n });\n };\n}\n\nexport interface MastraVoiceAgentOptions {\n /**\n * The Mastra agent that generates replies. Tools and memory run inside this agent. Provide\n * either this or {@link MastraVoiceAgentOptions.generate}.\n */\n agent?: MastraAgent;\n /**\n * A lower-level reply generator (e.g. from `createWorkflowReplyGenerator`). Use instead of\n * `agent` to drive replies with a workflow or any custom generator.\n */\n generate?: VoiceReplyGenerator;\n /**\n * Conversation persistence. When set, only messages new since the agent last spoke are\n * sent each turn and Mastra Memory supplies history. When `false`, the full LiveKit\n * in-session context is sent on every turn instead.\n */\n memory?: MastraVoiceAgentMemory | false;\n /** Request context entries forwarded to generation. */\n requestContext?: RequestContext | Record<string, unknown>;\n /**\n * Called when the Mastra agent starts a tool call mid-reply. Return a short phrase (e.g. \"Let\n * me look that up.\") to speak it while the tool runs; it also appears in the transcript. Return\n * nothing to stay silent. Applies to the agent generator built here; the workflow generator\n * takes its own equivalent via `createWorkflowReplyGenerator`.\n */\n toolFeedback?: (toolCall: VoiceToolCall) => string | undefined | void;\n /**\n * Called as each tool call starts mid-reply (before the tool result is known), the building block\n * for tool-driven side effects — analytics, agent-initiated hang-up — without waiting for the turn\n * to finish. Runs synchronously on the stream; keep it cheap and non-throwing. Applies to the agent\n * generator built here; the workflow generator surfaces tool calls via `onTurnComplete` instead.\n */\n onToolCall?: (toolCall: VoiceToolCall) => void;\n /**\n * Called once per turn after the reply has finished streaming to text-to-speech. Runs off the\n * audio path and fire-and-forget — the turn does not await it — so post-turn memory\n * maintenance, CRM writes, or analytics never delay the caller or the next turn. The context\n * carries the produced reply ({@link VoiceTurnResult}) and the resolved `memory` mapping, so\n * this is where a truly non-blocking `memory.updateWorkingMemory(...)` belongs. A thrown error\n * or rejected promise is logged, not propagated. Applies to the agent generator built here; the\n * workflow generator takes its own via `createWorkflowReplyGenerator`.\n */\n onTurnComplete?: VoiceTurnCompleteHook;\n /**\n * Periodic AI re-disclosure. When set, once `everyMs` has elapsed since the last disclosure the\n * NEXT turn's reply is prefixed with `text` (spoken at the turn boundary, never mid-turn), so long\n * calls keep re-disclosing the AI status. Applies to the agent and workflow/custom generators. The\n * worker derives this from `configuration.greeting.repeatEvery` / `repeatText`; `text` defaults to\n * {@link DEFAULT_DISCLOSURE_REMINDER}.\n */\n greetingReminder?: { everyMs: number; text?: string };\n /** Extra options merged into every `agent.stream()` call (agent generator only). */\n streamOptions?: MastraStreamOptions;\n /** LiveKit agent instructions. Unused for reply generation (the Mastra agent/workflow applies its own). */\n instructions?: string;\n id?: voice.AgentOptions<unknown>['id'];\n stt?: voice.AgentOptions<unknown>['stt'];\n vad?: voice.AgentOptions<unknown>['vad'];\n tts?: voice.AgentOptions<unknown>['tts'];\n turnHandling?: voice.AgentOptions<unknown>['turnHandling'];\n}\n\nfunction toRequestContext(value: RequestContext | Record<string, unknown> | undefined): RequestContext | undefined {\n if (!value) return undefined;\n if (value instanceof RequestContext) return value;\n return new RequestContext<unknown>(Object.entries(value));\n}\n\n/**\n * The session only runs its cascaded reply pipeline when an `llm` instance is present —\n * `llmNode` replaces the inference step, but the gate checks `llm instanceof LLM`. This\n * placeholder satisfies the gate; the Mastra agent/workflow does the actual generation.\n */\nclass MastraPlaceholderLLM extends llm.LLM {\n label(): string {\n return 'mastra.MastraVoiceAgent';\n }\n\n override get model(): string {\n return 'mastra-agent';\n }\n\n override get provider(): string {\n return 'mastra';\n }\n\n chat(): llm.LLMStream {\n throw new Error(\n '@mastra/livekit: reply generation runs through the Mastra agent via llmNode; the placeholder LLM cannot be used for inference.',\n );\n }\n}\n\n/**\n * A LiveKit `voice.Agent` whose replies come from a Mastra agent or workflow.\n *\n * LiveKit keeps ownership of the audio loop (VAD, STT, turn detection, TTS, barge-in) and calls\n * `llmNode` once per detected user turn; the node delegates to a {@link VoiceReplyGenerator}\n * which streams text deltas back. On barge-in LiveKit cancels the returned stream, which aborts\n * the in-flight generation.\n */\nexport class MastraVoiceAgent extends voice.Agent {\n readonly mastraAgent?: MastraAgent;\n readonly memory: MastraVoiceAgentMemory | false;\n readonly requestContext?: RequestContext;\n readonly streamOptions?: MastraStreamOptions;\n private readonly replyGenerator: VoiceReplyGenerator;\n private readonly reminder?: DisclosureReminder;\n\n constructor(options: MastraVoiceAgentOptions) {\n if (options.agent && options.generate) {\n throw new Error(\n '@mastra/livekit: MastraVoiceAgent requires `agent` or `generate`, not both — they are mutually exclusive reply sources.',\n );\n }\n super({\n id: options.id,\n instructions: options.instructions ?? DEFAULT_INSTRUCTIONS,\n stt: options.stt,\n vad: options.vad,\n llm: new MastraPlaceholderLLM(),\n tts: options.tts,\n turnHandling: options.turnHandling,\n });\n this.memory = options.memory ?? false;\n this.requestContext = toRequestContext(options.requestContext);\n this.streamOptions = options.streamOptions;\n if (options.greetingReminder) {\n this.reminder = new DisclosureReminder(\n options.greetingReminder.everyMs,\n options.greetingReminder.text?.trim() || DEFAULT_DISCLOSURE_REMINDER,\n );\n }\n\n if (options.generate) {\n this.replyGenerator = options.generate;\n } else if (options.agent) {\n this.mastraAgent = options.agent;\n this.replyGenerator = createAgentReplyGenerator({\n agent: options.agent,\n streamOptions: options.streamOptions,\n toolFeedback: options.toolFeedback,\n onToolCall: options.onToolCall,\n onTurnComplete: options.onTurnComplete,\n });\n } else {\n throw new Error('@mastra/livekit: MastraVoiceAgent requires `agent` or `generate`.');\n }\n }\n\n override async llmNode(\n chatCtx: llm.ChatContext,\n _toolCtx: llm.ToolContext,\n _modelSettings: voice.ModelSettings,\n ): Promise<ReadableStream<llm.ChatChunk | string> | null> {\n const messages: VoiceTurnMessage[] =\n this.memory === false ? chatContextToMessages(chatCtx) : extractNewTurnMessages(chatCtx);\n if (messages.length === 0) return null;\n\n const reply = await this.replyGenerator({\n messages,\n chatCtx,\n memory: this.memory,\n requestContext: this.requestContext,\n tracingContext: this.streamOptions?.tracingContext,\n });\n if (!reply) return null;\n\n // Periodic AI re-disclosure: when the interval has elapsed, prefix this turn's spoken reply with\n // the reminder. Done at the turn boundary (never mid-turn), riding the same stream so barge-in\n // cancellation still propagates to the underlying generation. The clock only resets once the\n // reminder is actually threaded into the outgoing reply below, not just because it was due.\n // KNOWN LIMIT: \"threaded into the reply\" is stream-build time, not playout. Under LiveKit's\n // preemptive generation a discarded speculative reply still resets the clock, so the next real\n // turn can miss its reminder — hence the documented repeatEvery/preemptiveGeneration\n // incompatibility. A playout-accurate reset needs a confirmed-turn signal llmNode doesn't have.\n const reminder = this.reminder?.due();\n if (!reminder) return reply;\n this.reminder?.markDelivered();\n return prependText(reply, reminder);\n }\n}\n\nexport function createMastraVoiceAgent(options: MastraVoiceAgentOptions): MastraVoiceAgent {\n return new MastraVoiceAgent(options);\n}\n","import { ReadableStream } from 'node:stream/web';\nimport { APIConnectionError, APIError, APIStatusError, APITimeoutError } from '@livekit/agents';\nimport { RequestContext } from '@mastra/core/request-context';\nimport { mapTurnUsage } from './bridge';\nimport type {\n VoiceReplyGenerator,\n VoiceToolCall,\n VoiceTurnCompleteContext,\n VoiceTurnCompleteHook,\n VoiceTurnUsage,\n} from './bridge';\n\nconst DEFAULT_API_PREFIX = '/api';\n/** Connect + first-token budget when not overridden. Plugin mode passes `connOptions.timeoutMs`. */\nexport const DEFAULT_REMOTE_TIMEOUT_MS = 10_000;\n/** Standalone initial-connection retry attempts. Plugin mode forces this to 0 (base class owns retries). */\nexport const DEFAULT_REMOTE_RETRIES = 2;\n\n/** Thrown (as a plain, non-retryable error) when the server emits a chunk that needs client action. */\nconst HITL_UNSUPPORTED_MESSAGE =\n '@mastra/livekit: the agent requested tool approval or suspended a tool call; human-in-the-loop ' +\n 'flows (approve-tool-call / resume-stream) are not supported on the voice path. Remove requireApproval ' +\n 'or suspend from the tools this agent uses on voice calls.';\n\n/**\n * Options for the remote Mastra transport. Shape mirrors the in-process `AgentReplyGeneratorOptions`\n * so `MastraLLM` can accept either source interchangeably.\n */\nexport interface RemoteMastraAgentOptions {\n /** Base URL of the remote Mastra server, e.g. `https://my-app.mastra.cloud`. */\n baseUrl: string;\n /** Agent key in the Mastra config's `agents`. */\n agentId: string;\n /** Path prefix for the Mastra API. Defaults to `'/api'`. */\n apiPrefix?: string;\n /** Static headers, or a (possibly async) resolver invoked per turn — e.g. to mint a fresh token. */\n headers?: Record<string, string> | (() => Record<string, string> | Promise<Record<string, string>>);\n /** Injectable `fetch` for tests/proxies. Defaults to `globalThis.fetch`. */\n fetch?: typeof globalThis.fetch;\n /**\n * Connect + first-token timeout in ms. Plugin mode default: LiveKit's `connOptions.timeoutMs` (10s).\n * Standalone default: {@link DEFAULT_REMOTE_TIMEOUT_MS}.\n */\n timeoutMs?: number;\n /**\n * Initial-connection retry attempts (before the first chunk only). Standalone default:\n * {@link DEFAULT_REMOTE_RETRIES}. In plugin mode the LiveKit base class owns retries and this is\n * forced to 0.\n */\n retries?: number;\n /** Extra fields merged into each stream request body (advanced). */\n body?: Record<string, unknown>;\n}\n\n/** {@link RemoteMastraAgentOptions} plus the per-turn observer hooks the generator threads through. */\nexport interface RemoteAgentReplyGeneratorOptions extends RemoteMastraAgentOptions {\n /** Speak a short phrase while a tool runs. See {@link MastraVoiceAgentOptions.toolFeedback}. */\n toolFeedback?: (toolCall: VoiceToolCall) => string | undefined | void;\n /** Notified as each tool-call chunk arrives, mid-stream. See {@link MastraVoiceAgentOptions.onToolCall}. */\n onToolCall?: (toolCall: VoiceToolCall) => void;\n /** Fired off the audio path after the reply streams. See {@link MastraVoiceAgentOptions.onTurnComplete}. */\n onTurnComplete?: VoiceTurnCompleteHook;\n}\n\ntype RawChunk = { type?: string; payload?: Record<string, unknown> };\n\nfunction trimTrailingSlash(url: string): string {\n return url.endsWith('/') ? url.slice(0, -1) : url;\n}\n\nfunction toMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nasync function resolveHeaders(headers: RemoteMastraAgentOptions['headers']): Promise<Record<string, string>> {\n if (!headers) return {};\n if (typeof headers === 'function') return (await headers()) ?? {};\n return headers;\n}\n\nfunction serializeRequestContext(\n requestContext: RequestContext | Record<string, unknown> | undefined,\n): Record<string, unknown> | undefined {\n if (!requestContext) return undefined;\n // Mirror client-js `parseClientRequestContext`.\n if (requestContext instanceof RequestContext) return Object.fromEntries(requestContext.entries());\n return requestContext;\n}\n\nasync function safeReadBody(response: Response): Promise<object | null> {\n try {\n const text = await response.text();\n if (!text) return null;\n try {\n const parsed: unknown = JSON.parse(text);\n return parsed && typeof parsed === 'object' ? (parsed as object) : { message: String(parsed) };\n } catch {\n return { message: text };\n }\n } catch {\n return null;\n }\n}\n\n/**\n * Reads a Mastra SSE stream and yields each event's parsed JSON. Framing matches the server's\n * `processMastraStream` (buffer, split on `\\n\\n`, strip `data: `, stop on `[DONE]`); undecodable\n * `data:` lines are skipped. Aborting `signal` cancels the underlying reader.\n */\nexport async function* readMastraSSE(\n body: globalThis.ReadableStream<Uint8Array>,\n signal: AbortSignal,\n): AsyncGenerator<RawChunk> {\n const reader = body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n const onAbort = () => void reader.cancel().catch(() => {});\n if (signal.aborted) {\n void reader.cancel().catch(() => {});\n return;\n }\n signal.addEventListener('abort', onAbort, { once: true });\n try {\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n const events = buffer.split('\\n\\n');\n buffer = events.pop() ?? '';\n for (const event of events) {\n if (!event.startsWith('data:')) continue;\n const data = event.slice(event.startsWith('data: ') ? 6 : 5).trim();\n if (data === '[DONE]') return;\n if (!data) continue;\n let json: unknown;\n try {\n json = JSON.parse(data);\n } catch {\n continue; // tolerate a stray non-JSON line\n }\n if (json && typeof json === 'object') yield json as RawChunk;\n }\n }\n } finally {\n signal.removeEventListener('abort', onAbort);\n try {\n reader.releaseLock();\n } catch {\n // A read may still be pending when the fetch was aborted; the stream is torn down anyway.\n }\n }\n}\n\n/**\n * A {@link VoiceReplyGenerator} that runs the Mastra agent loop on a **remote** Mastra server over\n * HTTP/SSE. Shaped exactly like the in-process `createAgentReplyGenerator`: it consumes the same\n * chunk vocabulary, drives the same `toolFeedback` / `onToolCall` / `onTurnComplete` seams, and\n * cancelling the returned stream (LiveKit does this on barge-in) tears down the HTTP request so the\n * server aborts generation.\n *\n * Usable standalone via the worker's `generate:` hatch (a minimum-viable remote worker mode), and as\n * the transport `MastraLLM` wraps. Errors are thrown as LiveKit `APIError` subclasses so the plugin's\n * base-class retry loop and `FallbackAdapter` behave; a connect + first-token watchdog prevents\n * indefinite dead air.\n */\nexport function createRemoteAgentReplyGenerator(options: RemoteAgentReplyGeneratorOptions): VoiceReplyGenerator {\n const {\n baseUrl,\n agentId,\n apiPrefix = DEFAULT_API_PREFIX,\n headers,\n fetch: fetchImpl = globalThis.fetch,\n timeoutMs = DEFAULT_REMOTE_TIMEOUT_MS,\n retries = DEFAULT_REMOTE_RETRIES,\n body: extraBody,\n toolFeedback,\n onToolCall,\n onTurnComplete,\n } = options;\n\n if (!fetchImpl) {\n throw new Error('@mastra/livekit: no fetch implementation available; pass `fetch` or run on Node ≥ 22.');\n }\n const url = `${trimTrailingSlash(baseUrl)}${apiPrefix}/agents/${agentId}/stream`;\n\n return ctx => {\n if (ctx.messages.length === 0) return null;\n\n // Reassigned per retry attempt (see the loop below) so a watchdog abort on one attempt can't\n // poison the next; `cancel()` always aborts whichever attempt is currently in flight.\n let currentAbortController: AbortController | undefined;\n let cancelled = false;\n // Accumulated as the turn streams so the post-turn hook sees what was actually produced.\n let replyText = '';\n const toolCalls: VoiceToolCall[] = [];\n let usage: VoiceTurnUsage | undefined;\n\n const emitTurnComplete = (interrupted: boolean) => {\n if (!onTurnComplete) return;\n const completeCtx: VoiceTurnCompleteContext = {\n ...ctx,\n result: { text: replyText, toolCalls, interrupted, usage },\n };\n Promise.resolve()\n .then(() => onTurnComplete(completeCtx))\n .catch(error => {\n console.warn('@mastra/livekit: onTurnComplete hook threw', error);\n });\n };\n\n const requestBody: Record<string, unknown> = {\n messages: ctx.messages,\n // The server schema requires a resource when memory is present; default it to the thread id,\n // matching the worker's own thread bootstrap.\n memory: ctx.memory\n ? { thread: ctx.memory.thread, resource: ctx.memory.resource ?? ctx.memory.thread }\n : undefined,\n requestContext: serializeRequestContext(ctx.requestContext),\n ...extraBody,\n };\n\n return new ReadableStream<string>({\n start: async controller => {\n // `retryable` is the LiveKit contract flag: true only before the first chunk is emitted, so a\n // voice turn is never replayed half-heard. It also gates the standalone connect-retry.\n let retryable = true;\n try {\n for (let attempt = 0; ; attempt++) {\n // A fresh controller per attempt: reusing one across retries meant a watchdog abort on an\n // earlier attempt left every subsequent attempt's fetch already-aborted before it started.\n const abortController = new AbortController();\n currentAbortController = abortController;\n let timedOut = false;\n let watchdog: ReturnType<typeof setTimeout> | undefined;\n const clearWatchdog = () => {\n if (watchdog) {\n clearTimeout(watchdog);\n watchdog = undefined;\n }\n };\n try {\n watchdog = setTimeout(() => {\n timedOut = true;\n abortController.abort();\n }, timeoutMs);\n (watchdog as { unref?: () => void }).unref?.();\n\n const resolvedHeaders = await resolveHeaders(headers);\n if (cancelled) {\n clearWatchdog();\n break;\n }\n const response = await fetchImpl(url, {\n method: 'POST',\n headers: { 'content-type': 'application/json', accept: 'text/event-stream', ...resolvedHeaders },\n body: JSON.stringify(requestBody),\n signal: abortController.signal,\n });\n if (!response.ok) {\n const errorBody = await safeReadBody(response);\n throw new APIStatusError({\n message: `@mastra/livekit: Mastra agent stream request failed with status ${response.status}`,\n options: { statusCode: response.status, body: errorBody, retryable },\n });\n }\n if (!response.body) {\n throw new APIConnectionError({\n message: '@mastra/livekit: Mastra agent stream returned an empty response body',\n options: { retryable },\n });\n }\n\n for await (const chunk of readMastraSSE(\n response.body as unknown as globalThis.ReadableStream<Uint8Array>,\n abortController.signal,\n )) {\n if (cancelled) break;\n // First chunk: the server has committed to this generation — forbid any further\n // retry so the turn can't be replayed mid-stream. The watchdog is NOT cleared here:\n // lifecycle metadata (step-start, text-start, ...) isn't proof the model is\n // producing anything, and disarming on it would turn a post-metadata stall into\n // indefinite dead air. It disarms on the first sign of model output below.\n retryable = false;\n const payload = chunk.payload ?? {};\n switch (chunk.type) {\n case 'text-delta': {\n const text = payload.text;\n if (typeof text === 'string' && text) {\n clearWatchdog();\n replyText += text;\n controller.enqueue(text);\n }\n break;\n }\n case 'tool-call': {\n // A tool call is first-token progress too — the model committed to a tool run,\n // which may legitimately outlast the connect budget before any text streams.\n clearWatchdog();\n const toolCall: VoiceToolCall = {\n toolCallId: String(payload.toolCallId ?? ''),\n toolName: String(payload.toolName ?? ''),\n args: payload.args,\n };\n toolCalls.push(toolCall);\n // Observer hooks are customer code: a throw must not tear down an otherwise\n // healthy reply stream (same isolation as onTurnComplete).\n try {\n onToolCall?.(toolCall);\n } catch (error) {\n console.warn('@mastra/livekit: onToolCall hook threw', error);\n }\n if (toolFeedback) {\n let filler: string | undefined | void;\n try {\n filler = toolFeedback(toolCall);\n } catch (error) {\n console.warn('@mastra/livekit: toolFeedback hook threw', error);\n }\n if (filler) controller.enqueue(filler.endsWith(' ') ? filler : `${filler} `);\n }\n break;\n }\n case 'finish': {\n clearWatchdog();\n const output = payload.output as { usage?: unknown } | undefined;\n const turnUsage = mapTurnUsage(output?.usage);\n if (turnUsage) {\n usage = turnUsage;\n try {\n ctx.onUsage?.(turnUsage);\n } catch (error) {\n console.warn('@mastra/livekit: onUsage hook threw', error);\n }\n }\n break;\n }\n case 'tool-call-approval':\n case 'tool-call-suspended':\n throw new Error(HITL_UNSUPPORTED_MESSAGE);\n case 'error': {\n const error = payload.error;\n throw error instanceof Error ? error : new Error(String(error));\n }\n default:\n break; // ignore everything else (text-start, step-start, tool-result, ...)\n }\n }\n clearWatchdog();\n break; // success, or a clean barge-in break out of the SSE loop\n } catch (error) {\n clearWatchdog();\n if (cancelled) break; // barge-in: not a failure, emit interrupted below\n\n // Classify into the LiveKit error vocabulary. Application errors thrown after streaming\n // began (an `error` chunk, a HITL chunk) are not connection failures — propagate as-is.\n let typed: APIError;\n if (timedOut) {\n typed = new APITimeoutError({ options: { retryable } });\n } else if (error instanceof APIError) {\n typed = error;\n } else if (retryable) {\n typed = new APIConnectionError({ message: toMessage(error), options: { retryable } });\n } else {\n throw error;\n }\n if (typed.retryable && attempt < retries) continue;\n throw typed;\n }\n }\n if (!cancelled) controller.close();\n // Success or clean barge-in: the turn is done either way.\n emitTurnComplete(cancelled);\n } catch (error) {\n // Barge-in never reaches here (handled above); a real failure errors the stream and does\n // not fire onTurnComplete — same contract as the in-process generator.\n if (cancelled) {\n emitTurnComplete(true);\n return;\n }\n controller.error(error);\n }\n },\n cancel: () => {\n cancelled = true;\n currentAbortController?.abort();\n },\n });\n };\n}\n"],"mappings":"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"}
|
|
@@ -624,6 +624,6 @@ function createRemoteAgentReplyGenerator(options) {
|
|
|
624
624
|
};
|
|
625
625
|
}
|
|
626
626
|
//#endregion
|
|
627
|
-
export {
|
|
627
|
+
export { chatContextToMessages as a, createMastraVoiceAgent as i, MastraVoiceAgent as n, extractNewTurnMessages as o, createAgentReplyGenerator as r, createRemoteAgentReplyGenerator as t };
|
|
628
628
|
|
|
629
|
-
//# sourceMappingURL=remote-
|
|
629
|
+
//# sourceMappingURL=remote-D7n50m8S.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"remote-C9K3UzKv.js","names":[],"sources":["../src/messages.ts","../src/bridge.ts","../src/remote.ts"],"sourcesContent":["import type { llm } from '@livekit/agents';\n\n/**\n * Fixed id LiveKit gives the customer Agent's instructions when it injects them as a leading\n * `role: 'system'` message into the chat context passed to `chat()` / `llmNode`. We drop this\n * item so the server-side Mastra agent's own system prompt is authoritative.\n */\nexport const LIVEKIT_INSTRUCTIONS_MESSAGE_ID = 'lk.agent_task.instructions';\n\n/**\n * A message bound for `agent.stream(...)` (in-process) or the Mastra server stream route (remote).\n * `id` carries the LiveKit `ChatMessage.id` so the server can dedupe/upsert by id — making\n * base-class retries, preemptive double-sends, and the interrupted-turn reconciliation recipe idempotent.\n */\nexport type VoiceTurnMessage =\n | { role: 'system'; content: string; id?: string }\n | { role: 'user'; content: string; id?: string }\n | { role: 'assistant'; content: string; id?: string };\n\nfunction textOfMessage(message: llm.ChatMessage): string {\n const parts: string[] = [];\n for (const part of message.content) {\n if (typeof part === 'string') {\n parts.push(part);\n } else if (part.type === 'instructions') {\n parts.push(part.value);\n } else if (part.type === 'audio_content' && part.transcript) {\n parts.push(part.transcript);\n }\n }\n return parts.join('\\n').trim();\n}\n\nfunction toVoiceTurnMessage(item: llm.ChatItem): VoiceTurnMessage | undefined {\n if (item.type !== 'message') return undefined;\n const content = textOfMessage(item);\n if (!content) return undefined;\n const id = item.id;\n if (item.role === 'user') return { role: 'user', content, id };\n if (item.role === 'assistant') return { role: 'assistant', content, id };\n // 'system' and 'developer' both map to a Mastra system message.\n return { role: 'system', content, id };\n}\n\n/**\n * Extracts only the messages added since the agent last spoke. Used when Mastra Memory is\n * the source of truth for conversation history: prior turns are already persisted in the\n * thread, so re-sending them would duplicate history.\n *\n * Two extensions over the naive \"slice after the last assistant message\":\n *\n * - **Interrupted-turn self-heal:** when the last assistant message was cut off by barge-in\n * (`interrupted: true`), the server never persisted it — aborted runs skip persistence — so\n * its heard-only text is missing from the thread. Re-send that fragment (ordered first) this\n * turn to backfill it. It stops being \"the last assistant message\" once a full reply lands,\n * so each interrupted fragment is sent exactly once, on the following turn.\n * - **Instructions filter:** LiveKit injects the customer Agent's `instructions` as a\n * leading `system` message ({@link LIVEKIT_INSTRUCTIONS_MESSAGE_ID}); the server-side Mastra\n * agent owns its own system prompt, so drop it (it would otherwise ship on the first turn,\n * before any assistant message).\n */\nexport function extractNewTurnMessages(chatCtx: llm.ChatContext): VoiceTurnMessage[] {\n const items = chatCtx.items;\n let lastAssistantIdx = -1;\n for (let i = items.length - 1; i >= 0; i--) {\n const item = items[i];\n if (item?.type === 'message' && item.role === 'assistant') {\n lastAssistantIdx = i;\n break;\n }\n }\n const lastAssistant = lastAssistantIdx >= 0 ? items[lastAssistantIdx] : undefined;\n const healInterrupted =\n lastAssistant?.type === 'message' && lastAssistant.role === 'assistant' && lastAssistant.interrupted;\n // Include the interrupted fragment by starting the slice AT it, otherwise start strictly after.\n const startIdx = healInterrupted ? lastAssistantIdx : lastAssistantIdx + 1;\n\n const messages: VoiceTurnMessage[] = [];\n for (const item of items.slice(startIdx)) {\n if (item.type === 'message' && item.id === LIVEKIT_INSTRUCTIONS_MESSAGE_ID) continue;\n const message = toVoiceTurnMessage(item);\n if (message) messages.push(message);\n }\n return messages;\n}\n\n/**\n * Converts the full LiveKit chat context to Mastra messages. Used when the bridge runs\n * without Mastra Memory and LiveKit's in-session context is the only history. The agent's\n * LiveKit-level instructions are excluded — the Mastra agent applies its own instructions.\n */\nexport function chatContextToMessages(chatCtx: llm.ChatContext): VoiceTurnMessage[] {\n const withoutInstructions = chatCtx.copy({ excludeInstructions: true, excludeFunctionCall: true });\n const messages: VoiceTurnMessage[] = [];\n for (const item of withoutInstructions.items) {\n const message = toVoiceTurnMessage(item);\n if (message) messages.push(message);\n }\n return messages;\n}\n","import { ReadableStream } from 'node:stream/web';\nimport { llm, voice } from '@livekit/agents';\nimport type { Agent as MastraAgent, AgentExecutionOptionsBase } from '@mastra/core/agent';\nimport type { TracingContext } from '@mastra/core/observability';\nimport { RequestContext } from '@mastra/core/request-context';\nimport { chatContextToMessages, extractNewTurnMessages } from './messages';\nimport type { VoiceTurnMessage } from './messages';\n\nconst DEFAULT_INSTRUCTIONS = 'You are a helpful voice assistant powered by a Mastra agent.';\n\n/** Default spoken text for periodic AI re-disclosure. See {@link MastraVoiceAgentOptions.greetingReminder}. */\nexport const DEFAULT_DISCLOSURE_REMINDER = \"Just a reminder, you're speaking with an AI assistant.\";\n\n/**\n * Tracks periodic AI re-disclosure for a single call. `due()` returns the reminder text once\n * `everyMs` has elapsed since the last disclosure (resetting the clock), otherwise `undefined`.\n * Time is injectable so the interval logic is deterministically testable.\n */\nexport class DisclosureReminder {\n private lastAt: number;\n constructor(\n private readonly everyMs: number,\n private readonly text: string,\n now: number = Date.now(),\n ) {\n this.lastAt = now;\n }\n /** Call once per turn: the reminder text if it's due, else `undefined`. Does not reset the clock —\n * call {@link DisclosureReminder.markDelivered} once the reminder is actually threaded into the\n * outgoing reply, so a reminder that never makes it out isn't silently skipped for a full interval. */\n due(now: number = Date.now()): string | undefined {\n if (now - this.lastAt < this.everyMs) return undefined;\n return this.text;\n }\n /** Resets the clock. Call only once the reminder text from {@link due} was actually emitted. */\n markDelivered(now: number = Date.now()): void {\n this.lastAt = now;\n }\n}\n\n/**\n * Wraps `source` in a stream that emits `text` as a single leading chunk (with a trailing space, so\n * TTS pauses before the reply) before piping the rest of `source` through unchanged. Cancelling the\n * wrapper cancels `source` — so barge-in still aborts the underlying generation.\n */\nexport function prependText(source: ReadableStream<string>, text: string): ReadableStream<string> {\n const prefix = text.endsWith(' ') ? text : `${text} `;\n const reader = source.getReader();\n return new ReadableStream<string>({\n start(controller) {\n controller.enqueue(prefix);\n },\n async pull(controller) {\n try {\n const { done, value } = await reader.read();\n if (done) controller.close();\n else controller.enqueue(value);\n } catch (error) {\n controller.error(error);\n }\n },\n cancel(reason) {\n return reader.cancel(reason);\n },\n });\n}\n\nexport type MastraStreamOptions = Partial<AgentExecutionOptionsBase<unknown>>;\n\nexport interface VoiceToolCall {\n toolCallId: string;\n toolName: string;\n args?: unknown;\n}\n\n/**\n * Token usage for one turn, captured from the model's `finish` chunk. Field names mirror LiveKit's\n * `CompletionUsage` so the plugin can forward it into `metrics_collected` without remapping.\n */\nexport interface VoiceTurnUsage {\n /** Tokens in the prompt (LiveKit `promptTokens`). */\n promptTokens: number;\n /** Tokens in the completion (LiveKit `completionTokens`). */\n completionTokens: number;\n /** Cached prompt tokens (LiveKit `promptCachedTokens`). */\n promptCachedTokens: number;\n /** Total tokens for the turn. */\n totalTokens: number;\n}\n\n/**\n * Maps a Mastra `finish` chunk's usage (`payload.output.usage`, AI-SDK `LanguageModelUsage`) to the\n * LiveKit-shaped {@link VoiceTurnUsage}, or `undefined` when the chunk carries no token counts.\n * Handles both the flat V2 usage shape (`inputTokens`/`outputTokens`) and the nested V3 shape\n * (`inputTokens.total`/`outputTokens.total`).\n */\nexport function mapTurnUsage(usage: unknown): VoiceTurnUsage | undefined {\n if (!usage || typeof usage !== 'object') return undefined;\n const u = usage as Record<string, unknown>;\n // Prefer the flat V2 shape (`inputTokens: number`); fall back to the nested V3 shape\n // (`inputTokens: { total, cacheRead }`).\n const totalOf = (v: unknown): number | undefined => {\n if (typeof v === 'number') return v;\n if (v && typeof v === 'object' && typeof (v as { total?: unknown }).total === 'number') {\n return (v as { total: number }).total;\n }\n return undefined;\n };\n const cacheReadOf = (v: unknown): number | undefined =>\n v && typeof v === 'object' && typeof (v as { cacheRead?: unknown }).cacheRead === 'number'\n ? (v as { cacheRead: number }).cacheRead\n : undefined;\n\n const promptTokens = totalOf(u.inputTokens) ?? 0;\n const completionTokens = totalOf(u.outputTokens) ?? 0;\n const promptCachedTokens =\n (typeof u.cachedInputTokens === 'number' ? u.cachedInputTokens : cacheReadOf(u.inputTokens)) ?? 0;\n const totalTokens = typeof u.totalTokens === 'number' ? u.totalTokens : promptTokens + completionTokens;\n // Nothing was reported at all → treat as no usage rather than emitting an all-zero chunk.\n if (promptTokens === 0 && completionTokens === 0 && totalTokens === 0 && promptCachedTokens === 0) {\n return undefined;\n }\n return { promptTokens, completionTokens, promptCachedTokens, totalTokens };\n}\n\nexport interface MastraVoiceAgentMemory {\n thread: string;\n resource?: string;\n}\n\n/**\n * Per-turn context handed to a {@link VoiceReplyGenerator}. LiveKit calls `llmNode` once per\n * detected user turn; the bridge builds this context and asks the generator for the reply.\n */\nexport interface VoiceTurnContext {\n /**\n * The messages to generate a reply from. With Mastra Memory on, only the messages new since\n * the agent last spoke (history comes from the thread); with memory off, the full session.\n *\n * For a workflow / custom generator: pass these straight to a memory-backed `agent.stream(...,\n * { memory })` inside a step so the agent backfills history from the thread (no duplication). A\n * stateless workflow that wants the entire transcript every turn should read `chatCtx` instead\n * (e.g. `chatContextToMessages(chatCtx)`), since there is no thread to backfill from.\n */\n messages: VoiceTurnMessage[];\n /** The raw LiveKit chat context, for generators that want the full transcript or message parts. */\n chatCtx: llm.ChatContext;\n /** Resolved memory mapping for the call, or `false` when memory is disabled. */\n memory: MastraVoiceAgentMemory | false;\n /** Request context forwarded to generation. */\n requestContext?: RequestContext;\n /** Voice-call span context, so each turn's generation nests under the call trace. */\n tracingContext?: TracingContext;\n /**\n * Internal, per-turn side channel for token usage. A generator invokes this once, when the\n * `finish` chunk carries usage, so the caller (e.g. `MastraLLMStream`) can attribute usage to\n * exactly this turn — kept on the context (not on generator options) so overlapping turns from\n * preemptive generation can't misattribute usage. Fire-and-forget; the generator does not await it.\n */\n onUsage?: (usage: VoiceTurnUsage) => void;\n}\n\n/**\n * What a turn produced, handed to {@link VoiceTurnCompleteHook} after the reply finishes.\n */\nexport interface VoiceTurnResult {\n /** The assistant reply text streamed this turn, accumulated from the model's text deltas. */\n text: string;\n /** Tool calls the agent made during the turn, in order. */\n toolCalls: VoiceToolCall[];\n /** True when barge-in cut the turn short before it finished streaming. */\n interrupted: boolean;\n /** Token usage for the turn when the model reported it in its `finish` chunk. */\n usage?: VoiceTurnUsage;\n}\n\n/** {@link VoiceTurnContext} plus the reply it produced. Passed to {@link VoiceTurnCompleteHook}. */\nexport interface VoiceTurnCompleteContext extends VoiceTurnContext {\n /** The reply the agent produced this turn. */\n result: VoiceTurnResult;\n}\n\n/**\n * Called once per turn AFTER the reply has finished streaming to text-to-speech — off the audio\n * path. It runs fire-and-forget: the turn does not await it, so post-turn work (memory\n * maintenance, CRM writes, analytics) never delays what the caller hears or the next turn. A\n * thrown error or rejected promise is logged, not propagated. Because the resolved `memory`\n * mapping (`thread`/`resource`) is on the context, this is the place for a truly non-blocking\n * `memory.updateWorkingMemory(...)`. See {@link MastraVoiceAgentOptions.onTurnComplete}.\n */\nexport type VoiceTurnCompleteHook = (ctx: VoiceTurnCompleteContext) => void | Promise<void>;\n\n/**\n * Produces a stream of text deltas for one conversational turn, or `null` to stay silent.\n * Cancelling the returned stream (LiveKit does this on barge-in) must abort the underlying\n * generation. Built-in implementations: {@link createAgentReplyGenerator} (a Mastra agent) and\n * `createWorkflowReplyGenerator` (a Mastra workflow).\n */\nexport type VoiceReplyGenerator = (\n ctx: VoiceTurnContext,\n) => ReadableStream<string> | null | Promise<ReadableStream<string> | null>;\n\nexport interface AgentReplyGeneratorOptions {\n /** The Mastra agent that generates replies. Tools and memory run inside this agent. */\n agent: MastraAgent;\n /** Extra options merged into every `agent.stream()` call (e.g. `tracingContext`). */\n streamOptions?: MastraStreamOptions;\n /** Speak a short phrase while a tool call runs. See {@link MastraVoiceAgentOptions.toolFeedback}. */\n toolFeedback?: (toolCall: VoiceToolCall) => string | undefined | void;\n /** Notified as each tool-call chunk arrives, mid-stream. See {@link MastraVoiceAgentOptions.onToolCall}. */\n onToolCall?: (toolCall: VoiceToolCall) => void;\n /** Fired off the audio path after the reply streams. See {@link MastraVoiceAgentOptions.onTurnComplete}. */\n onTurnComplete?: VoiceTurnCompleteHook;\n}\n\n/**\n * A {@link VoiceReplyGenerator} backed by a Mastra agent: runs the agent's full loop (model,\n * tools, memory) and streams its text deltas. On barge-in the returned stream is cancelled,\n * which aborts the in-flight `agent.stream()`.\n */\nexport function createAgentReplyGenerator(options: AgentReplyGeneratorOptions): VoiceReplyGenerator {\n const { agent, streamOptions, toolFeedback, onToolCall, onTurnComplete } = options;\n return ctx => {\n if (ctx.messages.length === 0) return null;\n\n const abortController = new AbortController();\n const mergedOptions: MastraStreamOptions = {\n ...streamOptions,\n abortSignal: abortController.signal,\n };\n if (ctx.memory) mergedOptions.memory = ctx.memory;\n if (ctx.requestContext) mergedOptions.requestContext = ctx.requestContext;\n\n let cancelled = false;\n // Accumulated as the turn streams so the post-turn hook can see what was actually produced.\n let replyText = '';\n const toolCalls: VoiceToolCall[] = [];\n let usage: VoiceTurnUsage | undefined;\n\n // Fire-and-forget after the reply has streamed: off the audio path (the caller already heard\n // the text), and not awaited, so it never delays the next turn. Errors are logged, not thrown.\n const emitTurnComplete = (interrupted: boolean) => {\n if (!onTurnComplete) return;\n const completeCtx: VoiceTurnCompleteContext = {\n ...ctx,\n result: { text: replyText, toolCalls, interrupted, usage },\n };\n Promise.resolve()\n .then(() => onTurnComplete(completeCtx))\n .catch(error => {\n console.warn('@mastra/livekit: onTurnComplete hook threw', error);\n });\n };\n\n return new ReadableStream<string>({\n start: async controller => {\n try {\n const result = await agent.stream(ctx.messages, mergedOptions);\n for await (const chunk of result.fullStream) {\n if (cancelled) break;\n if (chunk.type === 'text-delta') {\n if (chunk.payload.text) {\n replyText += chunk.payload.text;\n controller.enqueue(chunk.payload.text);\n }\n } else if (chunk.type === 'tool-call') {\n const toolCall: VoiceToolCall = {\n toolCallId: chunk.payload.toolCallId,\n toolName: chunk.payload.toolName,\n args: chunk.payload.args,\n };\n toolCalls.push(toolCall);\n // Observer hooks are customer code: a throw must not tear down an otherwise healthy\n // reply stream (same isolation as onTurnComplete).\n try {\n onToolCall?.(toolCall);\n } catch (error) {\n console.warn('@mastra/livekit: onToolCall hook threw', error);\n }\n if (toolFeedback) {\n let filler: string | undefined | void;\n try {\n filler = toolFeedback(toolCall);\n } catch (error) {\n console.warn('@mastra/livekit: toolFeedback hook threw', error);\n }\n if (filler) controller.enqueue(filler.endsWith(' ') ? filler : `${filler} `);\n }\n } else if (chunk.type === 'finish') {\n // Usage is dropped from the spoken stream but surfaced via the per-turn side channel\n // and on the turn result, so the plugin and onTurnComplete consumers can read it.\n const output = (chunk.payload as { output?: { usage?: unknown } }).output;\n const turnUsage = mapTurnUsage(output?.usage);\n if (turnUsage) {\n usage = turnUsage;\n try {\n ctx.onUsage?.(turnUsage);\n } catch (error) {\n console.warn('@mastra/livekit: onUsage hook threw', error);\n }\n }\n } else if (chunk.type === 'error') {\n const error = chunk.payload.error;\n throw error instanceof Error ? error : new Error(String(error));\n }\n }\n if (!cancelled) controller.close();\n // Success, or a clean barge-in break out of the loop: the turn is done either way.\n emitTurnComplete(cancelled);\n } catch (error) {\n // Barge-in cancels the stream and aborts generation; that's not a failure — the turn\n // still completed (interrupted), so the hook still fires for memory reconciliation.\n if (cancelled || abortController.signal.aborted) {\n emitTurnComplete(true);\n return;\n }\n controller.error(error);\n }\n },\n cancel: () => {\n cancelled = true;\n abortController.abort();\n },\n });\n };\n}\n\nexport interface MastraVoiceAgentOptions {\n /**\n * The Mastra agent that generates replies. Tools and memory run inside this agent. Provide\n * either this or {@link MastraVoiceAgentOptions.generate}.\n */\n agent?: MastraAgent;\n /**\n * A lower-level reply generator (e.g. from `createWorkflowReplyGenerator`). Use instead of\n * `agent` to drive replies with a workflow or any custom generator.\n */\n generate?: VoiceReplyGenerator;\n /**\n * Conversation persistence. When set, only messages new since the agent last spoke are\n * sent each turn and Mastra Memory supplies history. When `false`, the full LiveKit\n * in-session context is sent on every turn instead.\n */\n memory?: MastraVoiceAgentMemory | false;\n /** Request context entries forwarded to generation. */\n requestContext?: RequestContext | Record<string, unknown>;\n /**\n * Called when the Mastra agent starts a tool call mid-reply. Return a short phrase (e.g. \"Let\n * me look that up.\") to speak it while the tool runs; it also appears in the transcript. Return\n * nothing to stay silent. Applies to the agent generator built here; the workflow generator\n * takes its own equivalent via `createWorkflowReplyGenerator`.\n */\n toolFeedback?: (toolCall: VoiceToolCall) => string | undefined | void;\n /**\n * Called as each tool call starts mid-reply (before the tool result is known), the building block\n * for tool-driven side effects — analytics, agent-initiated hang-up — without waiting for the turn\n * to finish. Runs synchronously on the stream; keep it cheap and non-throwing. Applies to the agent\n * generator built here; the workflow generator surfaces tool calls via `onTurnComplete` instead.\n */\n onToolCall?: (toolCall: VoiceToolCall) => void;\n /**\n * Called once per turn after the reply has finished streaming to text-to-speech. Runs off the\n * audio path and fire-and-forget — the turn does not await it — so post-turn memory\n * maintenance, CRM writes, or analytics never delay the caller or the next turn. The context\n * carries the produced reply ({@link VoiceTurnResult}) and the resolved `memory` mapping, so\n * this is where a truly non-blocking `memory.updateWorkingMemory(...)` belongs. A thrown error\n * or rejected promise is logged, not propagated. Applies to the agent generator built here; the\n * workflow generator takes its own via `createWorkflowReplyGenerator`.\n */\n onTurnComplete?: VoiceTurnCompleteHook;\n /**\n * Periodic AI re-disclosure. When set, once `everyMs` has elapsed since the last disclosure the\n * NEXT turn's reply is prefixed with `text` (spoken at the turn boundary, never mid-turn), so long\n * calls keep re-disclosing the AI status. Applies to the agent and workflow/custom generators. The\n * worker derives this from `configuration.greeting.repeatEvery` / `repeatText`; `text` defaults to\n * {@link DEFAULT_DISCLOSURE_REMINDER}.\n */\n greetingReminder?: { everyMs: number; text?: string };\n /** Extra options merged into every `agent.stream()` call (agent generator only). */\n streamOptions?: MastraStreamOptions;\n /** LiveKit agent instructions. Unused for reply generation (the Mastra agent/workflow applies its own). */\n instructions?: string;\n id?: voice.AgentOptions<unknown>['id'];\n stt?: voice.AgentOptions<unknown>['stt'];\n vad?: voice.AgentOptions<unknown>['vad'];\n tts?: voice.AgentOptions<unknown>['tts'];\n turnHandling?: voice.AgentOptions<unknown>['turnHandling'];\n}\n\nfunction toRequestContext(value: RequestContext | Record<string, unknown> | undefined): RequestContext | undefined {\n if (!value) return undefined;\n if (value instanceof RequestContext) return value;\n return new RequestContext<unknown>(Object.entries(value));\n}\n\n/**\n * The session only runs its cascaded reply pipeline when an `llm` instance is present —\n * `llmNode` replaces the inference step, but the gate checks `llm instanceof LLM`. This\n * placeholder satisfies the gate; the Mastra agent/workflow does the actual generation.\n */\nclass MastraPlaceholderLLM extends llm.LLM {\n label(): string {\n return 'mastra.MastraVoiceAgent';\n }\n\n override get model(): string {\n return 'mastra-agent';\n }\n\n override get provider(): string {\n return 'mastra';\n }\n\n chat(): llm.LLMStream {\n throw new Error(\n '@mastra/livekit: reply generation runs through the Mastra agent via llmNode; the placeholder LLM cannot be used for inference.',\n );\n }\n}\n\n/**\n * A LiveKit `voice.Agent` whose replies come from a Mastra agent or workflow.\n *\n * LiveKit keeps ownership of the audio loop (VAD, STT, turn detection, TTS, barge-in) and calls\n * `llmNode` once per detected user turn; the node delegates to a {@link VoiceReplyGenerator}\n * which streams text deltas back. On barge-in LiveKit cancels the returned stream, which aborts\n * the in-flight generation.\n */\nexport class MastraVoiceAgent extends voice.Agent {\n readonly mastraAgent?: MastraAgent;\n readonly memory: MastraVoiceAgentMemory | false;\n readonly requestContext?: RequestContext;\n readonly streamOptions?: MastraStreamOptions;\n private readonly replyGenerator: VoiceReplyGenerator;\n private readonly reminder?: DisclosureReminder;\n\n constructor(options: MastraVoiceAgentOptions) {\n if (options.agent && options.generate) {\n throw new Error(\n '@mastra/livekit: MastraVoiceAgent requires `agent` or `generate`, not both — they are mutually exclusive reply sources.',\n );\n }\n super({\n id: options.id,\n instructions: options.instructions ?? DEFAULT_INSTRUCTIONS,\n stt: options.stt,\n vad: options.vad,\n llm: new MastraPlaceholderLLM(),\n tts: options.tts,\n turnHandling: options.turnHandling,\n });\n this.memory = options.memory ?? false;\n this.requestContext = toRequestContext(options.requestContext);\n this.streamOptions = options.streamOptions;\n if (options.greetingReminder) {\n this.reminder = new DisclosureReminder(\n options.greetingReminder.everyMs,\n options.greetingReminder.text?.trim() || DEFAULT_DISCLOSURE_REMINDER,\n );\n }\n\n if (options.generate) {\n this.replyGenerator = options.generate;\n } else if (options.agent) {\n this.mastraAgent = options.agent;\n this.replyGenerator = createAgentReplyGenerator({\n agent: options.agent,\n streamOptions: options.streamOptions,\n toolFeedback: options.toolFeedback,\n onToolCall: options.onToolCall,\n onTurnComplete: options.onTurnComplete,\n });\n } else {\n throw new Error('@mastra/livekit: MastraVoiceAgent requires `agent` or `generate`.');\n }\n }\n\n override async llmNode(\n chatCtx: llm.ChatContext,\n _toolCtx: llm.ToolContext,\n _modelSettings: voice.ModelSettings,\n ): Promise<ReadableStream<llm.ChatChunk | string> | null> {\n const messages: VoiceTurnMessage[] =\n this.memory === false ? chatContextToMessages(chatCtx) : extractNewTurnMessages(chatCtx);\n if (messages.length === 0) return null;\n\n const reply = await this.replyGenerator({\n messages,\n chatCtx,\n memory: this.memory,\n requestContext: this.requestContext,\n tracingContext: this.streamOptions?.tracingContext,\n });\n if (!reply) return null;\n\n // Periodic AI re-disclosure: when the interval has elapsed, prefix this turn's spoken reply with\n // the reminder. Done at the turn boundary (never mid-turn), riding the same stream so barge-in\n // cancellation still propagates to the underlying generation. The clock only resets once the\n // reminder is actually threaded into the outgoing reply below, not just because it was due.\n // KNOWN LIMIT: \"threaded into the reply\" is stream-build time, not playout. Under LiveKit's\n // preemptive generation a discarded speculative reply still resets the clock, so the next real\n // turn can miss its reminder — hence the documented repeatEvery/preemptiveGeneration\n // incompatibility. A playout-accurate reset needs a confirmed-turn signal llmNode doesn't have.\n const reminder = this.reminder?.due();\n if (!reminder) return reply;\n this.reminder?.markDelivered();\n return prependText(reply, reminder);\n }\n}\n\nexport function createMastraVoiceAgent(options: MastraVoiceAgentOptions): MastraVoiceAgent {\n return new MastraVoiceAgent(options);\n}\n","import { ReadableStream } from 'node:stream/web';\nimport { APIConnectionError, APIError, APIStatusError, APITimeoutError } from '@livekit/agents';\nimport { RequestContext } from '@mastra/core/request-context';\nimport { mapTurnUsage } from './bridge';\nimport type {\n VoiceReplyGenerator,\n VoiceToolCall,\n VoiceTurnCompleteContext,\n VoiceTurnCompleteHook,\n VoiceTurnUsage,\n} from './bridge';\n\nconst DEFAULT_API_PREFIX = '/api';\n/** Connect + first-token budget when not overridden. Plugin mode passes `connOptions.timeoutMs`. */\nexport const DEFAULT_REMOTE_TIMEOUT_MS = 10_000;\n/** Standalone initial-connection retry attempts. Plugin mode forces this to 0 (base class owns retries). */\nexport const DEFAULT_REMOTE_RETRIES = 2;\n\n/** Thrown (as a plain, non-retryable error) when the server emits a chunk that needs client action. */\nconst HITL_UNSUPPORTED_MESSAGE =\n '@mastra/livekit: the agent requested tool approval or suspended a tool call; human-in-the-loop ' +\n 'flows (approve-tool-call / resume-stream) are not supported on the voice path. Remove requireApproval ' +\n 'or suspend from the tools this agent uses on voice calls.';\n\n/**\n * Options for the remote Mastra transport. Shape mirrors the in-process `AgentReplyGeneratorOptions`\n * so `MastraLLM` can accept either source interchangeably.\n */\nexport interface RemoteMastraAgentOptions {\n /** Base URL of the remote Mastra server, e.g. `https://my-app.mastra.cloud`. */\n baseUrl: string;\n /** Agent key in the Mastra config's `agents`. */\n agentId: string;\n /** Path prefix for the Mastra API. Defaults to `'/api'`. */\n apiPrefix?: string;\n /** Static headers, or a (possibly async) resolver invoked per turn — e.g. to mint a fresh token. */\n headers?: Record<string, string> | (() => Record<string, string> | Promise<Record<string, string>>);\n /** Injectable `fetch` for tests/proxies. Defaults to `globalThis.fetch`. */\n fetch?: typeof globalThis.fetch;\n /**\n * Connect + first-token timeout in ms. Plugin mode default: LiveKit's `connOptions.timeoutMs` (10s).\n * Standalone default: {@link DEFAULT_REMOTE_TIMEOUT_MS}.\n */\n timeoutMs?: number;\n /**\n * Initial-connection retry attempts (before the first chunk only). Standalone default:\n * {@link DEFAULT_REMOTE_RETRIES}. In plugin mode the LiveKit base class owns retries and this is\n * forced to 0.\n */\n retries?: number;\n /** Extra fields merged into each stream request body (advanced). */\n body?: Record<string, unknown>;\n}\n\n/** {@link RemoteMastraAgentOptions} plus the per-turn observer hooks the generator threads through. */\nexport interface RemoteAgentReplyGeneratorOptions extends RemoteMastraAgentOptions {\n /** Speak a short phrase while a tool runs. See {@link MastraVoiceAgentOptions.toolFeedback}. */\n toolFeedback?: (toolCall: VoiceToolCall) => string | undefined | void;\n /** Notified as each tool-call chunk arrives, mid-stream. See {@link MastraVoiceAgentOptions.onToolCall}. */\n onToolCall?: (toolCall: VoiceToolCall) => void;\n /** Fired off the audio path after the reply streams. See {@link MastraVoiceAgentOptions.onTurnComplete}. */\n onTurnComplete?: VoiceTurnCompleteHook;\n}\n\ntype RawChunk = { type?: string; payload?: Record<string, unknown> };\n\nfunction trimTrailingSlash(url: string): string {\n return url.endsWith('/') ? url.slice(0, -1) : url;\n}\n\nfunction toMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nasync function resolveHeaders(headers: RemoteMastraAgentOptions['headers']): Promise<Record<string, string>> {\n if (!headers) return {};\n if (typeof headers === 'function') return (await headers()) ?? {};\n return headers;\n}\n\nfunction serializeRequestContext(\n requestContext: RequestContext | Record<string, unknown> | undefined,\n): Record<string, unknown> | undefined {\n if (!requestContext) return undefined;\n // Mirror client-js `parseClientRequestContext`.\n if (requestContext instanceof RequestContext) return Object.fromEntries(requestContext.entries());\n return requestContext;\n}\n\nasync function safeReadBody(response: Response): Promise<object | null> {\n try {\n const text = await response.text();\n if (!text) return null;\n try {\n const parsed: unknown = JSON.parse(text);\n return parsed && typeof parsed === 'object' ? (parsed as object) : { message: String(parsed) };\n } catch {\n return { message: text };\n }\n } catch {\n return null;\n }\n}\n\n/**\n * Reads a Mastra SSE stream and yields each event's parsed JSON. Framing matches the server's\n * `processMastraStream` (buffer, split on `\\n\\n`, strip `data: `, stop on `[DONE]`); undecodable\n * `data:` lines are skipped. Aborting `signal` cancels the underlying reader.\n */\nexport async function* readMastraSSE(\n body: globalThis.ReadableStream<Uint8Array>,\n signal: AbortSignal,\n): AsyncGenerator<RawChunk> {\n const reader = body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n const onAbort = () => void reader.cancel().catch(() => {});\n if (signal.aborted) {\n void reader.cancel().catch(() => {});\n return;\n }\n signal.addEventListener('abort', onAbort, { once: true });\n try {\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n const events = buffer.split('\\n\\n');\n buffer = events.pop() ?? '';\n for (const event of events) {\n if (!event.startsWith('data:')) continue;\n const data = event.slice(event.startsWith('data: ') ? 6 : 5).trim();\n if (data === '[DONE]') return;\n if (!data) continue;\n let json: unknown;\n try {\n json = JSON.parse(data);\n } catch {\n continue; // tolerate a stray non-JSON line\n }\n if (json && typeof json === 'object') yield json as RawChunk;\n }\n }\n } finally {\n signal.removeEventListener('abort', onAbort);\n try {\n reader.releaseLock();\n } catch {\n // A read may still be pending when the fetch was aborted; the stream is torn down anyway.\n }\n }\n}\n\n/**\n * A {@link VoiceReplyGenerator} that runs the Mastra agent loop on a **remote** Mastra server over\n * HTTP/SSE. Shaped exactly like the in-process `createAgentReplyGenerator`: it consumes the same\n * chunk vocabulary, drives the same `toolFeedback` / `onToolCall` / `onTurnComplete` seams, and\n * cancelling the returned stream (LiveKit does this on barge-in) tears down the HTTP request so the\n * server aborts generation.\n *\n * Usable standalone via the worker's `generate:` hatch (a minimum-viable remote worker mode), and as\n * the transport `MastraLLM` wraps. Errors are thrown as LiveKit `APIError` subclasses so the plugin's\n * base-class retry loop and `FallbackAdapter` behave; a connect + first-token watchdog prevents\n * indefinite dead air.\n */\nexport function createRemoteAgentReplyGenerator(options: RemoteAgentReplyGeneratorOptions): VoiceReplyGenerator {\n const {\n baseUrl,\n agentId,\n apiPrefix = DEFAULT_API_PREFIX,\n headers,\n fetch: fetchImpl = globalThis.fetch,\n timeoutMs = DEFAULT_REMOTE_TIMEOUT_MS,\n retries = DEFAULT_REMOTE_RETRIES,\n body: extraBody,\n toolFeedback,\n onToolCall,\n onTurnComplete,\n } = options;\n\n if (!fetchImpl) {\n throw new Error('@mastra/livekit: no fetch implementation available; pass `fetch` or run on Node ≥ 22.');\n }\n const url = `${trimTrailingSlash(baseUrl)}${apiPrefix}/agents/${agentId}/stream`;\n\n return ctx => {\n if (ctx.messages.length === 0) return null;\n\n // Reassigned per retry attempt (see the loop below) so a watchdog abort on one attempt can't\n // poison the next; `cancel()` always aborts whichever attempt is currently in flight.\n let currentAbortController: AbortController | undefined;\n let cancelled = false;\n // Accumulated as the turn streams so the post-turn hook sees what was actually produced.\n let replyText = '';\n const toolCalls: VoiceToolCall[] = [];\n let usage: VoiceTurnUsage | undefined;\n\n const emitTurnComplete = (interrupted: boolean) => {\n if (!onTurnComplete) return;\n const completeCtx: VoiceTurnCompleteContext = {\n ...ctx,\n result: { text: replyText, toolCalls, interrupted, usage },\n };\n Promise.resolve()\n .then(() => onTurnComplete(completeCtx))\n .catch(error => {\n console.warn('@mastra/livekit: onTurnComplete hook threw', error);\n });\n };\n\n const requestBody: Record<string, unknown> = {\n messages: ctx.messages,\n // The server schema requires a resource when memory is present; default it to the thread id,\n // matching the worker's own thread bootstrap.\n memory: ctx.memory\n ? { thread: ctx.memory.thread, resource: ctx.memory.resource ?? ctx.memory.thread }\n : undefined,\n requestContext: serializeRequestContext(ctx.requestContext),\n ...extraBody,\n };\n\n return new ReadableStream<string>({\n start: async controller => {\n // `retryable` is the LiveKit contract flag: true only before the first chunk is emitted, so a\n // voice turn is never replayed half-heard. It also gates the standalone connect-retry.\n let retryable = true;\n try {\n for (let attempt = 0; ; attempt++) {\n // A fresh controller per attempt: reusing one across retries meant a watchdog abort on an\n // earlier attempt left every subsequent attempt's fetch already-aborted before it started.\n const abortController = new AbortController();\n currentAbortController = abortController;\n let timedOut = false;\n let watchdog: ReturnType<typeof setTimeout> | undefined;\n const clearWatchdog = () => {\n if (watchdog) {\n clearTimeout(watchdog);\n watchdog = undefined;\n }\n };\n try {\n watchdog = setTimeout(() => {\n timedOut = true;\n abortController.abort();\n }, timeoutMs);\n (watchdog as { unref?: () => void }).unref?.();\n\n const resolvedHeaders = await resolveHeaders(headers);\n if (cancelled) {\n clearWatchdog();\n break;\n }\n const response = await fetchImpl(url, {\n method: 'POST',\n headers: { 'content-type': 'application/json', accept: 'text/event-stream', ...resolvedHeaders },\n body: JSON.stringify(requestBody),\n signal: abortController.signal,\n });\n if (!response.ok) {\n const errorBody = await safeReadBody(response);\n throw new APIStatusError({\n message: `@mastra/livekit: Mastra agent stream request failed with status ${response.status}`,\n options: { statusCode: response.status, body: errorBody, retryable },\n });\n }\n if (!response.body) {\n throw new APIConnectionError({\n message: '@mastra/livekit: Mastra agent stream returned an empty response body',\n options: { retryable },\n });\n }\n\n for await (const chunk of readMastraSSE(\n response.body as unknown as globalThis.ReadableStream<Uint8Array>,\n abortController.signal,\n )) {\n if (cancelled) break;\n // First chunk: the server has committed to this generation — forbid any further\n // retry so the turn can't be replayed mid-stream. The watchdog is NOT cleared here:\n // lifecycle metadata (step-start, text-start, ...) isn't proof the model is\n // producing anything, and disarming on it would turn a post-metadata stall into\n // indefinite dead air. It disarms on the first sign of model output below.\n retryable = false;\n const payload = chunk.payload ?? {};\n switch (chunk.type) {\n case 'text-delta': {\n const text = payload.text;\n if (typeof text === 'string' && text) {\n clearWatchdog();\n replyText += text;\n controller.enqueue(text);\n }\n break;\n }\n case 'tool-call': {\n // A tool call is first-token progress too — the model committed to a tool run,\n // which may legitimately outlast the connect budget before any text streams.\n clearWatchdog();\n const toolCall: VoiceToolCall = {\n toolCallId: String(payload.toolCallId ?? ''),\n toolName: String(payload.toolName ?? ''),\n args: payload.args,\n };\n toolCalls.push(toolCall);\n // Observer hooks are customer code: a throw must not tear down an otherwise\n // healthy reply stream (same isolation as onTurnComplete).\n try {\n onToolCall?.(toolCall);\n } catch (error) {\n console.warn('@mastra/livekit: onToolCall hook threw', error);\n }\n if (toolFeedback) {\n let filler: string | undefined | void;\n try {\n filler = toolFeedback(toolCall);\n } catch (error) {\n console.warn('@mastra/livekit: toolFeedback hook threw', error);\n }\n if (filler) controller.enqueue(filler.endsWith(' ') ? filler : `${filler} `);\n }\n break;\n }\n case 'finish': {\n clearWatchdog();\n const output = payload.output as { usage?: unknown } | undefined;\n const turnUsage = mapTurnUsage(output?.usage);\n if (turnUsage) {\n usage = turnUsage;\n try {\n ctx.onUsage?.(turnUsage);\n } catch (error) {\n console.warn('@mastra/livekit: onUsage hook threw', error);\n }\n }\n break;\n }\n case 'tool-call-approval':\n case 'tool-call-suspended':\n throw new Error(HITL_UNSUPPORTED_MESSAGE);\n case 'error': {\n const error = payload.error;\n throw error instanceof Error ? error : new Error(String(error));\n }\n default:\n break; // ignore everything else (text-start, step-start, tool-result, ...)\n }\n }\n clearWatchdog();\n break; // success, or a clean barge-in break out of the SSE loop\n } catch (error) {\n clearWatchdog();\n if (cancelled) break; // barge-in: not a failure, emit interrupted below\n\n // Classify into the LiveKit error vocabulary. Application errors thrown after streaming\n // began (an `error` chunk, a HITL chunk) are not connection failures — propagate as-is.\n let typed: APIError;\n if (timedOut) {\n typed = new APITimeoutError({ options: { retryable } });\n } else if (error instanceof APIError) {\n typed = error;\n } else if (retryable) {\n typed = new APIConnectionError({ message: toMessage(error), options: { retryable } });\n } else {\n throw error;\n }\n if (typed.retryable && attempt < retries) continue;\n throw typed;\n }\n }\n if (!cancelled) controller.close();\n // Success or clean barge-in: the turn is done either way.\n emitTurnComplete(cancelled);\n } catch (error) {\n // Barge-in never reaches here (handled above); a real failure errors the stream and does\n // not fire onTurnComplete — same contract as the in-process generator.\n if (cancelled) {\n emitTurnComplete(true);\n return;\n }\n controller.error(error);\n }\n },\n cancel: () => {\n cancelled = true;\n currentAbortController?.abort();\n },\n });\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"remote-D7n50m8S.js","names":[],"sources":["../src/messages.ts","../src/bridge.ts","../src/remote.ts"],"sourcesContent":["import type { llm } from '@livekit/agents';\n\n/**\n * Fixed id LiveKit gives the customer Agent's instructions when it injects them as a leading\n * `role: 'system'` message into the chat context passed to `chat()` / `llmNode`. We drop this\n * item so the server-side Mastra agent's own system prompt is authoritative.\n */\nexport const LIVEKIT_INSTRUCTIONS_MESSAGE_ID = 'lk.agent_task.instructions';\n\n/**\n * A message bound for `agent.stream(...)` (in-process) or the Mastra server stream route (remote).\n * `id` carries the LiveKit `ChatMessage.id` so the server can dedupe/upsert by id — making\n * base-class retries, preemptive double-sends, and the interrupted-turn reconciliation recipe idempotent.\n */\nexport type VoiceTurnMessage =\n | { role: 'system'; content: string; id?: string }\n | { role: 'user'; content: string; id?: string }\n | { role: 'assistant'; content: string; id?: string };\n\nfunction textOfMessage(message: llm.ChatMessage): string {\n const parts: string[] = [];\n for (const part of message.content) {\n if (typeof part === 'string') {\n parts.push(part);\n } else if (part.type === 'instructions') {\n parts.push(part.value);\n } else if (part.type === 'audio_content' && part.transcript) {\n parts.push(part.transcript);\n }\n }\n return parts.join('\\n').trim();\n}\n\nfunction toVoiceTurnMessage(item: llm.ChatItem): VoiceTurnMessage | undefined {\n if (item.type !== 'message') return undefined;\n const content = textOfMessage(item);\n if (!content) return undefined;\n const id = item.id;\n if (item.role === 'user') return { role: 'user', content, id };\n if (item.role === 'assistant') return { role: 'assistant', content, id };\n // 'system' and 'developer' both map to a Mastra system message.\n return { role: 'system', content, id };\n}\n\n/**\n * Extracts only the messages added since the agent last spoke. Used when Mastra Memory is\n * the source of truth for conversation history: prior turns are already persisted in the\n * thread, so re-sending them would duplicate history.\n *\n * Two extensions over the naive \"slice after the last assistant message\":\n *\n * - **Interrupted-turn self-heal:** when the last assistant message was cut off by barge-in\n * (`interrupted: true`), the server never persisted it — aborted runs skip persistence — so\n * its heard-only text is missing from the thread. Re-send that fragment (ordered first) this\n * turn to backfill it. It stops being \"the last assistant message\" once a full reply lands,\n * so each interrupted fragment is sent exactly once, on the following turn.\n * - **Instructions filter:** LiveKit injects the customer Agent's `instructions` as a\n * leading `system` message ({@link LIVEKIT_INSTRUCTIONS_MESSAGE_ID}); the server-side Mastra\n * agent owns its own system prompt, so drop it (it would otherwise ship on the first turn,\n * before any assistant message).\n */\nexport function extractNewTurnMessages(chatCtx: llm.ChatContext): VoiceTurnMessage[] {\n const items = chatCtx.items;\n let lastAssistantIdx = -1;\n for (let i = items.length - 1; i >= 0; i--) {\n const item = items[i];\n if (item?.type === 'message' && item.role === 'assistant') {\n lastAssistantIdx = i;\n break;\n }\n }\n const lastAssistant = lastAssistantIdx >= 0 ? items[lastAssistantIdx] : undefined;\n const healInterrupted =\n lastAssistant?.type === 'message' && lastAssistant.role === 'assistant' && lastAssistant.interrupted;\n // Include the interrupted fragment by starting the slice AT it, otherwise start strictly after.\n const startIdx = healInterrupted ? lastAssistantIdx : lastAssistantIdx + 1;\n\n const messages: VoiceTurnMessage[] = [];\n for (const item of items.slice(startIdx)) {\n if (item.type === 'message' && item.id === LIVEKIT_INSTRUCTIONS_MESSAGE_ID) continue;\n const message = toVoiceTurnMessage(item);\n if (message) messages.push(message);\n }\n return messages;\n}\n\n/**\n * Converts the full LiveKit chat context to Mastra messages. Used when the bridge runs\n * without Mastra Memory and LiveKit's in-session context is the only history. The agent's\n * LiveKit-level instructions are excluded — the Mastra agent applies its own instructions.\n */\nexport function chatContextToMessages(chatCtx: llm.ChatContext): VoiceTurnMessage[] {\n const withoutInstructions = chatCtx.copy({ excludeInstructions: true, excludeFunctionCall: true });\n const messages: VoiceTurnMessage[] = [];\n for (const item of withoutInstructions.items) {\n const message = toVoiceTurnMessage(item);\n if (message) messages.push(message);\n }\n return messages;\n}\n","import { ReadableStream } from 'node:stream/web';\nimport { llm, voice } from '@livekit/agents';\nimport type { Agent as MastraAgent, AgentExecutionOptionsBase } from '@mastra/core/agent';\nimport type { TracingContext } from '@mastra/core/observability';\nimport { RequestContext } from '@mastra/core/request-context';\nimport { chatContextToMessages, extractNewTurnMessages } from './messages';\nimport type { VoiceTurnMessage } from './messages';\n\nconst DEFAULT_INSTRUCTIONS = 'You are a helpful voice assistant powered by a Mastra agent.';\n\n/** Default spoken text for periodic AI re-disclosure. See {@link MastraVoiceAgentOptions.greetingReminder}. */\nexport const DEFAULT_DISCLOSURE_REMINDER = \"Just a reminder, you're speaking with an AI assistant.\";\n\n/**\n * Tracks periodic AI re-disclosure for a single call. `due()` returns the reminder text once\n * `everyMs` has elapsed since the last disclosure (resetting the clock), otherwise `undefined`.\n * Time is injectable so the interval logic is deterministically testable.\n */\nexport class DisclosureReminder {\n private lastAt: number;\n constructor(\n private readonly everyMs: number,\n private readonly text: string,\n now: number = Date.now(),\n ) {\n this.lastAt = now;\n }\n /** Call once per turn: the reminder text if it's due, else `undefined`. Does not reset the clock —\n * call {@link DisclosureReminder.markDelivered} once the reminder is actually threaded into the\n * outgoing reply, so a reminder that never makes it out isn't silently skipped for a full interval. */\n due(now: number = Date.now()): string | undefined {\n if (now - this.lastAt < this.everyMs) return undefined;\n return this.text;\n }\n /** Resets the clock. Call only once the reminder text from {@link due} was actually emitted. */\n markDelivered(now: number = Date.now()): void {\n this.lastAt = now;\n }\n}\n\n/**\n * Wraps `source` in a stream that emits `text` as a single leading chunk (with a trailing space, so\n * TTS pauses before the reply) before piping the rest of `source` through unchanged. Cancelling the\n * wrapper cancels `source` — so barge-in still aborts the underlying generation.\n */\nexport function prependText(source: ReadableStream<string>, text: string): ReadableStream<string> {\n const prefix = text.endsWith(' ') ? text : `${text} `;\n const reader = source.getReader();\n return new ReadableStream<string>({\n start(controller) {\n controller.enqueue(prefix);\n },\n async pull(controller) {\n try {\n const { done, value } = await reader.read();\n if (done) controller.close();\n else controller.enqueue(value);\n } catch (error) {\n controller.error(error);\n }\n },\n cancel(reason) {\n return reader.cancel(reason);\n },\n });\n}\n\nexport type MastraStreamOptions = Partial<AgentExecutionOptionsBase<unknown>>;\n\nexport interface VoiceToolCall {\n toolCallId: string;\n toolName: string;\n args?: unknown;\n}\n\n/**\n * Token usage for one turn, captured from the model's `finish` chunk. Field names mirror LiveKit's\n * `CompletionUsage` so the plugin can forward it into `metrics_collected` without remapping.\n */\nexport interface VoiceTurnUsage {\n /** Tokens in the prompt (LiveKit `promptTokens`). */\n promptTokens: number;\n /** Tokens in the completion (LiveKit `completionTokens`). */\n completionTokens: number;\n /** Cached prompt tokens (LiveKit `promptCachedTokens`). */\n promptCachedTokens: number;\n /** Total tokens for the turn. */\n totalTokens: number;\n}\n\n/**\n * Maps a Mastra `finish` chunk's usage (`payload.output.usage`, AI-SDK `LanguageModelUsage`) to the\n * LiveKit-shaped {@link VoiceTurnUsage}, or `undefined` when the chunk carries no token counts.\n * Handles both the flat V2 usage shape (`inputTokens`/`outputTokens`) and the nested V3 shape\n * (`inputTokens.total`/`outputTokens.total`).\n */\nexport function mapTurnUsage(usage: unknown): VoiceTurnUsage | undefined {\n if (!usage || typeof usage !== 'object') return undefined;\n const u = usage as Record<string, unknown>;\n // Prefer the flat V2 shape (`inputTokens: number`); fall back to the nested V3 shape\n // (`inputTokens: { total, cacheRead }`).\n const totalOf = (v: unknown): number | undefined => {\n if (typeof v === 'number') return v;\n if (v && typeof v === 'object' && typeof (v as { total?: unknown }).total === 'number') {\n return (v as { total: number }).total;\n }\n return undefined;\n };\n const cacheReadOf = (v: unknown): number | undefined =>\n v && typeof v === 'object' && typeof (v as { cacheRead?: unknown }).cacheRead === 'number'\n ? (v as { cacheRead: number }).cacheRead\n : undefined;\n\n const promptTokens = totalOf(u.inputTokens) ?? 0;\n const completionTokens = totalOf(u.outputTokens) ?? 0;\n const promptCachedTokens =\n (typeof u.cachedInputTokens === 'number' ? u.cachedInputTokens : cacheReadOf(u.inputTokens)) ?? 0;\n const totalTokens = typeof u.totalTokens === 'number' ? u.totalTokens : promptTokens + completionTokens;\n // Nothing was reported at all → treat as no usage rather than emitting an all-zero chunk.\n if (promptTokens === 0 && completionTokens === 0 && totalTokens === 0 && promptCachedTokens === 0) {\n return undefined;\n }\n return { promptTokens, completionTokens, promptCachedTokens, totalTokens };\n}\n\nexport interface MastraVoiceAgentMemory {\n thread: string;\n resource?: string;\n}\n\n/**\n * Per-turn context handed to a {@link VoiceReplyGenerator}. LiveKit calls `llmNode` once per\n * detected user turn; the bridge builds this context and asks the generator for the reply.\n */\nexport interface VoiceTurnContext {\n /**\n * The messages to generate a reply from. With Mastra Memory on, only the messages new since\n * the agent last spoke (history comes from the thread); with memory off, the full session.\n *\n * For a workflow / custom generator: pass these straight to a memory-backed `agent.stream(...,\n * { memory })` inside a step so the agent backfills history from the thread (no duplication). A\n * stateless workflow that wants the entire transcript every turn should read `chatCtx` instead\n * (e.g. `chatContextToMessages(chatCtx)`), since there is no thread to backfill from.\n */\n messages: VoiceTurnMessage[];\n /** The raw LiveKit chat context, for generators that want the full transcript or message parts. */\n chatCtx: llm.ChatContext;\n /** Resolved memory mapping for the call, or `false` when memory is disabled. */\n memory: MastraVoiceAgentMemory | false;\n /** Request context forwarded to generation. */\n requestContext?: RequestContext;\n /** Voice-call span context, so each turn's generation nests under the call trace. */\n tracingContext?: TracingContext;\n /**\n * Internal, per-turn side channel for token usage. A generator invokes this once, when the\n * `finish` chunk carries usage, so the caller (e.g. `MastraLLMStream`) can attribute usage to\n * exactly this turn — kept on the context (not on generator options) so overlapping turns from\n * preemptive generation can't misattribute usage. Fire-and-forget; the generator does not await it.\n */\n onUsage?: (usage: VoiceTurnUsage) => void;\n}\n\n/**\n * What a turn produced, handed to {@link VoiceTurnCompleteHook} after the reply finishes.\n */\nexport interface VoiceTurnResult {\n /** The assistant reply text streamed this turn, accumulated from the model's text deltas. */\n text: string;\n /** Tool calls the agent made during the turn, in order. */\n toolCalls: VoiceToolCall[];\n /** True when barge-in cut the turn short before it finished streaming. */\n interrupted: boolean;\n /** Token usage for the turn when the model reported it in its `finish` chunk. */\n usage?: VoiceTurnUsage;\n}\n\n/** {@link VoiceTurnContext} plus the reply it produced. Passed to {@link VoiceTurnCompleteHook}. */\nexport interface VoiceTurnCompleteContext extends VoiceTurnContext {\n /** The reply the agent produced this turn. */\n result: VoiceTurnResult;\n}\n\n/**\n * Called once per turn AFTER the reply has finished streaming to text-to-speech — off the audio\n * path. It runs fire-and-forget: the turn does not await it, so post-turn work (memory\n * maintenance, CRM writes, analytics) never delays what the caller hears or the next turn. A\n * thrown error or rejected promise is logged, not propagated. Because the resolved `memory`\n * mapping (`thread`/`resource`) is on the context, this is the place for a truly non-blocking\n * `memory.updateWorkingMemory(...)`. See {@link MastraVoiceAgentOptions.onTurnComplete}.\n */\nexport type VoiceTurnCompleteHook = (ctx: VoiceTurnCompleteContext) => void | Promise<void>;\n\n/**\n * Produces a stream of text deltas for one conversational turn, or `null` to stay silent.\n * Cancelling the returned stream (LiveKit does this on barge-in) must abort the underlying\n * generation. Built-in implementations: {@link createAgentReplyGenerator} (a Mastra agent) and\n * `createWorkflowReplyGenerator` (a Mastra workflow).\n */\nexport type VoiceReplyGenerator = (\n ctx: VoiceTurnContext,\n) => ReadableStream<string> | null | Promise<ReadableStream<string> | null>;\n\nexport interface AgentReplyGeneratorOptions {\n /** The Mastra agent that generates replies. Tools and memory run inside this agent. */\n agent: MastraAgent;\n /** Extra options merged into every `agent.stream()` call (e.g. `tracingContext`). */\n streamOptions?: MastraStreamOptions;\n /** Speak a short phrase while a tool call runs. See {@link MastraVoiceAgentOptions.toolFeedback}. */\n toolFeedback?: (toolCall: VoiceToolCall) => string | undefined | void;\n /** Notified as each tool-call chunk arrives, mid-stream. See {@link MastraVoiceAgentOptions.onToolCall}. */\n onToolCall?: (toolCall: VoiceToolCall) => void;\n /** Fired off the audio path after the reply streams. See {@link MastraVoiceAgentOptions.onTurnComplete}. */\n onTurnComplete?: VoiceTurnCompleteHook;\n}\n\n/**\n * A {@link VoiceReplyGenerator} backed by a Mastra agent: runs the agent's full loop (model,\n * tools, memory) and streams its text deltas. On barge-in the returned stream is cancelled,\n * which aborts the in-flight `agent.stream()`.\n */\nexport function createAgentReplyGenerator(options: AgentReplyGeneratorOptions): VoiceReplyGenerator {\n const { agent, streamOptions, toolFeedback, onToolCall, onTurnComplete } = options;\n return ctx => {\n if (ctx.messages.length === 0) return null;\n\n const abortController = new AbortController();\n const mergedOptions: MastraStreamOptions = {\n ...streamOptions,\n abortSignal: abortController.signal,\n };\n if (ctx.memory) mergedOptions.memory = ctx.memory;\n if (ctx.requestContext) mergedOptions.requestContext = ctx.requestContext;\n\n let cancelled = false;\n // Accumulated as the turn streams so the post-turn hook can see what was actually produced.\n let replyText = '';\n const toolCalls: VoiceToolCall[] = [];\n let usage: VoiceTurnUsage | undefined;\n\n // Fire-and-forget after the reply has streamed: off the audio path (the caller already heard\n // the text), and not awaited, so it never delays the next turn. Errors are logged, not thrown.\n const emitTurnComplete = (interrupted: boolean) => {\n if (!onTurnComplete) return;\n const completeCtx: VoiceTurnCompleteContext = {\n ...ctx,\n result: { text: replyText, toolCalls, interrupted, usage },\n };\n Promise.resolve()\n .then(() => onTurnComplete(completeCtx))\n .catch(error => {\n console.warn('@mastra/livekit: onTurnComplete hook threw', error);\n });\n };\n\n return new ReadableStream<string>({\n start: async controller => {\n try {\n const result = await agent.stream(ctx.messages, mergedOptions);\n for await (const chunk of result.fullStream) {\n if (cancelled) break;\n if (chunk.type === 'text-delta') {\n if (chunk.payload.text) {\n replyText += chunk.payload.text;\n controller.enqueue(chunk.payload.text);\n }\n } else if (chunk.type === 'tool-call') {\n const toolCall: VoiceToolCall = {\n toolCallId: chunk.payload.toolCallId,\n toolName: chunk.payload.toolName,\n args: chunk.payload.args,\n };\n toolCalls.push(toolCall);\n // Observer hooks are customer code: a throw must not tear down an otherwise healthy\n // reply stream (same isolation as onTurnComplete).\n try {\n onToolCall?.(toolCall);\n } catch (error) {\n console.warn('@mastra/livekit: onToolCall hook threw', error);\n }\n if (toolFeedback) {\n let filler: string | undefined | void;\n try {\n filler = toolFeedback(toolCall);\n } catch (error) {\n console.warn('@mastra/livekit: toolFeedback hook threw', error);\n }\n if (filler) controller.enqueue(filler.endsWith(' ') ? filler : `${filler} `);\n }\n } else if (chunk.type === 'finish') {\n // Usage is dropped from the spoken stream but surfaced via the per-turn side channel\n // and on the turn result, so the plugin and onTurnComplete consumers can read it.\n const output = (chunk.payload as { output?: { usage?: unknown } }).output;\n const turnUsage = mapTurnUsage(output?.usage);\n if (turnUsage) {\n usage = turnUsage;\n try {\n ctx.onUsage?.(turnUsage);\n } catch (error) {\n console.warn('@mastra/livekit: onUsage hook threw', error);\n }\n }\n } else if (chunk.type === 'error') {\n const error = chunk.payload.error;\n throw error instanceof Error ? error : new Error(String(error));\n }\n }\n if (!cancelled) controller.close();\n // Success, or a clean barge-in break out of the loop: the turn is done either way.\n emitTurnComplete(cancelled);\n } catch (error) {\n // Barge-in cancels the stream and aborts generation; that's not a failure — the turn\n // still completed (interrupted), so the hook still fires for memory reconciliation.\n if (cancelled || abortController.signal.aborted) {\n emitTurnComplete(true);\n return;\n }\n controller.error(error);\n }\n },\n cancel: () => {\n cancelled = true;\n abortController.abort();\n },\n });\n };\n}\n\nexport interface MastraVoiceAgentOptions {\n /**\n * The Mastra agent that generates replies. Tools and memory run inside this agent. Provide\n * either this or {@link MastraVoiceAgentOptions.generate}.\n */\n agent?: MastraAgent;\n /**\n * A lower-level reply generator (e.g. from `createWorkflowReplyGenerator`). Use instead of\n * `agent` to drive replies with a workflow or any custom generator.\n */\n generate?: VoiceReplyGenerator;\n /**\n * Conversation persistence. When set, only messages new since the agent last spoke are\n * sent each turn and Mastra Memory supplies history. When `false`, the full LiveKit\n * in-session context is sent on every turn instead.\n */\n memory?: MastraVoiceAgentMemory | false;\n /** Request context entries forwarded to generation. */\n requestContext?: RequestContext | Record<string, unknown>;\n /**\n * Called when the Mastra agent starts a tool call mid-reply. Return a short phrase (e.g. \"Let\n * me look that up.\") to speak it while the tool runs; it also appears in the transcript. Return\n * nothing to stay silent. Applies to the agent generator built here; the workflow generator\n * takes its own equivalent via `createWorkflowReplyGenerator`.\n */\n toolFeedback?: (toolCall: VoiceToolCall) => string | undefined | void;\n /**\n * Called as each tool call starts mid-reply (before the tool result is known), the building block\n * for tool-driven side effects — analytics, agent-initiated hang-up — without waiting for the turn\n * to finish. Runs synchronously on the stream; keep it cheap and non-throwing. Applies to the agent\n * generator built here; the workflow generator surfaces tool calls via `onTurnComplete` instead.\n */\n onToolCall?: (toolCall: VoiceToolCall) => void;\n /**\n * Called once per turn after the reply has finished streaming to text-to-speech. Runs off the\n * audio path and fire-and-forget — the turn does not await it — so post-turn memory\n * maintenance, CRM writes, or analytics never delay the caller or the next turn. The context\n * carries the produced reply ({@link VoiceTurnResult}) and the resolved `memory` mapping, so\n * this is where a truly non-blocking `memory.updateWorkingMemory(...)` belongs. A thrown error\n * or rejected promise is logged, not propagated. Applies to the agent generator built here; the\n * workflow generator takes its own via `createWorkflowReplyGenerator`.\n */\n onTurnComplete?: VoiceTurnCompleteHook;\n /**\n * Periodic AI re-disclosure. When set, once `everyMs` has elapsed since the last disclosure the\n * NEXT turn's reply is prefixed with `text` (spoken at the turn boundary, never mid-turn), so long\n * calls keep re-disclosing the AI status. Applies to the agent and workflow/custom generators. The\n * worker derives this from `configuration.greeting.repeatEvery` / `repeatText`; `text` defaults to\n * {@link DEFAULT_DISCLOSURE_REMINDER}.\n */\n greetingReminder?: { everyMs: number; text?: string };\n /** Extra options merged into every `agent.stream()` call (agent generator only). */\n streamOptions?: MastraStreamOptions;\n /** LiveKit agent instructions. Unused for reply generation (the Mastra agent/workflow applies its own). */\n instructions?: string;\n id?: voice.AgentOptions<unknown>['id'];\n stt?: voice.AgentOptions<unknown>['stt'];\n vad?: voice.AgentOptions<unknown>['vad'];\n tts?: voice.AgentOptions<unknown>['tts'];\n turnHandling?: voice.AgentOptions<unknown>['turnHandling'];\n}\n\nfunction toRequestContext(value: RequestContext | Record<string, unknown> | undefined): RequestContext | undefined {\n if (!value) return undefined;\n if (value instanceof RequestContext) return value;\n return new RequestContext<unknown>(Object.entries(value));\n}\n\n/**\n * The session only runs its cascaded reply pipeline when an `llm` instance is present —\n * `llmNode` replaces the inference step, but the gate checks `llm instanceof LLM`. This\n * placeholder satisfies the gate; the Mastra agent/workflow does the actual generation.\n */\nclass MastraPlaceholderLLM extends llm.LLM {\n label(): string {\n return 'mastra.MastraVoiceAgent';\n }\n\n override get model(): string {\n return 'mastra-agent';\n }\n\n override get provider(): string {\n return 'mastra';\n }\n\n chat(): llm.LLMStream {\n throw new Error(\n '@mastra/livekit: reply generation runs through the Mastra agent via llmNode; the placeholder LLM cannot be used for inference.',\n );\n }\n}\n\n/**\n * A LiveKit `voice.Agent` whose replies come from a Mastra agent or workflow.\n *\n * LiveKit keeps ownership of the audio loop (VAD, STT, turn detection, TTS, barge-in) and calls\n * `llmNode` once per detected user turn; the node delegates to a {@link VoiceReplyGenerator}\n * which streams text deltas back. On barge-in LiveKit cancels the returned stream, which aborts\n * the in-flight generation.\n */\nexport class MastraVoiceAgent extends voice.Agent {\n readonly mastraAgent?: MastraAgent;\n readonly memory: MastraVoiceAgentMemory | false;\n readonly requestContext?: RequestContext;\n readonly streamOptions?: MastraStreamOptions;\n private readonly replyGenerator: VoiceReplyGenerator;\n private readonly reminder?: DisclosureReminder;\n\n constructor(options: MastraVoiceAgentOptions) {\n if (options.agent && options.generate) {\n throw new Error(\n '@mastra/livekit: MastraVoiceAgent requires `agent` or `generate`, not both — they are mutually exclusive reply sources.',\n );\n }\n super({\n id: options.id,\n instructions: options.instructions ?? DEFAULT_INSTRUCTIONS,\n stt: options.stt,\n vad: options.vad,\n llm: new MastraPlaceholderLLM(),\n tts: options.tts,\n turnHandling: options.turnHandling,\n });\n this.memory = options.memory ?? false;\n this.requestContext = toRequestContext(options.requestContext);\n this.streamOptions = options.streamOptions;\n if (options.greetingReminder) {\n this.reminder = new DisclosureReminder(\n options.greetingReminder.everyMs,\n options.greetingReminder.text?.trim() || DEFAULT_DISCLOSURE_REMINDER,\n );\n }\n\n if (options.generate) {\n this.replyGenerator = options.generate;\n } else if (options.agent) {\n this.mastraAgent = options.agent;\n this.replyGenerator = createAgentReplyGenerator({\n agent: options.agent,\n streamOptions: options.streamOptions,\n toolFeedback: options.toolFeedback,\n onToolCall: options.onToolCall,\n onTurnComplete: options.onTurnComplete,\n });\n } else {\n throw new Error('@mastra/livekit: MastraVoiceAgent requires `agent` or `generate`.');\n }\n }\n\n override async llmNode(\n chatCtx: llm.ChatContext,\n _toolCtx: llm.ToolContext,\n _modelSettings: voice.ModelSettings,\n ): Promise<ReadableStream<llm.ChatChunk | string> | null> {\n const messages: VoiceTurnMessage[] =\n this.memory === false ? chatContextToMessages(chatCtx) : extractNewTurnMessages(chatCtx);\n if (messages.length === 0) return null;\n\n const reply = await this.replyGenerator({\n messages,\n chatCtx,\n memory: this.memory,\n requestContext: this.requestContext,\n tracingContext: this.streamOptions?.tracingContext,\n });\n if (!reply) return null;\n\n // Periodic AI re-disclosure: when the interval has elapsed, prefix this turn's spoken reply with\n // the reminder. Done at the turn boundary (never mid-turn), riding the same stream so barge-in\n // cancellation still propagates to the underlying generation. The clock only resets once the\n // reminder is actually threaded into the outgoing reply below, not just because it was due.\n // KNOWN LIMIT: \"threaded into the reply\" is stream-build time, not playout. Under LiveKit's\n // preemptive generation a discarded speculative reply still resets the clock, so the next real\n // turn can miss its reminder — hence the documented repeatEvery/preemptiveGeneration\n // incompatibility. A playout-accurate reset needs a confirmed-turn signal llmNode doesn't have.\n const reminder = this.reminder?.due();\n if (!reminder) return reply;\n this.reminder?.markDelivered();\n return prependText(reply, reminder);\n }\n}\n\nexport function createMastraVoiceAgent(options: MastraVoiceAgentOptions): MastraVoiceAgent {\n return new MastraVoiceAgent(options);\n}\n","import { ReadableStream } from 'node:stream/web';\nimport { APIConnectionError, APIError, APIStatusError, APITimeoutError } from '@livekit/agents';\nimport { RequestContext } from '@mastra/core/request-context';\nimport { mapTurnUsage } from './bridge';\nimport type {\n VoiceReplyGenerator,\n VoiceToolCall,\n VoiceTurnCompleteContext,\n VoiceTurnCompleteHook,\n VoiceTurnUsage,\n} from './bridge';\n\nconst DEFAULT_API_PREFIX = '/api';\n/** Connect + first-token budget when not overridden. Plugin mode passes `connOptions.timeoutMs`. */\nexport const DEFAULT_REMOTE_TIMEOUT_MS = 10_000;\n/** Standalone initial-connection retry attempts. Plugin mode forces this to 0 (base class owns retries). */\nexport const DEFAULT_REMOTE_RETRIES = 2;\n\n/** Thrown (as a plain, non-retryable error) when the server emits a chunk that needs client action. */\nconst HITL_UNSUPPORTED_MESSAGE =\n '@mastra/livekit: the agent requested tool approval or suspended a tool call; human-in-the-loop ' +\n 'flows (approve-tool-call / resume-stream) are not supported on the voice path. Remove requireApproval ' +\n 'or suspend from the tools this agent uses on voice calls.';\n\n/**\n * Options for the remote Mastra transport. Shape mirrors the in-process `AgentReplyGeneratorOptions`\n * so `MastraLLM` can accept either source interchangeably.\n */\nexport interface RemoteMastraAgentOptions {\n /** Base URL of the remote Mastra server, e.g. `https://my-app.mastra.cloud`. */\n baseUrl: string;\n /** Agent key in the Mastra config's `agents`. */\n agentId: string;\n /** Path prefix for the Mastra API. Defaults to `'/api'`. */\n apiPrefix?: string;\n /** Static headers, or a (possibly async) resolver invoked per turn — e.g. to mint a fresh token. */\n headers?: Record<string, string> | (() => Record<string, string> | Promise<Record<string, string>>);\n /** Injectable `fetch` for tests/proxies. Defaults to `globalThis.fetch`. */\n fetch?: typeof globalThis.fetch;\n /**\n * Connect + first-token timeout in ms. Plugin mode default: LiveKit's `connOptions.timeoutMs` (10s).\n * Standalone default: {@link DEFAULT_REMOTE_TIMEOUT_MS}.\n */\n timeoutMs?: number;\n /**\n * Initial-connection retry attempts (before the first chunk only). Standalone default:\n * {@link DEFAULT_REMOTE_RETRIES}. In plugin mode the LiveKit base class owns retries and this is\n * forced to 0.\n */\n retries?: number;\n /** Extra fields merged into each stream request body (advanced). */\n body?: Record<string, unknown>;\n}\n\n/** {@link RemoteMastraAgentOptions} plus the per-turn observer hooks the generator threads through. */\nexport interface RemoteAgentReplyGeneratorOptions extends RemoteMastraAgentOptions {\n /** Speak a short phrase while a tool runs. See {@link MastraVoiceAgentOptions.toolFeedback}. */\n toolFeedback?: (toolCall: VoiceToolCall) => string | undefined | void;\n /** Notified as each tool-call chunk arrives, mid-stream. See {@link MastraVoiceAgentOptions.onToolCall}. */\n onToolCall?: (toolCall: VoiceToolCall) => void;\n /** Fired off the audio path after the reply streams. See {@link MastraVoiceAgentOptions.onTurnComplete}. */\n onTurnComplete?: VoiceTurnCompleteHook;\n}\n\ntype RawChunk = { type?: string; payload?: Record<string, unknown> };\n\nfunction trimTrailingSlash(url: string): string {\n return url.endsWith('/') ? url.slice(0, -1) : url;\n}\n\nfunction toMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nasync function resolveHeaders(headers: RemoteMastraAgentOptions['headers']): Promise<Record<string, string>> {\n if (!headers) return {};\n if (typeof headers === 'function') return (await headers()) ?? {};\n return headers;\n}\n\nfunction serializeRequestContext(\n requestContext: RequestContext | Record<string, unknown> | undefined,\n): Record<string, unknown> | undefined {\n if (!requestContext) return undefined;\n // Mirror client-js `parseClientRequestContext`.\n if (requestContext instanceof RequestContext) return Object.fromEntries(requestContext.entries());\n return requestContext;\n}\n\nasync function safeReadBody(response: Response): Promise<object | null> {\n try {\n const text = await response.text();\n if (!text) return null;\n try {\n const parsed: unknown = JSON.parse(text);\n return parsed && typeof parsed === 'object' ? (parsed as object) : { message: String(parsed) };\n } catch {\n return { message: text };\n }\n } catch {\n return null;\n }\n}\n\n/**\n * Reads a Mastra SSE stream and yields each event's parsed JSON. Framing matches the server's\n * `processMastraStream` (buffer, split on `\\n\\n`, strip `data: `, stop on `[DONE]`); undecodable\n * `data:` lines are skipped. Aborting `signal` cancels the underlying reader.\n */\nexport async function* readMastraSSE(\n body: globalThis.ReadableStream<Uint8Array>,\n signal: AbortSignal,\n): AsyncGenerator<RawChunk> {\n const reader = body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n const onAbort = () => void reader.cancel().catch(() => {});\n if (signal.aborted) {\n void reader.cancel().catch(() => {});\n return;\n }\n signal.addEventListener('abort', onAbort, { once: true });\n try {\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n const events = buffer.split('\\n\\n');\n buffer = events.pop() ?? '';\n for (const event of events) {\n if (!event.startsWith('data:')) continue;\n const data = event.slice(event.startsWith('data: ') ? 6 : 5).trim();\n if (data === '[DONE]') return;\n if (!data) continue;\n let json: unknown;\n try {\n json = JSON.parse(data);\n } catch {\n continue; // tolerate a stray non-JSON line\n }\n if (json && typeof json === 'object') yield json as RawChunk;\n }\n }\n } finally {\n signal.removeEventListener('abort', onAbort);\n try {\n reader.releaseLock();\n } catch {\n // A read may still be pending when the fetch was aborted; the stream is torn down anyway.\n }\n }\n}\n\n/**\n * A {@link VoiceReplyGenerator} that runs the Mastra agent loop on a **remote** Mastra server over\n * HTTP/SSE. Shaped exactly like the in-process `createAgentReplyGenerator`: it consumes the same\n * chunk vocabulary, drives the same `toolFeedback` / `onToolCall` / `onTurnComplete` seams, and\n * cancelling the returned stream (LiveKit does this on barge-in) tears down the HTTP request so the\n * server aborts generation.\n *\n * Usable standalone via the worker's `generate:` hatch (a minimum-viable remote worker mode), and as\n * the transport `MastraLLM` wraps. Errors are thrown as LiveKit `APIError` subclasses so the plugin's\n * base-class retry loop and `FallbackAdapter` behave; a connect + first-token watchdog prevents\n * indefinite dead air.\n */\nexport function createRemoteAgentReplyGenerator(options: RemoteAgentReplyGeneratorOptions): VoiceReplyGenerator {\n const {\n baseUrl,\n agentId,\n apiPrefix = DEFAULT_API_PREFIX,\n headers,\n fetch: fetchImpl = globalThis.fetch,\n timeoutMs = DEFAULT_REMOTE_TIMEOUT_MS,\n retries = DEFAULT_REMOTE_RETRIES,\n body: extraBody,\n toolFeedback,\n onToolCall,\n onTurnComplete,\n } = options;\n\n if (!fetchImpl) {\n throw new Error('@mastra/livekit: no fetch implementation available; pass `fetch` or run on Node ≥ 22.');\n }\n const url = `${trimTrailingSlash(baseUrl)}${apiPrefix}/agents/${agentId}/stream`;\n\n return ctx => {\n if (ctx.messages.length === 0) return null;\n\n // Reassigned per retry attempt (see the loop below) so a watchdog abort on one attempt can't\n // poison the next; `cancel()` always aborts whichever attempt is currently in flight.\n let currentAbortController: AbortController | undefined;\n let cancelled = false;\n // Accumulated as the turn streams so the post-turn hook sees what was actually produced.\n let replyText = '';\n const toolCalls: VoiceToolCall[] = [];\n let usage: VoiceTurnUsage | undefined;\n\n const emitTurnComplete = (interrupted: boolean) => {\n if (!onTurnComplete) return;\n const completeCtx: VoiceTurnCompleteContext = {\n ...ctx,\n result: { text: replyText, toolCalls, interrupted, usage },\n };\n Promise.resolve()\n .then(() => onTurnComplete(completeCtx))\n .catch(error => {\n console.warn('@mastra/livekit: onTurnComplete hook threw', error);\n });\n };\n\n const requestBody: Record<string, unknown> = {\n messages: ctx.messages,\n // The server schema requires a resource when memory is present; default it to the thread id,\n // matching the worker's own thread bootstrap.\n memory: ctx.memory\n ? { thread: ctx.memory.thread, resource: ctx.memory.resource ?? ctx.memory.thread }\n : undefined,\n requestContext: serializeRequestContext(ctx.requestContext),\n ...extraBody,\n };\n\n return new ReadableStream<string>({\n start: async controller => {\n // `retryable` is the LiveKit contract flag: true only before the first chunk is emitted, so a\n // voice turn is never replayed half-heard. It also gates the standalone connect-retry.\n let retryable = true;\n try {\n for (let attempt = 0; ; attempt++) {\n // A fresh controller per attempt: reusing one across retries meant a watchdog abort on an\n // earlier attempt left every subsequent attempt's fetch already-aborted before it started.\n const abortController = new AbortController();\n currentAbortController = abortController;\n let timedOut = false;\n let watchdog: ReturnType<typeof setTimeout> | undefined;\n const clearWatchdog = () => {\n if (watchdog) {\n clearTimeout(watchdog);\n watchdog = undefined;\n }\n };\n try {\n watchdog = setTimeout(() => {\n timedOut = true;\n abortController.abort();\n }, timeoutMs);\n (watchdog as { unref?: () => void }).unref?.();\n\n const resolvedHeaders = await resolveHeaders(headers);\n if (cancelled) {\n clearWatchdog();\n break;\n }\n const response = await fetchImpl(url, {\n method: 'POST',\n headers: { 'content-type': 'application/json', accept: 'text/event-stream', ...resolvedHeaders },\n body: JSON.stringify(requestBody),\n signal: abortController.signal,\n });\n if (!response.ok) {\n const errorBody = await safeReadBody(response);\n throw new APIStatusError({\n message: `@mastra/livekit: Mastra agent stream request failed with status ${response.status}`,\n options: { statusCode: response.status, body: errorBody, retryable },\n });\n }\n if (!response.body) {\n throw new APIConnectionError({\n message: '@mastra/livekit: Mastra agent stream returned an empty response body',\n options: { retryable },\n });\n }\n\n for await (const chunk of readMastraSSE(\n response.body as unknown as globalThis.ReadableStream<Uint8Array>,\n abortController.signal,\n )) {\n if (cancelled) break;\n // First chunk: the server has committed to this generation — forbid any further\n // retry so the turn can't be replayed mid-stream. The watchdog is NOT cleared here:\n // lifecycle metadata (step-start, text-start, ...) isn't proof the model is\n // producing anything, and disarming on it would turn a post-metadata stall into\n // indefinite dead air. It disarms on the first sign of model output below.\n retryable = false;\n const payload = chunk.payload ?? {};\n switch (chunk.type) {\n case 'text-delta': {\n const text = payload.text;\n if (typeof text === 'string' && text) {\n clearWatchdog();\n replyText += text;\n controller.enqueue(text);\n }\n break;\n }\n case 'tool-call': {\n // A tool call is first-token progress too — the model committed to a tool run,\n // which may legitimately outlast the connect budget before any text streams.\n clearWatchdog();\n const toolCall: VoiceToolCall = {\n toolCallId: String(payload.toolCallId ?? ''),\n toolName: String(payload.toolName ?? ''),\n args: payload.args,\n };\n toolCalls.push(toolCall);\n // Observer hooks are customer code: a throw must not tear down an otherwise\n // healthy reply stream (same isolation as onTurnComplete).\n try {\n onToolCall?.(toolCall);\n } catch (error) {\n console.warn('@mastra/livekit: onToolCall hook threw', error);\n }\n if (toolFeedback) {\n let filler: string | undefined | void;\n try {\n filler = toolFeedback(toolCall);\n } catch (error) {\n console.warn('@mastra/livekit: toolFeedback hook threw', error);\n }\n if (filler) controller.enqueue(filler.endsWith(' ') ? filler : `${filler} `);\n }\n break;\n }\n case 'finish': {\n clearWatchdog();\n const output = payload.output as { usage?: unknown } | undefined;\n const turnUsage = mapTurnUsage(output?.usage);\n if (turnUsage) {\n usage = turnUsage;\n try {\n ctx.onUsage?.(turnUsage);\n } catch (error) {\n console.warn('@mastra/livekit: onUsage hook threw', error);\n }\n }\n break;\n }\n case 'tool-call-approval':\n case 'tool-call-suspended':\n throw new Error(HITL_UNSUPPORTED_MESSAGE);\n case 'error': {\n const error = payload.error;\n throw error instanceof Error ? error : new Error(String(error));\n }\n default:\n break; // ignore everything else (text-start, step-start, tool-result, ...)\n }\n }\n clearWatchdog();\n break; // success, or a clean barge-in break out of the SSE loop\n } catch (error) {\n clearWatchdog();\n if (cancelled) break; // barge-in: not a failure, emit interrupted below\n\n // Classify into the LiveKit error vocabulary. Application errors thrown after streaming\n // began (an `error` chunk, a HITL chunk) are not connection failures — propagate as-is.\n let typed: APIError;\n if (timedOut) {\n typed = new APITimeoutError({ options: { retryable } });\n } else if (error instanceof APIError) {\n typed = error;\n } else if (retryable) {\n typed = new APIConnectionError({ message: toMessage(error), options: { retryable } });\n } else {\n throw error;\n }\n if (typed.retryable && attempt < retries) continue;\n throw typed;\n }\n }\n if (!cancelled) controller.close();\n // Success or clean barge-in: the turn is done either way.\n emitTurnComplete(cancelled);\n } catch (error) {\n // Barge-in never reaches here (handled above); a real failure errors the stream and does\n // not fire onTurnComplete — same contract as the in-process generator.\n if (cancelled) {\n emitTurnComplete(true);\n return;\n }\n controller.error(error);\n }\n },\n cancel: () => {\n cancelled = true;\n currentAbortController?.abort();\n },\n });\n };\n}\n"],"mappings":"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"}
|
package/dist/worker-entry.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_workflow_generator = require("./workflow-generator-B67QrY8L.cjs");
|
|
3
|
-
const require_remote = require("./remote-
|
|
3
|
+
const require_remote = require("./remote-BZ7eyB1q.cjs");
|
|
4
4
|
let crypto = require("crypto");
|
|
5
5
|
let _livekit_agents = require("@livekit/agents");
|
|
6
6
|
let _mastra_core_request_context = require("@mastra/core/request-context");
|
|
@@ -675,8 +675,10 @@ exports.DEFAULT_END_CALL_DRAIN_MS = DEFAULT_END_CALL_DRAIN_MS;
|
|
|
675
675
|
exports.DEFAULT_END_CALL_MAX_WAIT_MS = DEFAULT_END_CALL_MAX_WAIT_MS;
|
|
676
676
|
exports.DEFAULT_END_CALL_REASON = DEFAULT_END_CALL_REASON;
|
|
677
677
|
exports.DEFAULT_END_CALL_TOOL = DEFAULT_END_CALL_TOOL;
|
|
678
|
+
exports.MastraVoiceAgent = require_remote.MastraVoiceAgent;
|
|
678
679
|
exports.chatContextToMessages = require_remote.chatContextToMessages;
|
|
679
680
|
exports.createLiveKitWorker = createLiveKitWorker;
|
|
681
|
+
exports.createMastraVoiceAgent = require_remote.createMastraVoiceAgent;
|
|
680
682
|
exports.createRemoteAgentReplyGenerator = require_remote.createRemoteAgentReplyGenerator;
|
|
681
683
|
exports.runEndCall = runEndCall;
|
|
682
684
|
exports.runLiveKitWorker = runLiveKitWorker;
|
package/dist/worker-entry.d.ts
CHANGED
|
@@ -4,7 +4,8 @@ export { runLiveKitWorker } from './run.js';
|
|
|
4
4
|
export type { RunLiveKitWorkerOptions } from './run.js';
|
|
5
5
|
export { chatContextToMessages } from './messages.js';
|
|
6
6
|
export type { VoiceTurnMessage } from './messages.js';
|
|
7
|
-
export
|
|
7
|
+
export { MastraVoiceAgent, createMastraVoiceAgent } from './bridge.js';
|
|
8
|
+
export type { MastraStreamOptions, MastraVoiceAgentMemory, MastraVoiceAgentOptions, VoiceReplyGenerator, VoiceToolCall, VoiceTurnCompleteContext, VoiceTurnCompleteHook, VoiceTurnContext, VoiceTurnResult, VoiceTurnUsage, } from './bridge.js';
|
|
8
9
|
export { createRemoteAgentReplyGenerator } from './remote.js';
|
|
9
10
|
export type { RemoteMastraAgentOptions, RemoteAgentReplyGeneratorOptions } from './remote.js';
|
|
10
11
|
//# sourceMappingURL=worker-entry.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worker-entry.d.ts","sourceRoot":"","sources":["../src/worker-entry.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,mBAAmB,EAInB,aAAa,EACb,wBAAwB,EACxB,UAAU,EACV,qBAAqB,EACrB,uBAAuB,EACvB,4BAA4B,EAC5B,yBAAyB,GAC1B,MAAM,UAAU,CAAC;AAClB,YAAY,EACV,oBAAoB,EACpB,kBAAkB,EAClB,0BAA0B,EAC1B,oBAAoB,EACpB,qBAAqB,EACrB,eAAe,EACf,YAAY,EACZ,0BAA0B,EAC1B,sBAAsB,EACtB,wBAAwB,EACxB,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,gBAAgB,EAAE,MAAM,OAAO,CAAC;AACzC,YAAY,EAAE,uBAAuB,EAAE,MAAM,OAAO,CAAC;AACrD,OAAO,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AACnD,YAAY,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"worker-entry.d.ts","sourceRoot":"","sources":["../src/worker-entry.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,mBAAmB,EAInB,aAAa,EACb,wBAAwB,EACxB,UAAU,EACV,qBAAqB,EACrB,uBAAuB,EACvB,4BAA4B,EAC5B,yBAAyB,GAC1B,MAAM,UAAU,CAAC;AAClB,YAAY,EACV,oBAAoB,EACpB,kBAAkB,EAClB,0BAA0B,EAC1B,oBAAoB,EACpB,qBAAqB,EACrB,eAAe,EACf,YAAY,EACZ,0BAA0B,EAC1B,sBAAsB,EACtB,wBAAwB,EACxB,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,gBAAgB,EAAE,MAAM,OAAO,CAAC;AACzC,YAAY,EAAE,uBAAuB,EAAE,MAAM,OAAO,CAAC;AACrD,OAAO,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AACnD,YAAY,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAInD,OAAO,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AACpE,YAAY,EACV,mBAAmB,EACnB,sBAAsB,EACtB,uBAAuB,EACvB,mBAAmB,EACnB,aAAa,EACb,wBAAwB,EACxB,qBAAqB,EACrB,gBAAgB,EAChB,eAAe,EACf,cAAc,GACf,MAAM,UAAU,CAAC;AAIlB,OAAO,EAAE,+BAA+B,EAAE,MAAM,UAAU,CAAC;AAC3D,YAAY,EAAE,wBAAwB,EAAE,gCAAgC,EAAE,MAAM,UAAU,CAAC"}
|
package/dist/worker-entry.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { r as parseSessionMetadata, t as createWorkflowReplyGenerator } from "./workflow-generator-BtfClQcM.js";
|
|
2
|
-
import {
|
|
2
|
+
import { a as chatContextToMessages, i as createMastraVoiceAgent, n as MastraVoiceAgent, t as createRemoteAgentReplyGenerator } from "./remote-D7n50m8S.js";
|
|
3
3
|
import { randomUUID } from "crypto";
|
|
4
4
|
import { InferenceRunner, ServerOptions, cli, defineAgent, metrics, voice } from "@livekit/agents";
|
|
5
5
|
import { RequestContext } from "@mastra/core/request-context";
|
|
@@ -670,6 +670,6 @@ function runLiveKitWorker(options) {
|
|
|
670
670
|
});
|
|
671
671
|
}
|
|
672
672
|
//#endregion
|
|
673
|
-
export { DEFAULT_END_CALL_DRAIN_MS, DEFAULT_END_CALL_MAX_WAIT_MS, DEFAULT_END_CALL_REASON, DEFAULT_END_CALL_TOOL, chatContextToMessages, createLiveKitWorker, createRemoteAgentReplyGenerator, runEndCall, runLiveKitWorker, speakGreeting, waitForAgentDoneSpeaking };
|
|
673
|
+
export { DEFAULT_END_CALL_DRAIN_MS, DEFAULT_END_CALL_MAX_WAIT_MS, DEFAULT_END_CALL_REASON, DEFAULT_END_CALL_TOOL, MastraVoiceAgent, chatContextToMessages, createLiveKitWorker, createMastraVoiceAgent, createRemoteAgentReplyGenerator, runEndCall, runLiveKitWorker, speakGreeting, waitForAgentDoneSpeaking };
|
|
674
674
|
|
|
675
675
|
//# sourceMappingURL=worker-entry.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mastra/livekit",
|
|
3
|
-
"version": "0.3.1-alpha.
|
|
3
|
+
"version": "0.3.1-alpha.2",
|
|
4
4
|
"description": "LiveKit voice integration for Mastra agents — realtime voice with semantic turn detection and barge-in",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
"zod": "^4.4.3",
|
|
95
95
|
"@internal/lint": "0.0.129",
|
|
96
96
|
"@internal/types-builder": "0.0.104",
|
|
97
|
-
"@mastra/core": "1.64.0-alpha.
|
|
97
|
+
"@mastra/core": "1.64.0-alpha.7"
|
|
98
98
|
},
|
|
99
99
|
"scripts": {
|
|
100
100
|
"build:lib": "tsdown --silent --config tsdown.config.ts",
|