@mastra/livekit 0.3.0 → 0.3.1-alpha.0
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/LICENSE.md +6 -4
- package/README.md +1 -1
- package/dist/bridge.d.ts.map +1 -1
- package/dist/index.cjs +172 -134
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +168 -121
- package/dist/index.js.map +1 -1
- package/dist/llm-plugin.d.ts.map +1 -1
- package/dist/plugin-entry.cjs +199 -172
- package/dist/plugin-entry.cjs.map +1 -1
- package/dist/plugin-entry.js +195 -165
- package/dist/plugin-entry.js.map +1 -1
- package/dist/remote-C9K3UzKv.js +629 -0
- package/dist/remote-C9K3UzKv.js.map +1 -0
- package/dist/remote-D0Y5P6e4.cjs +658 -0
- package/dist/remote-D0Y5P6e4.cjs.map +1 -0
- package/dist/worker-entry.cjs +621 -538
- package/dist/worker-entry.cjs.map +1 -1
- package/dist/worker-entry.js +618 -529
- package/dist/worker-entry.js.map +1 -1
- package/dist/workflow-generator-B67QrY8L.cjs +209 -0
- package/dist/workflow-generator-B67QrY8L.cjs.map +1 -0
- package/dist/workflow-generator-BtfClQcM.js +180 -0
- package/dist/workflow-generator-BtfClQcM.js.map +1 -0
- package/package.json +14 -14
- package/CHANGELOG.md +0 -426
- package/dist/chunk-2E3MTAOA.js +0 -133
- package/dist/chunk-2E3MTAOA.js.map +0 -1
- package/dist/chunk-4O7IN74Y.js +0 -568
- package/dist/chunk-4O7IN74Y.js.map +0 -1
- package/dist/chunk-DBVKNDAQ.cjs +0 -574
- package/dist/chunk-DBVKNDAQ.cjs.map +0 -1
- package/dist/chunk-MWTEZOBS.cjs +0 -139
- package/dist/chunk-MWTEZOBS.cjs.map +0 -1
package/CHANGELOG.md
DELETED
|
@@ -1,426 +0,0 @@
|
|
|
1
|
-
# @mastra/livekit
|
|
2
|
-
|
|
3
|
-
## 0.3.0
|
|
4
|
-
|
|
5
|
-
### Minor Changes
|
|
6
|
-
|
|
7
|
-
- Added per-call speech-to-text and text-to-speech selection to `createLiveKitWorker`. Set the new `configuration.stt` and `configuration.tts` resolvers to pick the transcriber and voice for each call — one voice or language per tenant — keyed off the dispatch metadata and request context. Each resolver runs once per call and falls back to the top-level `stt` / `tts` option when it returns `undefined`. ([#19136](https://github.com/mastra-ai/mastra/pull/19136))
|
|
8
|
-
|
|
9
|
-
```ts
|
|
10
|
-
export default createLiveKitWorker({
|
|
11
|
-
mastra,
|
|
12
|
-
agent: 'support',
|
|
13
|
-
stt: 'deepgram/nova-3',
|
|
14
|
-
tts: 'cartesia/sonic-3', // fallback voice
|
|
15
|
-
configuration: {
|
|
16
|
-
// Give each tenant its own voice, resolved per call from the dispatch metadata.
|
|
17
|
-
tts: ({ requestContext }) => tenantVoices[requestContext?.tenant as string],
|
|
18
|
-
},
|
|
19
|
-
});
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
Previously the worker's speech pipeline was fixed at construction, so a multi-tenant worker could not vary voices or transcription per call. Customers who own their LiveKit session (the `MastraLLM` plugin path) already choose STT/TTS per call by construction; this brings the same flexibility to the batteries-included worker.
|
|
23
|
-
|
|
24
|
-
- Added `MastraLLM`, a standard LiveKit LLM plugin, on the new `@mastra/livekit/plugin` entry point. Build your own `voice.AgentSession` and put a Mastra agent in the `llm` slot — the agent loop, tools, and memory run on a remote Mastra server reached over HTTP, so the worker process needs no Mastra app, database, or model provider keys. ([#19136](https://github.com/mastra-ai/mastra/pull/19136))
|
|
25
|
-
|
|
26
|
-
Before, the worker wrapper always owned the LiveKit session:
|
|
27
|
-
|
|
28
|
-
```ts
|
|
29
|
-
import { createLiveKitWorker } from '@mastra/livekit/worker';
|
|
30
|
-
import { mastra } from './index';
|
|
31
|
-
|
|
32
|
-
export default createLiveKitWorker({
|
|
33
|
-
mastra,
|
|
34
|
-
agent: 'support',
|
|
35
|
-
stt: 'deepgram/nova-3',
|
|
36
|
-
tts: 'cartesia/sonic-3',
|
|
37
|
-
});
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
Now you can own the session and keep Mastra as the LLM component:
|
|
41
|
-
|
|
42
|
-
```ts
|
|
43
|
-
import { voice } from '@livekit/agents';
|
|
44
|
-
import { MastraLLM } from '@mastra/livekit/plugin';
|
|
45
|
-
|
|
46
|
-
const session = new voice.AgentSession({
|
|
47
|
-
llm: new MastraLLM({
|
|
48
|
-
remote: { baseUrl: process.env.MASTRA_URL!, agentId: 'support' },
|
|
49
|
-
memory: { thread: callId, resource: userId },
|
|
50
|
-
}),
|
|
51
|
-
stt: 'deepgram/nova-3',
|
|
52
|
-
tts: 'cartesia/sonic-3',
|
|
53
|
-
// Required with `memory`: LiveKit enables preemptive generation by default.
|
|
54
|
-
turnHandling: { preemptiveGeneration: { enabled: false } },
|
|
55
|
-
});
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
`createLiveKitWorker` stays the batteries-included path; the plugin is the composable one. Tools keep running server-side on the Mastra agent, and interrupting the agent aborts the server-side generation.
|
|
59
|
-
|
|
60
|
-
**New transport and helpers**
|
|
61
|
-
- Added `createRemoteAgentReplyGenerator()`: streams replies from a remote Mastra server over HTTP with per-turn abort, LiveKit-typed errors, and a connect + first-token timeout. It also plugs into `createLiveKitWorker`'s `generate` option to run the existing worker against a remote server.
|
|
62
|
-
- Promoted `speakGreeting()`, `waitForAgentDoneSpeaking()`, and `runEndCall()` to public exports of `@mastra/livekit/worker`, so a worker that owns its session can rebuild the greeting and agent-initiated hang-up patterns in a few lines.
|
|
63
|
-
|
|
64
|
-
**Improvements to the existing worker**
|
|
65
|
-
- Interrupted turns now self-heal: when a caller interrupts a reply, nothing is persisted at that moment, and the part the caller actually heard is backfilled into the memory thread on the next turn — so saved transcripts match the call.
|
|
66
|
-
- Added an `onToolCall` hook that fires as each tool call starts mid-reply, the building block for tool-driven side effects such as analytics or hang-up.
|
|
67
|
-
- `onTurnComplete` now receives the turn's token usage as `result.usage`.
|
|
68
|
-
|
|
69
|
-
- Added a `configuration` option to `createLiveKitWorker` — one grouped home for conversation and compliance controls, so these don't each become a separate top-level worker option. It ships with greeting/AI-disclosure controls, a consent model, and agent-initiated hang-up, and is where further compliance controls will land. ([#19136](https://github.com/mastra-ai/mastra/pull/19136))
|
|
70
|
-
|
|
71
|
-
**Greeting and AI disclosure**
|
|
72
|
-
|
|
73
|
-
`configuration.greeting` controls the opening line spoken at call start. Set `allowInterruptions: false` so a legally-required AI disclosure plays through and can't be talked over (EU AI Act Art. 50), `awaitPlayout: true` to hold post-greeting work until it finishes, and `repeatEvery` to re-disclose periodically on long calls (spoken at the next turn boundary, never mid-sentence).
|
|
74
|
-
|
|
75
|
-
```ts
|
|
76
|
-
createLiveKitWorker({
|
|
77
|
-
mastra,
|
|
78
|
-
agent: 'support',
|
|
79
|
-
configuration: {
|
|
80
|
-
greeting: {
|
|
81
|
-
text: 'You are speaking with an AI assistant. This call may be recorded. How can I help?',
|
|
82
|
-
allowInterruptions: false,
|
|
83
|
-
awaitPlayout: true,
|
|
84
|
-
repeatEvery: 3 * 60_000, // re-disclose ~every 3 minutes
|
|
85
|
-
},
|
|
86
|
-
},
|
|
87
|
-
});
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
**Per-tenant greeting**
|
|
91
|
-
|
|
92
|
-
`greeting.text` also accepts a resolver, called once per call with the call context, so one multi-tenant agent can open differently per tenant based on the dispatch metadata:
|
|
93
|
-
|
|
94
|
-
```ts
|
|
95
|
-
greeting: {
|
|
96
|
-
text: ({ metadata }) => `Thanks for calling ${tenantName(metadata)}. You're speaking with an AI assistant.`,
|
|
97
|
-
allowInterruptions: false,
|
|
98
|
-
}
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
**Consent**
|
|
102
|
-
|
|
103
|
-
`configuration.consentPolicy` declares which data-use consents a call needs, as a named, extensible set (starting with `summaryStorage`) rather than one global flag. Declaring the policy enforces nothing by itself: the new `createConsentTool` captures the caller's decision at runtime — add it to your agent and it hands each decision to your own store — and your code enforces the requirement at `onCallEnd` (or before any consent-gated step).
|
|
104
|
-
|
|
105
|
-
```ts
|
|
106
|
-
import { createConsentTool } from '@mastra/livekit';
|
|
107
|
-
|
|
108
|
-
// in your agent's tools:
|
|
109
|
-
recordConsent: createConsentTool({
|
|
110
|
-
items: ['summaryStorage'],
|
|
111
|
-
onGrant: async ({ item, granted, resourceId }) => {
|
|
112
|
-
if (resourceId) await db.saveConsent(resourceId, item, granted);
|
|
113
|
-
},
|
|
114
|
-
}),
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
**Agent-initiated hang-up**
|
|
118
|
-
|
|
119
|
-
`configuration.endCall` lets the agent end the call itself. Add the new `createEndCallTool` to your agent and instruct it to say goodbye and then call the tool; the worker waits for the closing words to finish playing, holds a short audio drain (`drainMs`, default 800ms) so the tail of the goodbye isn't clipped while it's still buffered at the caller, then hangs up — running `onCallEnd` on the way out, exactly as a caller hang-up does. It works on both the agent and workflow reply paths.
|
|
120
|
-
|
|
121
|
-
```ts
|
|
122
|
-
import { createEndCallTool } from '@mastra/livekit';
|
|
123
|
-
|
|
124
|
-
// in your agent's tools:
|
|
125
|
-
endCall: (createEndCallTool(),
|
|
126
|
-
// on the worker:
|
|
127
|
-
createLiveKitWorker({ mastra, agent: 'support', configuration: { endCall: {} } }));
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
**Backwards compatible**
|
|
131
|
-
|
|
132
|
-
The previous top-level `greeting` (string) and `persistGreeting` options still work as deprecated aliases for `configuration.greeting.text` and `configuration.greeting.persist`. When both are set, `configuration.greeting` wins field by field, so existing worker configs keep running unchanged.
|
|
133
|
-
|
|
134
|
-
### Patch Changes
|
|
135
|
-
|
|
136
|
-
- Updated dependencies [[`bd6d240`](https://github.com/mastra-ai/mastra/commit/bd6d2402db93dddaef0721667e7e8a030e7c6e16), [`0111486`](https://github.com/mastra-ai/mastra/commit/01114867612593eef5cfa2fda6a1194dfedda841), [`96a3749`](https://github.com/mastra-ai/mastra/commit/96a37492235f5b8076b3e3177d83ed5a5e44a640), [`fe1bda0`](https://github.com/mastra-ai/mastra/commit/fe1bda06f6af92a694a51712db747cda1e7185f0), [`25e7c12`](https://github.com/mastra-ai/mastra/commit/25e7c126a770069ae7fb7ecf1d2adb40e017b009), [`1ce5121`](https://github.com/mastra-ai/mastra/commit/1ce512155d122bb21f47d98383e82ffbf84b39e8), [`fb8aea3`](https://github.com/mastra-ai/mastra/commit/fb8aea384291e77311be3a64ee1717320d5c3c73), [`4adc391`](https://github.com/mastra-ai/mastra/commit/4adc3911075249c352bb4832d2471922826344de), [`a5c6337`](https://github.com/mastra-ai/mastra/commit/a5c6337d23c7686c81a32ce62f550f610543a240), [`3cfc47a`](https://github.com/mastra-ai/mastra/commit/3cfc47a6b89940aadd0f46fb01ae9624a73a865d), [`2bb7817`](https://github.com/mastra-ai/mastra/commit/2bb78176112fde628483de2830528f7eee911e56), [`51d9870`](https://github.com/mastra-ai/mastra/commit/51d987032c689c2855374d0f244f5d654da809d1), [`5cab274`](https://github.com/mastra-ai/mastra/commit/5cab2744250e22d12fefa7b32637dce224233cee), [`7fa27d3`](https://github.com/mastra-ai/mastra/commit/7fa27d3b6f5ed68cd34e454a4d3ad9c482a0cfbc), [`8b97958`](https://github.com/mastra-ai/mastra/commit/8b979589f9aa59ba67cac565949475f2ffeb4ac3), [`8410541`](https://github.com/mastra-ai/mastra/commit/84105412c60ecd3bb33a9838146f59c4b588228f), [`a58dcbb`](https://github.com/mastra-ai/mastra/commit/a58dcbb546d7e1d65ebdc1f39e55f0908fcd9391), [`aa38805`](https://github.com/mastra-ai/mastra/commit/aa38805b878b827403be785eb90688d7172f5a40), [`153bd3b`](https://github.com/mastra-ai/mastra/commit/153bd3b396bdfed6b74cf43de12db8fd2d83c04a), [`45a8e65`](https://github.com/mastra-ai/mastra/commit/45a8e65e1556d1362cb3f25187023c36de26661d), [`e955965`](https://github.com/mastra-ai/mastra/commit/e955965dce575a903e37cf054d28ea99aa48785e), [`2d22570`](https://github.com/mastra-ai/mastra/commit/2d22570c7dfdd02123d0ecc529efb05ccba2d9fc), [`07bb863`](https://github.com/mastra-ai/mastra/commit/07bb8631919c6f7cf377dccd45b096e0f17fbed0), [`c8ed116`](https://github.com/mastra-ai/mastra/commit/c8ed11699f62bcac70102ab4ec84d80d20541da6), [`01b338c`](https://github.com/mastra-ai/mastra/commit/01b338c56271f0219606710e3e8b26dee27ac6c2), [`a99eae8`](https://github.com/mastra-ai/mastra/commit/a99eae8908e500c1b2d12f9d277be616b98617a5), [`860ef7e`](https://github.com/mastra-ai/mastra/commit/860ef7e77d92b63469cbe5857aa1e626197e43e9), [`17e818c`](https://github.com/mastra-ai/mastra/commit/17e818c51a958ba90641b1a959dc38faf8c034e9), [`edce8d2`](https://github.com/mastra-ai/mastra/commit/edce8d2769f19e27a05737c627af2d765472a4f8), [`8a586ec`](https://github.com/mastra-ai/mastra/commit/8a586eca9a4914f31dff6140d0d45ac375b00669), [`4451dfe`](https://github.com/mastra-ai/mastra/commit/4451dfe857428e7abcc0261a507a2e186dae6d47), [`8b7361d`](https://github.com/mastra-ai/mastra/commit/8b7361d35de68b80d05d30a74e0c69e7218fd612), [`1d39058`](https://github.com/mastra-ai/mastra/commit/1d39058e548efd691799985d5c8af2737f1c3bd2), [`3927473`](https://github.com/mastra-ai/mastra/commit/392747323ddb10c643d12be7b9ae913159dfaeed), [`dce50dc`](https://github.com/mastra-ai/mastra/commit/dce50dc9a1c1fcd0f427bb5f6250ec74910cb04b), [`fd13f8e`](https://github.com/mastra-ai/mastra/commit/fd13f8e21990f9904c3eedba3a626bb4a929cdb8), [`634caff`](https://github.com/mastra-ai/mastra/commit/634caff29a9200ad058b67d53f96d9e5832fb8a2), [`f703f87`](https://github.com/mastra-ai/mastra/commit/f703f878de072d51fda557f9c50867d8252bef05), [`3e26c87`](https://github.com/mastra-ai/mastra/commit/3e26c87de0c5bc2583b795ce6ca5889b6b161acb), [`33f2b88`](https://github.com/mastra-ai/mastra/commit/33f2b88842c09a567f906fac4cb61cd5277ced59), [`177010f`](https://github.com/mastra-ai/mastra/commit/177010ff096d2e4b28d89803be5b1a4cad2a0d6b), [`0ad646f`](https://github.com/mastra-ai/mastra/commit/0ad646f71a530f2454664299e5e01bfd13fa12e5), [`b486abf`](https://github.com/mastra-ai/mastra/commit/b486abfa2a7528c6f527e4015c819ea9fa54aaad), [`54a51e0`](https://github.com/mastra-ai/mastra/commit/54a51e0a484fe1ebad3fb1f7ef5282a075709eb7), [`c43f3a9`](https://github.com/mastra-ai/mastra/commit/c43f3a9d1efde99b38789364ba4d0ba670f430e3), [`a5008f2`](https://github.com/mastra-ai/mastra/commit/a5008f22ae710ad9402ea9f2547d8c02f74d384b), [`e2d5f37`](https://github.com/mastra-ai/mastra/commit/e2d5f373bd289be534d5f8694d34465010533df6), [`4ce0163`](https://github.com/mastra-ai/mastra/commit/4ce0163dc86e675a86809685c8ce6c49f1aeb87e), [`4378341`](https://github.com/mastra-ai/mastra/commit/43783412df5ea3dd35f5b1f6e4851e79c346fc89)]:
|
|
137
|
-
- @mastra/core@1.51.0
|
|
138
|
-
|
|
139
|
-
## 0.3.0-alpha.0
|
|
140
|
-
|
|
141
|
-
### Minor Changes
|
|
142
|
-
|
|
143
|
-
- Added per-call speech-to-text and text-to-speech selection to `createLiveKitWorker`. Set the new `configuration.stt` and `configuration.tts` resolvers to pick the transcriber and voice for each call — one voice or language per tenant — keyed off the dispatch metadata and request context. Each resolver runs once per call and falls back to the top-level `stt` / `tts` option when it returns `undefined`. ([#19136](https://github.com/mastra-ai/mastra/pull/19136))
|
|
144
|
-
|
|
145
|
-
```ts
|
|
146
|
-
export default createLiveKitWorker({
|
|
147
|
-
mastra,
|
|
148
|
-
agent: 'support',
|
|
149
|
-
stt: 'deepgram/nova-3',
|
|
150
|
-
tts: 'cartesia/sonic-3', // fallback voice
|
|
151
|
-
configuration: {
|
|
152
|
-
// Give each tenant its own voice, resolved per call from the dispatch metadata.
|
|
153
|
-
tts: ({ requestContext }) => tenantVoices[requestContext?.tenant as string],
|
|
154
|
-
},
|
|
155
|
-
});
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
Previously the worker's speech pipeline was fixed at construction, so a multi-tenant worker could not vary voices or transcription per call. Customers who own their LiveKit session (the `MastraLLM` plugin path) already choose STT/TTS per call by construction; this brings the same flexibility to the batteries-included worker.
|
|
159
|
-
|
|
160
|
-
- Added `MastraLLM`, a standard LiveKit LLM plugin, on the new `@mastra/livekit/plugin` entry point. Build your own `voice.AgentSession` and put a Mastra agent in the `llm` slot — the agent loop, tools, and memory run on a remote Mastra server reached over HTTP, so the worker process needs no Mastra app, database, or model provider keys. ([#19136](https://github.com/mastra-ai/mastra/pull/19136))
|
|
161
|
-
|
|
162
|
-
Before, the worker wrapper always owned the LiveKit session:
|
|
163
|
-
|
|
164
|
-
```ts
|
|
165
|
-
import { createLiveKitWorker } from '@mastra/livekit/worker';
|
|
166
|
-
import { mastra } from './index';
|
|
167
|
-
|
|
168
|
-
export default createLiveKitWorker({
|
|
169
|
-
mastra,
|
|
170
|
-
agent: 'support',
|
|
171
|
-
stt: 'deepgram/nova-3',
|
|
172
|
-
tts: 'cartesia/sonic-3',
|
|
173
|
-
});
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
Now you can own the session and keep Mastra as the LLM component:
|
|
177
|
-
|
|
178
|
-
```ts
|
|
179
|
-
import { voice } from '@livekit/agents';
|
|
180
|
-
import { MastraLLM } from '@mastra/livekit/plugin';
|
|
181
|
-
|
|
182
|
-
const session = new voice.AgentSession({
|
|
183
|
-
llm: new MastraLLM({
|
|
184
|
-
remote: { baseUrl: process.env.MASTRA_URL!, agentId: 'support' },
|
|
185
|
-
memory: { thread: callId, resource: userId },
|
|
186
|
-
}),
|
|
187
|
-
stt: 'deepgram/nova-3',
|
|
188
|
-
tts: 'cartesia/sonic-3',
|
|
189
|
-
// Required with `memory`: LiveKit enables preemptive generation by default.
|
|
190
|
-
turnHandling: { preemptiveGeneration: { enabled: false } },
|
|
191
|
-
});
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
`createLiveKitWorker` stays the batteries-included path; the plugin is the composable one. Tools keep running server-side on the Mastra agent, and interrupting the agent aborts the server-side generation.
|
|
195
|
-
|
|
196
|
-
**New transport and helpers**
|
|
197
|
-
- Added `createRemoteAgentReplyGenerator()`: streams replies from a remote Mastra server over HTTP with per-turn abort, LiveKit-typed errors, and a connect + first-token timeout. It also plugs into `createLiveKitWorker`'s `generate` option to run the existing worker against a remote server.
|
|
198
|
-
- Promoted `speakGreeting()`, `waitForAgentDoneSpeaking()`, and `runEndCall()` to public exports of `@mastra/livekit/worker`, so a worker that owns its session can rebuild the greeting and agent-initiated hang-up patterns in a few lines.
|
|
199
|
-
|
|
200
|
-
**Improvements to the existing worker**
|
|
201
|
-
- Interrupted turns now self-heal: when a caller interrupts a reply, nothing is persisted at that moment, and the part the caller actually heard is backfilled into the memory thread on the next turn — so saved transcripts match the call.
|
|
202
|
-
- Added an `onToolCall` hook that fires as each tool call starts mid-reply, the building block for tool-driven side effects such as analytics or hang-up.
|
|
203
|
-
- `onTurnComplete` now receives the turn's token usage as `result.usage`.
|
|
204
|
-
|
|
205
|
-
- Added a `configuration` option to `createLiveKitWorker` — one grouped home for conversation and compliance controls, so these don't each become a separate top-level worker option. It ships with greeting/AI-disclosure controls, a consent model, and agent-initiated hang-up, and is where further compliance controls will land. ([#19136](https://github.com/mastra-ai/mastra/pull/19136))
|
|
206
|
-
|
|
207
|
-
**Greeting and AI disclosure**
|
|
208
|
-
|
|
209
|
-
`configuration.greeting` controls the opening line spoken at call start. Set `allowInterruptions: false` so a legally-required AI disclosure plays through and can't be talked over (EU AI Act Art. 50), `awaitPlayout: true` to hold post-greeting work until it finishes, and `repeatEvery` to re-disclose periodically on long calls (spoken at the next turn boundary, never mid-sentence).
|
|
210
|
-
|
|
211
|
-
```ts
|
|
212
|
-
createLiveKitWorker({
|
|
213
|
-
mastra,
|
|
214
|
-
agent: 'support',
|
|
215
|
-
configuration: {
|
|
216
|
-
greeting: {
|
|
217
|
-
text: 'You are speaking with an AI assistant. This call may be recorded. How can I help?',
|
|
218
|
-
allowInterruptions: false,
|
|
219
|
-
awaitPlayout: true,
|
|
220
|
-
repeatEvery: 3 * 60_000, // re-disclose ~every 3 minutes
|
|
221
|
-
},
|
|
222
|
-
},
|
|
223
|
-
});
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
**Per-tenant greeting**
|
|
227
|
-
|
|
228
|
-
`greeting.text` also accepts a resolver, called once per call with the call context, so one multi-tenant agent can open differently per tenant based on the dispatch metadata:
|
|
229
|
-
|
|
230
|
-
```ts
|
|
231
|
-
greeting: {
|
|
232
|
-
text: ({ metadata }) => `Thanks for calling ${tenantName(metadata)}. You're speaking with an AI assistant.`,
|
|
233
|
-
allowInterruptions: false,
|
|
234
|
-
}
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
**Consent**
|
|
238
|
-
|
|
239
|
-
`configuration.consentPolicy` declares which data-use consents a call needs, as a named, extensible set (starting with `summaryStorage`) rather than one global flag. Declaring the policy enforces nothing by itself: the new `createConsentTool` captures the caller's decision at runtime — add it to your agent and it hands each decision to your own store — and your code enforces the requirement at `onCallEnd` (or before any consent-gated step).
|
|
240
|
-
|
|
241
|
-
```ts
|
|
242
|
-
import { createConsentTool } from '@mastra/livekit';
|
|
243
|
-
|
|
244
|
-
// in your agent's tools:
|
|
245
|
-
recordConsent: createConsentTool({
|
|
246
|
-
items: ['summaryStorage'],
|
|
247
|
-
onGrant: async ({ item, granted, resourceId }) => {
|
|
248
|
-
if (resourceId) await db.saveConsent(resourceId, item, granted);
|
|
249
|
-
},
|
|
250
|
-
}),
|
|
251
|
-
```
|
|
252
|
-
|
|
253
|
-
**Agent-initiated hang-up**
|
|
254
|
-
|
|
255
|
-
`configuration.endCall` lets the agent end the call itself. Add the new `createEndCallTool` to your agent and instruct it to say goodbye and then call the tool; the worker waits for the closing words to finish playing, holds a short audio drain (`drainMs`, default 800ms) so the tail of the goodbye isn't clipped while it's still buffered at the caller, then hangs up — running `onCallEnd` on the way out, exactly as a caller hang-up does. It works on both the agent and workflow reply paths.
|
|
256
|
-
|
|
257
|
-
```ts
|
|
258
|
-
import { createEndCallTool } from '@mastra/livekit';
|
|
259
|
-
|
|
260
|
-
// in your agent's tools:
|
|
261
|
-
endCall: (createEndCallTool(),
|
|
262
|
-
// on the worker:
|
|
263
|
-
createLiveKitWorker({ mastra, agent: 'support', configuration: { endCall: {} } }));
|
|
264
|
-
```
|
|
265
|
-
|
|
266
|
-
**Backwards compatible**
|
|
267
|
-
|
|
268
|
-
The previous top-level `greeting` (string) and `persistGreeting` options still work as deprecated aliases for `configuration.greeting.text` and `configuration.greeting.persist`. When both are set, `configuration.greeting` wins field by field, so existing worker configs keep running unchanged.
|
|
269
|
-
|
|
270
|
-
## 0.2.0
|
|
271
|
-
|
|
272
|
-
### Minor Changes
|
|
273
|
-
|
|
274
|
-
- Added `@mastra/livekit`, a new package that turns Mastra agents into realtime voice agents using LiveKit. ([#17896](https://github.com/mastra-ai/mastra/pull/17896))
|
|
275
|
-
|
|
276
|
-
LiveKit's agents framework runs the audio loop — WebRTC transport, voice activity detection, streaming speech-to-text, semantic turn detection, and barge-in — while your Mastra agent generates every reply with its own model, tools, and memory. When a caller interrupts the agent, LiveKit cancels the in-flight stream and Mastra stops generating.
|
|
277
|
-
|
|
278
|
-
**Build a voice worker**
|
|
279
|
-
- `createLiveKitWorker()` builds a LiveKit worker that answers voice sessions with your Mastra agents; `runLiveKitWorker()` starts its CLI (`dev`/`start`). Both live on the `@mastra/livekit/worker` entry point.
|
|
280
|
-
- `liveKitConnectionRoute()` is an API route that mints LiveKit tokens and dispatches the voice agent into a room; `dispatchVoiceSession()` does the same programmatically for server-initiated sessions like outbound calls. These live on the `@mastra/livekit` entry point, which is safe to import from Mastra server code — it never loads the LiveKit agents runtime.
|
|
281
|
-
|
|
282
|
-
```ts
|
|
283
|
-
// src/mastra/voice-worker.ts
|
|
284
|
-
import { createLiveKitWorker } from '@mastra/livekit/worker';
|
|
285
|
-
import { mastra } from './index';
|
|
286
|
-
|
|
287
|
-
export default createLiveKitWorker({
|
|
288
|
-
mastra,
|
|
289
|
-
agent: 'support',
|
|
290
|
-
stt: 'deepgram/nova-3',
|
|
291
|
-
tts: 'cartesia/sonic-3',
|
|
292
|
-
turnDetection: 'multilingual',
|
|
293
|
-
});
|
|
294
|
-
```
|
|
295
|
-
|
|
296
|
-
**Drive replies with an agent or a workflow**
|
|
297
|
-
|
|
298
|
-
Each turn's reply can come from a Mastra agent (the default) or a Mastra workflow. With a workflow, LiveKit still owns the audio loop and calls into Mastra once per turn, so the workflow runs to completion each turn (no suspend/resume) — pass the transcript in, stream the reply out.
|
|
299
|
-
- `workflow` / `workflowInput` options on `createLiveKitWorker()` drive replies with a workflow.
|
|
300
|
-
- `pipeAgentReplyToWriter(agentStream, writer)` streams an agent's reply from inside a workflow step, forwarding both its words and its tool calls (piping only the text would drop the tool calls).
|
|
301
|
-
- `generate` is an escape hatch to plug in any custom reply generator.
|
|
302
|
-
|
|
303
|
-
```ts
|
|
304
|
-
export default createLiveKitWorker({
|
|
305
|
-
mastra,
|
|
306
|
-
workflow: 'phoneConversation',
|
|
307
|
-
workflowInput: ({ messages }) => ({ turn: messages }),
|
|
308
|
-
replyStep: 'generateResponse',
|
|
309
|
-
stt: 'deepgram/nova-3',
|
|
310
|
-
tts: 'cartesia/sonic-3',
|
|
311
|
-
});
|
|
312
|
-
```
|
|
313
|
-
|
|
314
|
-
**Run work after each turn and at the end of the call**
|
|
315
|
-
- `onTurnComplete` runs once per turn, right after the reply finishes playing. It runs in the background — the worker never waits for it — so you can save memory, update your CRM, or record analytics without adding any delay for the caller or the next reply. It also runs with `result.interrupted: true` when the caller talks over the agent.
|
|
316
|
-
- `onCallEnd` runs once when the call ends. Unlike `onTurnComplete`, the worker waits for it to finish before exiting, so it's the place for end-of-call work like summarizing the whole conversation into long-term memory once.
|
|
317
|
-
- `toolFeedback` speaks a short phrase while a tool runs; `memoryInstance` gives the workflow path a `Memory` instance to open the call's thread and save the greeting, so the saved conversation is complete — greeting included — like the agent path.
|
|
318
|
-
|
|
319
|
-
Both hooks work whether you drive replies with an agent or a workflow.
|
|
320
|
-
|
|
321
|
-
```ts
|
|
322
|
-
createLiveKitWorker({
|
|
323
|
-
mastra,
|
|
324
|
-
agent: 'callCenter',
|
|
325
|
-
onTurnComplete: async ({ result, memory }) => {
|
|
326
|
-
if (memory) await crm.logContact(memory.resource, result.text);
|
|
327
|
-
},
|
|
328
|
-
onCallEnd: async ({ memory }) => {
|
|
329
|
-
// After the caller hangs up: save a lasting summary of the call.
|
|
330
|
-
},
|
|
331
|
-
});
|
|
332
|
-
```
|
|
333
|
-
|
|
334
|
-
**Built-in observability**
|
|
335
|
-
|
|
336
|
-
When the Mastra instance has observability configured, each call opens a `voice call` trace that nests every turn's agent run and adds child spans for LiveKit's speech-to-text, text-to-speech, turn-detection, and LLM latency, closing with a per-model token, character, and audio usage roll-up. On by default; pass `observability: false` to disable.
|
|
337
|
-
|
|
338
|
-
**Studio voice mode**
|
|
339
|
-
|
|
340
|
-
Studio's agent chat gains a voice call mode: when the Mastra server exposes a LiveKit connection route and a voice worker is running, a phone button in the chat composer starts a realtime voice session with the agent. Live captions, agent state (listening, thinking, speaking), and barge-in all surface in the chat, and the conversation lands in the same memory thread as text chat.
|
|
341
|
-
|
|
342
|
-
See the [LiveKit voice guide](https://mastra.ai/docs/voice/livekit) for setup.
|
|
343
|
-
|
|
344
|
-
### Patch Changes
|
|
345
|
-
|
|
346
|
-
- Updated dependencies [[`b291760`](https://github.com/mastra-ai/mastra/commit/b291760df9d6c7e4fc72606c8f0a4af2cf6e946c), [`3ffb8b7`](https://github.com/mastra-ai/mastra/commit/3ffb8b720e90f5e6977129ec1f6707d43c2bebe0), [`6ef59fe`](https://github.com/mastra-ai/mastra/commit/6ef59fef1da52ed8da5fbb2a892c71cf4fb6c739), [`4039488`](https://github.com/mastra-ai/mastra/commit/403948898af7293198d9e8b3e7fb47f623c78b94), [`29b7ea6`](https://github.com/mastra-ai/mastra/commit/29b7ea64e72b5523d5bdcbd34ee03d2b854d54e1), [`b2c9d70`](https://github.com/mastra-ai/mastra/commit/b2c9d70757207fb01a9069549e69b6f0d73a6636), [`a51c63d`](https://github.com/mastra-ai/mastra/commit/a51c63d8ee639e4daeba2a0be093efa6a1b5e52f), [`252f63d`](https://github.com/mastra-ai/mastra/commit/252f63d8fec723955adb2202be2f01a75ad0e69c), [`5ea76a7`](https://github.com/mastra-ai/mastra/commit/5ea76a723d966c72da9aa3ab30ae20276e049765), [`6445560`](https://github.com/mastra-ai/mastra/commit/6445560327045d20b239585fc63fed72e9ce36ec), [`e2b9f33`](https://github.com/mastra-ai/mastra/commit/e2b9f33456fd638eca555f9466c6519d8d049666), [`10959d5`](https://github.com/mastra-ai/mastra/commit/10959d509d824f682d40ff96e05ee044aec3b0e5), [`c547a77`](https://github.com/mastra-ai/mastra/commit/c547a7729bdf64dfc2df29c965046c0712a18f10), [`a0085fa`](https://github.com/mastra-ai/mastra/commit/a0085fa0934e52c37c8c8b3d75a6bb5cd199af36), [`a2ba369`](https://github.com/mastra-ai/mastra/commit/a2ba369e796dfab610f41c6875965b488272fa55), [`ffc3c17`](https://github.com/mastra-ai/mastra/commit/ffc3c17274ea17c11aa6f73d3140649cd7fc8abc), [`81542c1`](https://github.com/mastra-ai/mastra/commit/81542c1835c35bc32f2ce4fa9136ee11993cd299), [`3908e53`](https://github.com/mastra-ai/mastra/commit/3908e53ce04bbea04f5e0c097d7aa298c35fabee), [`cb24ce7`](https://github.com/mastra-ai/mastra/commit/cb24ce76bd16ca88eb6a963f6277f8780e703029), [`02705fd`](https://github.com/mastra-ai/mastra/commit/02705fd2f5a9062210d64ea061adeeb10dc9452e), [`ae51e81`](https://github.com/mastra-ai/mastra/commit/ae51e818825582d42500338dfc1929a082eff0ba), [`6f304ef`](https://github.com/mastra-ai/mastra/commit/6f304ef319e99725e884bdb8d3193c001b6e5964), [`5f9858f`](https://github.com/mastra-ai/mastra/commit/5f9858f791f1137ca7d52d23559fb4568f7a9026)]:
|
|
347
|
-
- @mastra/core@1.50.0
|
|
348
|
-
|
|
349
|
-
## 0.2.0-alpha.0
|
|
350
|
-
|
|
351
|
-
### Minor Changes
|
|
352
|
-
|
|
353
|
-
- Added `@mastra/livekit`, a new package that turns Mastra agents into realtime voice agents using LiveKit. ([#17896](https://github.com/mastra-ai/mastra/pull/17896))
|
|
354
|
-
|
|
355
|
-
LiveKit's agents framework runs the audio loop — WebRTC transport, voice activity detection, streaming speech-to-text, semantic turn detection, and barge-in — while your Mastra agent generates every reply with its own model, tools, and memory. When a caller interrupts the agent, LiveKit cancels the in-flight stream and Mastra stops generating.
|
|
356
|
-
|
|
357
|
-
**Build a voice worker**
|
|
358
|
-
- `createLiveKitWorker()` builds a LiveKit worker that answers voice sessions with your Mastra agents; `runLiveKitWorker()` starts its CLI (`dev`/`start`). Both live on the `@mastra/livekit/worker` entry point.
|
|
359
|
-
- `liveKitConnectionRoute()` is an API route that mints LiveKit tokens and dispatches the voice agent into a room; `dispatchVoiceSession()` does the same programmatically for server-initiated sessions like outbound calls. These live on the `@mastra/livekit` entry point, which is safe to import from Mastra server code — it never loads the LiveKit agents runtime.
|
|
360
|
-
|
|
361
|
-
```ts
|
|
362
|
-
// src/mastra/voice-worker.ts
|
|
363
|
-
import { createLiveKitWorker } from '@mastra/livekit/worker';
|
|
364
|
-
import { mastra } from './index';
|
|
365
|
-
|
|
366
|
-
export default createLiveKitWorker({
|
|
367
|
-
mastra,
|
|
368
|
-
agent: 'support',
|
|
369
|
-
stt: 'deepgram/nova-3',
|
|
370
|
-
tts: 'cartesia/sonic-3',
|
|
371
|
-
turnDetection: 'multilingual',
|
|
372
|
-
});
|
|
373
|
-
```
|
|
374
|
-
|
|
375
|
-
**Drive replies with an agent or a workflow**
|
|
376
|
-
|
|
377
|
-
Each turn's reply can come from a Mastra agent (the default) or a Mastra workflow. With a workflow, LiveKit still owns the audio loop and calls into Mastra once per turn, so the workflow runs to completion each turn (no suspend/resume) — pass the transcript in, stream the reply out.
|
|
378
|
-
- `workflow` / `workflowInput` options on `createLiveKitWorker()` drive replies with a workflow.
|
|
379
|
-
- `pipeAgentReplyToWriter(agentStream, writer)` streams an agent's reply from inside a workflow step, forwarding both its words and its tool calls (piping only the text would drop the tool calls).
|
|
380
|
-
- `generate` is an escape hatch to plug in any custom reply generator.
|
|
381
|
-
|
|
382
|
-
```ts
|
|
383
|
-
export default createLiveKitWorker({
|
|
384
|
-
mastra,
|
|
385
|
-
workflow: 'phoneConversation',
|
|
386
|
-
workflowInput: ({ messages }) => ({ turn: messages }),
|
|
387
|
-
replyStep: 'generateResponse',
|
|
388
|
-
stt: 'deepgram/nova-3',
|
|
389
|
-
tts: 'cartesia/sonic-3',
|
|
390
|
-
});
|
|
391
|
-
```
|
|
392
|
-
|
|
393
|
-
**Run work after each turn and at the end of the call**
|
|
394
|
-
- `onTurnComplete` runs once per turn, right after the reply finishes playing. It runs in the background — the worker never waits for it — so you can save memory, update your CRM, or record analytics without adding any delay for the caller or the next reply. It also runs with `result.interrupted: true` when the caller talks over the agent.
|
|
395
|
-
- `onCallEnd` runs once when the call ends. Unlike `onTurnComplete`, the worker waits for it to finish before exiting, so it's the place for end-of-call work like summarizing the whole conversation into long-term memory once.
|
|
396
|
-
- `toolFeedback` speaks a short phrase while a tool runs; `memoryInstance` gives the workflow path a `Memory` instance to open the call's thread and save the greeting, so the saved conversation is complete — greeting included — like the agent path.
|
|
397
|
-
|
|
398
|
-
Both hooks work whether you drive replies with an agent or a workflow.
|
|
399
|
-
|
|
400
|
-
```ts
|
|
401
|
-
createLiveKitWorker({
|
|
402
|
-
mastra,
|
|
403
|
-
agent: 'callCenter',
|
|
404
|
-
onTurnComplete: async ({ result, memory }) => {
|
|
405
|
-
if (memory) await crm.logContact(memory.resource, result.text);
|
|
406
|
-
},
|
|
407
|
-
onCallEnd: async ({ memory }) => {
|
|
408
|
-
// After the caller hangs up: save a lasting summary of the call.
|
|
409
|
-
},
|
|
410
|
-
});
|
|
411
|
-
```
|
|
412
|
-
|
|
413
|
-
**Built-in observability**
|
|
414
|
-
|
|
415
|
-
When the Mastra instance has observability configured, each call opens a `voice call` trace that nests every turn's agent run and adds child spans for LiveKit's speech-to-text, text-to-speech, turn-detection, and LLM latency, closing with a per-model token, character, and audio usage roll-up. On by default; pass `observability: false` to disable.
|
|
416
|
-
|
|
417
|
-
**Studio voice mode**
|
|
418
|
-
|
|
419
|
-
Studio's agent chat gains a voice call mode: when the Mastra server exposes a LiveKit connection route and a voice worker is running, a phone button in the chat composer starts a realtime voice session with the agent. Live captions, agent state (listening, thinking, speaking), and barge-in all surface in the chat, and the conversation lands in the same memory thread as text chat.
|
|
420
|
-
|
|
421
|
-
See the [LiveKit voice guide](https://mastra.ai/docs/voice/livekit) for setup.
|
|
422
|
-
|
|
423
|
-
### Patch Changes
|
|
424
|
-
|
|
425
|
-
- Updated dependencies [[`a0085fa`](https://github.com/mastra-ai/mastra/commit/a0085fa0934e52c37c8c8b3d75a6bb5cd199af36)]:
|
|
426
|
-
- @mastra/core@1.50.0-alpha.5
|
package/dist/chunk-2E3MTAOA.js
DELETED
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import { ReadableStream } from 'stream/web';
|
|
2
|
-
|
|
3
|
-
// src/constants.ts
|
|
4
|
-
var DEFAULT_LIVEKIT_AGENT_NAME = "mastra-voice";
|
|
5
|
-
|
|
6
|
-
// src/metadata.ts
|
|
7
|
-
function parseSessionMetadata(raw) {
|
|
8
|
-
if (!raw) return {};
|
|
9
|
-
try {
|
|
10
|
-
const parsed = JSON.parse(raw);
|
|
11
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
12
|
-
return parsed;
|
|
13
|
-
}
|
|
14
|
-
} catch {
|
|
15
|
-
}
|
|
16
|
-
return {};
|
|
17
|
-
}
|
|
18
|
-
function serializeSessionMetadata(metadata) {
|
|
19
|
-
return JSON.stringify(metadata);
|
|
20
|
-
}
|
|
21
|
-
function unwrapStepText(output) {
|
|
22
|
-
if (typeof output === "string") return output;
|
|
23
|
-
if (output && typeof output === "object" && output.type === "text-delta") {
|
|
24
|
-
const text = output.payload?.text;
|
|
25
|
-
return typeof text === "string" ? text : void 0;
|
|
26
|
-
}
|
|
27
|
-
return void 0;
|
|
28
|
-
}
|
|
29
|
-
function unwrapStepToolCall(output) {
|
|
30
|
-
if (!output || typeof output !== "object" || output.type !== "tool-call") return void 0;
|
|
31
|
-
const payload = output.payload;
|
|
32
|
-
if (payload && typeof payload.toolCallId === "string" && typeof payload.toolName === "string") {
|
|
33
|
-
return { toolCallId: payload.toolCallId, toolName: payload.toolName, args: payload.args };
|
|
34
|
-
}
|
|
35
|
-
return void 0;
|
|
36
|
-
}
|
|
37
|
-
function pipeAgentReplyToWriter(agentStream, writer) {
|
|
38
|
-
let text = "";
|
|
39
|
-
const forwarded = new ReadableStream({
|
|
40
|
-
start: async (controller) => {
|
|
41
|
-
for await (const chunk of agentStream.fullStream) {
|
|
42
|
-
const type = chunk?.type;
|
|
43
|
-
if (type === "text-delta") {
|
|
44
|
-
const delta = chunk.payload?.text;
|
|
45
|
-
if (typeof delta === "string" && delta) {
|
|
46
|
-
text += delta;
|
|
47
|
-
controller.enqueue(chunk);
|
|
48
|
-
}
|
|
49
|
-
} else if (type === "tool-call") {
|
|
50
|
-
controller.enqueue(chunk);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
controller.close();
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
return forwarded.pipeTo(writer).then(() => text);
|
|
57
|
-
}
|
|
58
|
-
function createWorkflowReplyGenerator(options) {
|
|
59
|
-
const { workflow, workflowInput, replyStep, resultText, toolFeedback, onTurnComplete } = options;
|
|
60
|
-
return async (ctx) => {
|
|
61
|
-
const inputData = await workflowInput(ctx);
|
|
62
|
-
const run = await workflow.createRun();
|
|
63
|
-
const streamArgs = {
|
|
64
|
-
inputData
|
|
65
|
-
};
|
|
66
|
-
if (ctx.tracingContext) streamArgs.tracingContext = ctx.tracingContext;
|
|
67
|
-
if (ctx.requestContext) streamArgs.requestContext = ctx.requestContext;
|
|
68
|
-
const output = run.stream(streamArgs);
|
|
69
|
-
let cancelled = false;
|
|
70
|
-
let replyText = "";
|
|
71
|
-
const toolCalls = [];
|
|
72
|
-
const emitTurnComplete = (interrupted) => {
|
|
73
|
-
if (!onTurnComplete) return;
|
|
74
|
-
const completeCtx = { ...ctx, result: { text: replyText, toolCalls, interrupted } };
|
|
75
|
-
Promise.resolve().then(() => onTurnComplete(completeCtx)).catch((error) => {
|
|
76
|
-
console.warn("@mastra/livekit: onTurnComplete hook threw", error);
|
|
77
|
-
});
|
|
78
|
-
};
|
|
79
|
-
return new ReadableStream({
|
|
80
|
-
start: async (controller) => {
|
|
81
|
-
let streamedAny = false;
|
|
82
|
-
try {
|
|
83
|
-
for await (const chunk of output.fullStream) {
|
|
84
|
-
if (cancelled) break;
|
|
85
|
-
if (chunk.type !== "workflow-step-output") continue;
|
|
86
|
-
const payload = chunk.payload;
|
|
87
|
-
if (replyStep && payload.stepName !== replyStep) continue;
|
|
88
|
-
const text = unwrapStepText(payload.output);
|
|
89
|
-
if (text) {
|
|
90
|
-
streamedAny = true;
|
|
91
|
-
replyText += text;
|
|
92
|
-
controller.enqueue(text);
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
const toolCall = unwrapStepToolCall(payload.output);
|
|
96
|
-
if (toolCall) {
|
|
97
|
-
toolCalls.push(toolCall);
|
|
98
|
-
if (toolFeedback) {
|
|
99
|
-
const filler = toolFeedback(toolCall);
|
|
100
|
-
if (filler) controller.enqueue(filler.endsWith(" ") ? filler : `${filler} `);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
if (!cancelled && !streamedAny && resultText) {
|
|
105
|
-
const finalText = resultText(await output.result);
|
|
106
|
-
if (finalText) {
|
|
107
|
-
replyText += finalText;
|
|
108
|
-
controller.enqueue(finalText);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
if (!cancelled) controller.close();
|
|
112
|
-
emitTurnComplete(cancelled);
|
|
113
|
-
} catch (error) {
|
|
114
|
-
if (cancelled) {
|
|
115
|
-
emitTurnComplete(true);
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
controller.error(error);
|
|
119
|
-
}
|
|
120
|
-
},
|
|
121
|
-
cancel: () => {
|
|
122
|
-
cancelled = true;
|
|
123
|
-
void Promise.resolve(run.cancel()).catch((error) => {
|
|
124
|
-
console.warn("@mastra/livekit: failed to cancel the workflow run on barge-in", error);
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
});
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export { DEFAULT_LIVEKIT_AGENT_NAME, createWorkflowReplyGenerator, parseSessionMetadata, pipeAgentReplyToWriter, serializeSessionMetadata };
|
|
132
|
-
//# sourceMappingURL=chunk-2E3MTAOA.js.map
|
|
133
|
-
//# sourceMappingURL=chunk-2E3MTAOA.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/constants.ts","../src/metadata.ts","../src/workflow-generator.ts"],"names":[],"mappings":"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","file":"chunk-2E3MTAOA.js","sourcesContent":["/** Default LiveKit agent name used for explicit dispatch when none is configured. */\nexport const DEFAULT_LIVEKIT_AGENT_NAME = 'mastra-voice';\n","/**\n * Session metadata passed from the Mastra server to the LiveKit agent worker through\n * LiveKit's job dispatch metadata (a plain string, so this is JSON-serialized).\n */\nexport interface LiveKitSessionMetadata {\n /** Mastra agent to run, by registered key or agent id. */\n agentId?: string;\n /** Memory thread id. Defaults to the LiveKit room name when omitted. */\n threadId?: string;\n /** Memory resource id (typically the end user id). */\n resourceId?: string;\n /** Plain-object entries restored into a RequestContext for agent execution. */\n requestContext?: Record<string, unknown>;\n}\n\nexport function parseSessionMetadata(raw: string | undefined | null): LiveKitSessionMetadata {\n if (!raw) return {};\n try {\n const parsed = JSON.parse(raw);\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\n return parsed as LiveKitSessionMetadata;\n }\n } catch {\n // Dispatch metadata is user-controlled and may not be JSON; treat as absent.\n }\n return {};\n}\n\nexport function serializeSessionMetadata(metadata: LiveKitSessionMetadata): string {\n return JSON.stringify(metadata);\n}\n","import { ReadableStream } from 'node:stream/web';\nimport type { WritableStream } from 'node:stream/web';\nimport type { TracingContext } from '@mastra/core/observability';\nimport type { RequestContext } from '@mastra/core/request-context';\nimport type { Workflow } from '@mastra/core/workflows';\nimport type {\n VoiceReplyGenerator,\n VoiceToolCall,\n VoiceTurnCompleteContext,\n VoiceTurnCompleteHook,\n VoiceTurnContext,\n} from './bridge';\n\nexport interface WorkflowReplyGeneratorOptions {\n /** The Mastra workflow that generates each turn's reply. Runs once per turn (no suspend/resume). */\n workflow: Workflow;\n /**\n * Maps a turn into the workflow's `inputData`. Required — input schemas are caller-defined.\n * A common shape passes the full transcript so the workflow is stateless between turns, e.g.\n * `ctx => ({ history: chatContextToMessages(ctx.chatCtx) })`.\n */\n workflowInput: (ctx: VoiceTurnContext) => unknown | Promise<unknown>;\n /**\n * Only stream text from this step (by id). Defaults to every step that writes text to its\n * `writer`. Set when multiple steps write and only one produces the spoken reply.\n */\n replyStep?: string;\n /**\n * Fallback when the workflow streams no text via `writer`: derive the spoken reply from the\n * final run result. Without this, a non-streaming workflow stays silent. Streaming via the\n * step `writer` is preferred — it gives the caller low time-to-first-token.\n */\n resultText?: (result: unknown) => string | undefined | void;\n /**\n * Speak a short phrase while a tool call runs. Fires only for tool calls the reply step surfaces\n * to its `writer` — use {@link pipeAgentReplyToWriter} (or pipe the agent's `fullStream`) so\n * `tool-call` chunks reach the stream. See {@link MastraVoiceAgentOptions.toolFeedback}.\n */\n toolFeedback?: (toolCall: VoiceToolCall) => string | undefined | void;\n /**\n * Fired off the audio path after the reply streams, fire-and-forget. Carries the produced reply\n * text and any tool calls the workflow surfaced. See {@link MastraVoiceAgentOptions.onTurnComplete}.\n */\n onTurnComplete?: VoiceTurnCompleteHook;\n}\n\n/**\n * Unwraps the text carried by a `workflow-step-output` chunk's `payload.output`. A step that\n * pipes `agent.stream().textStream` into its `writer` produces raw strings; a step built from an\n * agent (`createStep(agent)`) produces full `text-delta` chunks. Returns the text for both, or\n * `undefined` for any other shape.\n */\nexport function unwrapStepText(output: unknown): string | undefined {\n if (typeof output === 'string') return output;\n if (output && typeof output === 'object' && (output as { type?: unknown }).type === 'text-delta') {\n const text = (output as { payload?: { text?: unknown } }).payload?.text;\n return typeof text === 'string' ? text : undefined;\n }\n return undefined;\n}\n\n/**\n * Unwraps a tool call carried by a `workflow-step-output` chunk's `payload.output`. A step that\n * pipes the agent's `fullStream` (rather than just `textStream`) into its `writer` surfaces\n * `tool-call` chunks; this returns the {@link VoiceToolCall} for those, or `undefined` otherwise.\n */\nexport function unwrapStepToolCall(output: unknown): VoiceToolCall | undefined {\n if (!output || typeof output !== 'object' || (output as { type?: unknown }).type !== 'tool-call') return undefined;\n const payload = (output as { payload?: { toolCallId?: unknown; toolName?: unknown; args?: unknown } }).payload;\n if (payload && typeof payload.toolCallId === 'string' && typeof payload.toolName === 'string') {\n return { toolCallId: payload.toolCallId, toolName: payload.toolName, args: payload.args };\n }\n return undefined;\n}\n\n/** The minimal shape of a Mastra agent stream consumed by {@link pipeAgentReplyToWriter}. */\nexport interface AgentReplyStreamLike {\n fullStream: AsyncIterable<unknown>;\n}\n\n/**\n * Streams a Mastra agent's reply into a workflow step's `writer` for the LiveKit workflow\n * entrypoint — the recommended way to drive a turn's reply from a step.\n *\n * It forwards the agent's text deltas (so text-to-speech starts before the full reply is ready)\n * AND its `tool-call` chunks (so {@link WorkflowReplyGeneratorOptions.toolFeedback} fires and\n * {@link WorkflowReplyGeneratorOptions.onTurnComplete}'s `result.toolCalls` is populated). This is\n * the difference from piping only `agent.stream().textStream`, which silently drops tool calls.\n * Other chunk types (reasoning, tool results, lifecycle) are not forwarded, keeping the spoken\n * stream clean. Returns the accumulated reply text for the step to return.\n *\n * Pass the step's `abortSignal` to `agent.stream(...)` so barge-in stops generation promptly.\n *\n * ```ts\n * const generateResponse = createStep({\n * // ...\n * execute: async ({ inputData, mastra, writer, abortSignal }) => {\n * const stream = await mastra.getAgent('callCenter').stream(inputData.turn, { abortSignal });\n * const reply = await pipeAgentReplyToWriter(stream, writer);\n * return { reply };\n * },\n * });\n * ```\n */\nexport function pipeAgentReplyToWriter(\n agentStream: AgentReplyStreamLike,\n writer: WritableStream<unknown>,\n): Promise<string> {\n let text = '';\n // Re-emit only the chunks the voice path cares about, then pipe through the step writer — which\n // reuses pipeTo's proven backpressure + close handling rather than driving the writer by hand.\n const forwarded = new ReadableStream<unknown>({\n start: async controller => {\n for await (const chunk of agentStream.fullStream) {\n const type = (chunk as { type?: unknown })?.type;\n if (type === 'text-delta') {\n const delta = (chunk as { payload?: { text?: unknown } }).payload?.text;\n if (typeof delta === 'string' && delta) {\n text += delta;\n controller.enqueue(chunk);\n }\n } else if (type === 'tool-call') {\n controller.enqueue(chunk);\n }\n }\n controller.close();\n },\n });\n return forwarded.pipeTo(writer).then(() => text);\n}\n\n/**\n * A {@link VoiceReplyGenerator} backed by a Mastra workflow. Per turn it starts a fresh run to\n * completion (LiveKit owns the turn boundary, so there is no suspend/resume and no conversation\n * state carried between turns) and streams the text its steps write to their `writer`.\n *\n * A workflow's own stream emits structured step events, not token deltas — text only surfaces\n * when a step pipes it into the injected `writer`, arriving as `workflow-step-output` chunks. The\n * simplest correct reply step calls {@link pipeAgentReplyToWriter}, which forwards both text and\n * tool calls (and passes the step's `abortSignal` through `agent.stream` so barge-in stops\n * generation promptly). Piping only `agent.stream(...).textStream.pipeTo(writer)` works for text\n * but silently drops tool calls, so {@link WorkflowReplyGeneratorOptions.toolFeedback} and\n * {@link WorkflowReplyGeneratorOptions.onTurnComplete}'s `result.toolCalls` stay empty.\n */\nexport function createWorkflowReplyGenerator(options: WorkflowReplyGeneratorOptions): VoiceReplyGenerator {\n const { workflow, workflowInput, replyStep, resultText, toolFeedback, onTurnComplete } = options;\n return async ctx => {\n const inputData = await workflowInput(ctx);\n const run = await workflow.createRun();\n\n const streamArgs: { inputData: unknown; tracingContext?: TracingContext; requestContext?: RequestContext } = {\n inputData,\n };\n if (ctx.tracingContext) streamArgs.tracingContext = ctx.tracingContext;\n // Forward the per-session request context so workflow steps see it, mirroring the agent path.\n if (ctx.requestContext) streamArgs.requestContext = ctx.requestContext;\n const output = run.stream(streamArgs);\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\n // Fire-and-forget after the reply has streamed: off the audio path and not awaited, so it\n // never delays the next turn. Errors are logged, not thrown. Mirrors createAgentReplyGenerator.\n const emitTurnComplete = (interrupted: boolean) => {\n if (!onTurnComplete) return;\n const completeCtx: VoiceTurnCompleteContext = { ...ctx, result: { text: replyText, toolCalls, interrupted } };\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 let streamedAny = false;\n try {\n for await (const chunk of output.fullStream) {\n if (cancelled) break;\n if (chunk.type !== 'workflow-step-output') continue;\n const payload = chunk.payload as { output?: unknown; stepName?: unknown };\n if (replyStep && payload.stepName !== replyStep) continue;\n const text = unwrapStepText(payload.output);\n if (text) {\n streamedAny = true;\n replyText += text;\n controller.enqueue(text);\n continue;\n }\n // A tool call only surfaces when the step pipes the agent's fullStream; when it does,\n // mirror the agent path — record it and speak any toolFeedback filler.\n const toolCall = unwrapStepToolCall(payload.output);\n if (toolCall) {\n toolCalls.push(toolCall);\n if (toolFeedback) {\n const filler = toolFeedback(toolCall);\n if (filler) controller.enqueue(filler.endsWith(' ') ? filler : `${filler} `);\n }\n }\n }\n if (!cancelled && !streamedAny && resultText) {\n const finalText = resultText(await output.result);\n if (finalText) {\n replyText += finalText;\n controller.enqueue(finalText);\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 run; that's not a failure — the turn still completed\n // (interrupted), so the hook still fires for memory reconciliation.\n if (cancelled) {\n emitTurnComplete(true);\n return;\n }\n controller.error(error);\n }\n },\n cancel: () => {\n cancelled = true;\n // Barge-in fires this synchronously; swallow any rejection from the run cancellation so a\n // failed cancel can't surface as an unhandled promise rejection.\n void Promise.resolve(run.cancel()).catch(error => {\n console.warn('@mastra/livekit: failed to cancel the workflow run on barge-in', error);\n });\n },\n });\n };\n}\n"]}
|