@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.
@@ -1,568 +0,0 @@
1
- import { ReadableStream } from 'stream/web';
2
- import { APIStatusError, APIConnectionError, APITimeoutError, APIError, voice, llm } from '@livekit/agents';
3
- import { RequestContext } from '@mastra/core/request-context';
4
-
5
- // src/messages.ts
6
- var LIVEKIT_INSTRUCTIONS_MESSAGE_ID = "lk.agent_task.instructions";
7
- function textOfMessage(message) {
8
- const parts = [];
9
- for (const part of message.content) {
10
- if (typeof part === "string") {
11
- parts.push(part);
12
- } else if (part.type === "instructions") {
13
- parts.push(part.value);
14
- } else if (part.type === "audio_content" && part.transcript) {
15
- parts.push(part.transcript);
16
- }
17
- }
18
- return parts.join("\n").trim();
19
- }
20
- function toVoiceTurnMessage(item) {
21
- if (item.type !== "message") return void 0;
22
- const content = textOfMessage(item);
23
- if (!content) return void 0;
24
- const id = item.id;
25
- if (item.role === "user") return { role: "user", content, id };
26
- if (item.role === "assistant") return { role: "assistant", content, id };
27
- return { role: "system", content, id };
28
- }
29
- function extractNewTurnMessages(chatCtx) {
30
- const items = chatCtx.items;
31
- let lastAssistantIdx = -1;
32
- for (let i = items.length - 1; i >= 0; i--) {
33
- const item = items[i];
34
- if (item?.type === "message" && item.role === "assistant") {
35
- lastAssistantIdx = i;
36
- break;
37
- }
38
- }
39
- const lastAssistant = lastAssistantIdx >= 0 ? items[lastAssistantIdx] : void 0;
40
- const healInterrupted = lastAssistant?.type === "message" && lastAssistant.role === "assistant" && lastAssistant.interrupted;
41
- const startIdx = healInterrupted ? lastAssistantIdx : lastAssistantIdx + 1;
42
- const messages = [];
43
- for (const item of items.slice(startIdx)) {
44
- if (item.type === "message" && item.id === LIVEKIT_INSTRUCTIONS_MESSAGE_ID) continue;
45
- const message = toVoiceTurnMessage(item);
46
- if (message) messages.push(message);
47
- }
48
- return messages;
49
- }
50
- function chatContextToMessages(chatCtx) {
51
- const withoutInstructions = chatCtx.copy({ excludeInstructions: true, excludeFunctionCall: true });
52
- const messages = [];
53
- for (const item of withoutInstructions.items) {
54
- const message = toVoiceTurnMessage(item);
55
- if (message) messages.push(message);
56
- }
57
- return messages;
58
- }
59
- var DEFAULT_INSTRUCTIONS = "You are a helpful voice assistant powered by a Mastra agent.";
60
- var DEFAULT_DISCLOSURE_REMINDER = "Just a reminder, you're speaking with an AI assistant.";
61
- var DisclosureReminder = class {
62
- constructor(everyMs, text, now = Date.now()) {
63
- this.everyMs = everyMs;
64
- this.text = text;
65
- this.lastAt = now;
66
- }
67
- everyMs;
68
- text;
69
- lastAt;
70
- /** Call once per turn: the reminder text if it's due, else `undefined`. Does not reset the clock —
71
- * call {@link DisclosureReminder.markDelivered} once the reminder is actually threaded into the
72
- * outgoing reply, so a reminder that never makes it out isn't silently skipped for a full interval. */
73
- due(now = Date.now()) {
74
- if (now - this.lastAt < this.everyMs) return void 0;
75
- return this.text;
76
- }
77
- /** Resets the clock. Call only once the reminder text from {@link due} was actually emitted. */
78
- markDelivered(now = Date.now()) {
79
- this.lastAt = now;
80
- }
81
- };
82
- function prependText(source, text) {
83
- const prefix = text.endsWith(" ") ? text : `${text} `;
84
- const reader = source.getReader();
85
- return new ReadableStream({
86
- start(controller) {
87
- controller.enqueue(prefix);
88
- },
89
- async pull(controller) {
90
- try {
91
- const { done, value } = await reader.read();
92
- if (done) controller.close();
93
- else controller.enqueue(value);
94
- } catch (error) {
95
- controller.error(error);
96
- }
97
- },
98
- cancel(reason) {
99
- return reader.cancel(reason);
100
- }
101
- });
102
- }
103
- function mapTurnUsage(usage) {
104
- if (!usage || typeof usage !== "object") return void 0;
105
- const u = usage;
106
- const totalOf = (v) => {
107
- if (typeof v === "number") return v;
108
- if (v && typeof v === "object" && typeof v.total === "number") {
109
- return v.total;
110
- }
111
- return void 0;
112
- };
113
- const cacheReadOf = (v) => v && typeof v === "object" && typeof v.cacheRead === "number" ? v.cacheRead : void 0;
114
- const promptTokens = totalOf(u.inputTokens) ?? 0;
115
- const completionTokens = totalOf(u.outputTokens) ?? 0;
116
- const promptCachedTokens = (typeof u.cachedInputTokens === "number" ? u.cachedInputTokens : cacheReadOf(u.inputTokens)) ?? 0;
117
- const totalTokens = typeof u.totalTokens === "number" ? u.totalTokens : promptTokens + completionTokens;
118
- if (promptTokens === 0 && completionTokens === 0 && totalTokens === 0 && promptCachedTokens === 0) {
119
- return void 0;
120
- }
121
- return { promptTokens, completionTokens, promptCachedTokens, totalTokens };
122
- }
123
- function createAgentReplyGenerator(options) {
124
- const { agent, streamOptions, toolFeedback, onToolCall, onTurnComplete } = options;
125
- return (ctx) => {
126
- if (ctx.messages.length === 0) return null;
127
- const abortController = new AbortController();
128
- const mergedOptions = {
129
- ...streamOptions,
130
- abortSignal: abortController.signal
131
- };
132
- if (ctx.memory) mergedOptions.memory = ctx.memory;
133
- if (ctx.requestContext) mergedOptions.requestContext = ctx.requestContext;
134
- let cancelled = false;
135
- let replyText = "";
136
- const toolCalls = [];
137
- let usage;
138
- const emitTurnComplete = (interrupted) => {
139
- if (!onTurnComplete) return;
140
- const completeCtx = {
141
- ...ctx,
142
- result: { text: replyText, toolCalls, interrupted, usage }
143
- };
144
- Promise.resolve().then(() => onTurnComplete(completeCtx)).catch((error) => {
145
- console.warn("@mastra/livekit: onTurnComplete hook threw", error);
146
- });
147
- };
148
- return new ReadableStream({
149
- start: async (controller) => {
150
- try {
151
- const result = await agent.stream(ctx.messages, mergedOptions);
152
- for await (const chunk of result.fullStream) {
153
- if (cancelled) break;
154
- if (chunk.type === "text-delta") {
155
- if (chunk.payload.text) {
156
- replyText += chunk.payload.text;
157
- controller.enqueue(chunk.payload.text);
158
- }
159
- } else if (chunk.type === "tool-call") {
160
- const toolCall = {
161
- toolCallId: chunk.payload.toolCallId,
162
- toolName: chunk.payload.toolName,
163
- args: chunk.payload.args
164
- };
165
- toolCalls.push(toolCall);
166
- try {
167
- onToolCall?.(toolCall);
168
- } catch (error) {
169
- console.warn("@mastra/livekit: onToolCall hook threw", error);
170
- }
171
- if (toolFeedback) {
172
- let filler;
173
- try {
174
- filler = toolFeedback(toolCall);
175
- } catch (error) {
176
- console.warn("@mastra/livekit: toolFeedback hook threw", error);
177
- }
178
- if (filler) controller.enqueue(filler.endsWith(" ") ? filler : `${filler} `);
179
- }
180
- } else if (chunk.type === "finish") {
181
- const output = chunk.payload.output;
182
- const turnUsage = mapTurnUsage(output?.usage);
183
- if (turnUsage) {
184
- usage = turnUsage;
185
- try {
186
- ctx.onUsage?.(turnUsage);
187
- } catch (error) {
188
- console.warn("@mastra/livekit: onUsage hook threw", error);
189
- }
190
- }
191
- } else if (chunk.type === "error") {
192
- const error = chunk.payload.error;
193
- throw error instanceof Error ? error : new Error(String(error));
194
- }
195
- }
196
- if (!cancelled) controller.close();
197
- emitTurnComplete(cancelled);
198
- } catch (error) {
199
- if (cancelled || abortController.signal.aborted) {
200
- emitTurnComplete(true);
201
- return;
202
- }
203
- controller.error(error);
204
- }
205
- },
206
- cancel: () => {
207
- cancelled = true;
208
- abortController.abort();
209
- }
210
- });
211
- };
212
- }
213
- function toRequestContext(value) {
214
- if (!value) return void 0;
215
- if (value instanceof RequestContext) return value;
216
- return new RequestContext(Object.entries(value));
217
- }
218
- var MastraPlaceholderLLM = class extends llm.LLM {
219
- label() {
220
- return "mastra.MastraVoiceAgent";
221
- }
222
- get model() {
223
- return "mastra-agent";
224
- }
225
- get provider() {
226
- return "mastra";
227
- }
228
- chat() {
229
- throw new Error(
230
- "@mastra/livekit: reply generation runs through the Mastra agent via llmNode; the placeholder LLM cannot be used for inference."
231
- );
232
- }
233
- };
234
- var MastraVoiceAgent = class extends voice.Agent {
235
- mastraAgent;
236
- memory;
237
- requestContext;
238
- streamOptions;
239
- replyGenerator;
240
- reminder;
241
- constructor(options) {
242
- if (options.agent && options.generate) {
243
- throw new Error(
244
- "@mastra/livekit: MastraVoiceAgent requires `agent` or `generate`, not both \u2014 they are mutually exclusive reply sources."
245
- );
246
- }
247
- super({
248
- id: options.id,
249
- instructions: options.instructions ?? DEFAULT_INSTRUCTIONS,
250
- stt: options.stt,
251
- vad: options.vad,
252
- llm: new MastraPlaceholderLLM(),
253
- tts: options.tts,
254
- turnHandling: options.turnHandling
255
- });
256
- this.memory = options.memory ?? false;
257
- this.requestContext = toRequestContext(options.requestContext);
258
- this.streamOptions = options.streamOptions;
259
- if (options.greetingReminder) {
260
- this.reminder = new DisclosureReminder(
261
- options.greetingReminder.everyMs,
262
- options.greetingReminder.text?.trim() || DEFAULT_DISCLOSURE_REMINDER
263
- );
264
- }
265
- if (options.generate) {
266
- this.replyGenerator = options.generate;
267
- } else if (options.agent) {
268
- this.mastraAgent = options.agent;
269
- this.replyGenerator = createAgentReplyGenerator({
270
- agent: options.agent,
271
- streamOptions: options.streamOptions,
272
- toolFeedback: options.toolFeedback,
273
- onToolCall: options.onToolCall,
274
- onTurnComplete: options.onTurnComplete
275
- });
276
- } else {
277
- throw new Error("@mastra/livekit: MastraVoiceAgent requires `agent` or `generate`.");
278
- }
279
- }
280
- async llmNode(chatCtx, _toolCtx, _modelSettings) {
281
- const messages = this.memory === false ? chatContextToMessages(chatCtx) : extractNewTurnMessages(chatCtx);
282
- if (messages.length === 0) return null;
283
- const reply = await this.replyGenerator({
284
- messages,
285
- chatCtx,
286
- memory: this.memory,
287
- requestContext: this.requestContext,
288
- tracingContext: this.streamOptions?.tracingContext
289
- });
290
- if (!reply) return null;
291
- const reminder = this.reminder?.due();
292
- if (!reminder) return reply;
293
- this.reminder?.markDelivered();
294
- return prependText(reply, reminder);
295
- }
296
- };
297
- function createMastraVoiceAgent(options) {
298
- return new MastraVoiceAgent(options);
299
- }
300
-
301
- // src/remote.ts
302
- var DEFAULT_API_PREFIX = "/api";
303
- var DEFAULT_REMOTE_TIMEOUT_MS = 1e4;
304
- var DEFAULT_REMOTE_RETRIES = 2;
305
- var HITL_UNSUPPORTED_MESSAGE = "@mastra/livekit: the agent requested tool approval or suspended a tool call; human-in-the-loop flows (approve-tool-call / resume-stream) are not supported on the voice path. Remove requireApproval or suspend from the tools this agent uses on voice calls.";
306
- function trimTrailingSlash(url) {
307
- return url.endsWith("/") ? url.slice(0, -1) : url;
308
- }
309
- function toMessage(error) {
310
- return error instanceof Error ? error.message : String(error);
311
- }
312
- async function resolveHeaders(headers) {
313
- if (!headers) return {};
314
- if (typeof headers === "function") return await headers() ?? {};
315
- return headers;
316
- }
317
- function serializeRequestContext(requestContext) {
318
- if (!requestContext) return void 0;
319
- if (requestContext instanceof RequestContext) return Object.fromEntries(requestContext.entries());
320
- return requestContext;
321
- }
322
- async function safeReadBody(response) {
323
- try {
324
- const text = await response.text();
325
- if (!text) return null;
326
- try {
327
- const parsed = JSON.parse(text);
328
- return parsed && typeof parsed === "object" ? parsed : { message: String(parsed) };
329
- } catch {
330
- return { message: text };
331
- }
332
- } catch {
333
- return null;
334
- }
335
- }
336
- async function* readMastraSSE(body, signal) {
337
- const reader = body.getReader();
338
- const decoder = new TextDecoder();
339
- let buffer = "";
340
- const onAbort = () => void reader.cancel().catch(() => {
341
- });
342
- if (signal.aborted) {
343
- void reader.cancel().catch(() => {
344
- });
345
- return;
346
- }
347
- signal.addEventListener("abort", onAbort, { once: true });
348
- try {
349
- for (; ; ) {
350
- const { done, value } = await reader.read();
351
- if (done) break;
352
- buffer += decoder.decode(value, { stream: true });
353
- const events = buffer.split("\n\n");
354
- buffer = events.pop() ?? "";
355
- for (const event of events) {
356
- if (!event.startsWith("data:")) continue;
357
- const data = event.slice(event.startsWith("data: ") ? 6 : 5).trim();
358
- if (data === "[DONE]") return;
359
- if (!data) continue;
360
- let json;
361
- try {
362
- json = JSON.parse(data);
363
- } catch {
364
- continue;
365
- }
366
- if (json && typeof json === "object") yield json;
367
- }
368
- }
369
- } finally {
370
- signal.removeEventListener("abort", onAbort);
371
- try {
372
- reader.releaseLock();
373
- } catch {
374
- }
375
- }
376
- }
377
- function createRemoteAgentReplyGenerator(options) {
378
- const {
379
- baseUrl,
380
- agentId,
381
- apiPrefix = DEFAULT_API_PREFIX,
382
- headers,
383
- fetch: fetchImpl = globalThis.fetch,
384
- timeoutMs = DEFAULT_REMOTE_TIMEOUT_MS,
385
- retries = DEFAULT_REMOTE_RETRIES,
386
- body: extraBody,
387
- toolFeedback,
388
- onToolCall,
389
- onTurnComplete
390
- } = options;
391
- if (!fetchImpl) {
392
- throw new Error("@mastra/livekit: no fetch implementation available; pass `fetch` or run on Node \u2265 22.");
393
- }
394
- const url = `${trimTrailingSlash(baseUrl)}${apiPrefix}/agents/${agentId}/stream`;
395
- return (ctx) => {
396
- if (ctx.messages.length === 0) return null;
397
- let currentAbortController;
398
- let cancelled = false;
399
- let replyText = "";
400
- const toolCalls = [];
401
- let usage;
402
- const emitTurnComplete = (interrupted) => {
403
- if (!onTurnComplete) return;
404
- const completeCtx = {
405
- ...ctx,
406
- result: { text: replyText, toolCalls, interrupted, usage }
407
- };
408
- Promise.resolve().then(() => onTurnComplete(completeCtx)).catch((error) => {
409
- console.warn("@mastra/livekit: onTurnComplete hook threw", error);
410
- });
411
- };
412
- const requestBody = {
413
- messages: ctx.messages,
414
- // The server schema requires a resource when memory is present; default it to the thread id,
415
- // matching the worker's own thread bootstrap.
416
- memory: ctx.memory ? { thread: ctx.memory.thread, resource: ctx.memory.resource ?? ctx.memory.thread } : void 0,
417
- requestContext: serializeRequestContext(ctx.requestContext),
418
- ...extraBody
419
- };
420
- return new ReadableStream({
421
- start: async (controller) => {
422
- let retryable = true;
423
- try {
424
- for (let attempt = 0; ; attempt++) {
425
- const abortController = new AbortController();
426
- currentAbortController = abortController;
427
- let timedOut = false;
428
- let watchdog;
429
- const clearWatchdog = () => {
430
- if (watchdog) {
431
- clearTimeout(watchdog);
432
- watchdog = void 0;
433
- }
434
- };
435
- try {
436
- watchdog = setTimeout(() => {
437
- timedOut = true;
438
- abortController.abort();
439
- }, timeoutMs);
440
- watchdog.unref?.();
441
- const resolvedHeaders = await resolveHeaders(headers);
442
- if (cancelled) {
443
- clearWatchdog();
444
- break;
445
- }
446
- const response = await fetchImpl(url, {
447
- method: "POST",
448
- headers: { "content-type": "application/json", accept: "text/event-stream", ...resolvedHeaders },
449
- body: JSON.stringify(requestBody),
450
- signal: abortController.signal
451
- });
452
- if (!response.ok) {
453
- const errorBody = await safeReadBody(response);
454
- throw new APIStatusError({
455
- message: `@mastra/livekit: Mastra agent stream request failed with status ${response.status}`,
456
- options: { statusCode: response.status, body: errorBody, retryable }
457
- });
458
- }
459
- if (!response.body) {
460
- throw new APIConnectionError({
461
- message: "@mastra/livekit: Mastra agent stream returned an empty response body",
462
- options: { retryable }
463
- });
464
- }
465
- for await (const chunk of readMastraSSE(
466
- response.body,
467
- abortController.signal
468
- )) {
469
- if (cancelled) break;
470
- retryable = false;
471
- const payload = chunk.payload ?? {};
472
- switch (chunk.type) {
473
- case "text-delta": {
474
- const text = payload.text;
475
- if (typeof text === "string" && text) {
476
- clearWatchdog();
477
- replyText += text;
478
- controller.enqueue(text);
479
- }
480
- break;
481
- }
482
- case "tool-call": {
483
- clearWatchdog();
484
- const toolCall = {
485
- toolCallId: String(payload.toolCallId ?? ""),
486
- toolName: String(payload.toolName ?? ""),
487
- args: payload.args
488
- };
489
- toolCalls.push(toolCall);
490
- try {
491
- onToolCall?.(toolCall);
492
- } catch (error) {
493
- console.warn("@mastra/livekit: onToolCall hook threw", error);
494
- }
495
- if (toolFeedback) {
496
- let filler;
497
- try {
498
- filler = toolFeedback(toolCall);
499
- } catch (error) {
500
- console.warn("@mastra/livekit: toolFeedback hook threw", error);
501
- }
502
- if (filler) controller.enqueue(filler.endsWith(" ") ? filler : `${filler} `);
503
- }
504
- break;
505
- }
506
- case "finish": {
507
- clearWatchdog();
508
- const output = payload.output;
509
- const turnUsage = mapTurnUsage(output?.usage);
510
- if (turnUsage) {
511
- usage = turnUsage;
512
- try {
513
- ctx.onUsage?.(turnUsage);
514
- } catch (error) {
515
- console.warn("@mastra/livekit: onUsage hook threw", error);
516
- }
517
- }
518
- break;
519
- }
520
- case "tool-call-approval":
521
- case "tool-call-suspended":
522
- throw new Error(HITL_UNSUPPORTED_MESSAGE);
523
- case "error": {
524
- const error = payload.error;
525
- throw error instanceof Error ? error : new Error(String(error));
526
- }
527
- }
528
- }
529
- clearWatchdog();
530
- break;
531
- } catch (error) {
532
- clearWatchdog();
533
- if (cancelled) break;
534
- let typed;
535
- if (timedOut) {
536
- typed = new APITimeoutError({ options: { retryable } });
537
- } else if (error instanceof APIError) {
538
- typed = error;
539
- } else if (retryable) {
540
- typed = new APIConnectionError({ message: toMessage(error), options: { retryable } });
541
- } else {
542
- throw error;
543
- }
544
- if (typed.retryable && attempt < retries) continue;
545
- throw typed;
546
- }
547
- }
548
- if (!cancelled) controller.close();
549
- emitTurnComplete(cancelled);
550
- } catch (error) {
551
- if (cancelled) {
552
- emitTurnComplete(true);
553
- return;
554
- }
555
- controller.error(error);
556
- }
557
- },
558
- cancel: () => {
559
- cancelled = true;
560
- currentAbortController?.abort();
561
- }
562
- });
563
- };
564
- }
565
-
566
- export { chatContextToMessages, createAgentReplyGenerator, createMastraVoiceAgent, createRemoteAgentReplyGenerator, extractNewTurnMessages };
567
- //# sourceMappingURL=chunk-4O7IN74Y.js.map
568
- //# sourceMappingURL=chunk-4O7IN74Y.js.map