@mastra/livekit 0.3.0 → 0.3.1-alpha.1
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-BZ7eyB1q.cjs +664 -0
- package/dist/remote-BZ7eyB1q.cjs.map +1 -0
- package/dist/remote-D7n50m8S.js +629 -0
- package/dist/remote-D7n50m8S.js.map +1 -0
- package/dist/worker-entry.cjs +623 -538
- package/dist/worker-entry.cjs.map +1 -1
- package/dist/worker-entry.d.ts +2 -1
- package/dist/worker-entry.d.ts.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
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
import { ReadableStream } from "stream/web";
|
|
2
|
+
import { APIConnectionError, APIError, APIStatusError, APITimeoutError, llm, voice } from "@livekit/agents";
|
|
3
|
+
import { RequestContext } from "@mastra/core/request-context";
|
|
4
|
+
function textOfMessage(message) {
|
|
5
|
+
const parts = [];
|
|
6
|
+
for (const part of message.content) if (typeof part === "string") parts.push(part);
|
|
7
|
+
else if (part.type === "instructions") parts.push(part.value);
|
|
8
|
+
else if (part.type === "audio_content" && part.transcript) parts.push(part.transcript);
|
|
9
|
+
return parts.join("\n").trim();
|
|
10
|
+
}
|
|
11
|
+
function toVoiceTurnMessage(item) {
|
|
12
|
+
if (item.type !== "message") return void 0;
|
|
13
|
+
const content = textOfMessage(item);
|
|
14
|
+
if (!content) return void 0;
|
|
15
|
+
const id = item.id;
|
|
16
|
+
if (item.role === "user") return {
|
|
17
|
+
role: "user",
|
|
18
|
+
content,
|
|
19
|
+
id
|
|
20
|
+
};
|
|
21
|
+
if (item.role === "assistant") return {
|
|
22
|
+
role: "assistant",
|
|
23
|
+
content,
|
|
24
|
+
id
|
|
25
|
+
};
|
|
26
|
+
return {
|
|
27
|
+
role: "system",
|
|
28
|
+
content,
|
|
29
|
+
id
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Extracts only the messages added since the agent last spoke. Used when Mastra Memory is
|
|
34
|
+
* the source of truth for conversation history: prior turns are already persisted in the
|
|
35
|
+
* thread, so re-sending them would duplicate history.
|
|
36
|
+
*
|
|
37
|
+
* Two extensions over the naive "slice after the last assistant message":
|
|
38
|
+
*
|
|
39
|
+
* - **Interrupted-turn self-heal:** when the last assistant message was cut off by barge-in
|
|
40
|
+
* (`interrupted: true`), the server never persisted it — aborted runs skip persistence — so
|
|
41
|
+
* its heard-only text is missing from the thread. Re-send that fragment (ordered first) this
|
|
42
|
+
* turn to backfill it. It stops being "the last assistant message" once a full reply lands,
|
|
43
|
+
* so each interrupted fragment is sent exactly once, on the following turn.
|
|
44
|
+
* - **Instructions filter:** LiveKit injects the customer Agent's `instructions` as a
|
|
45
|
+
* leading `system` message ({@link LIVEKIT_INSTRUCTIONS_MESSAGE_ID}); the server-side Mastra
|
|
46
|
+
* agent owns its own system prompt, so drop it (it would otherwise ship on the first turn,
|
|
47
|
+
* before any assistant message).
|
|
48
|
+
*/
|
|
49
|
+
function extractNewTurnMessages(chatCtx) {
|
|
50
|
+
const items = chatCtx.items;
|
|
51
|
+
let lastAssistantIdx = -1;
|
|
52
|
+
for (let i = items.length - 1; i >= 0; i--) {
|
|
53
|
+
const item = items[i];
|
|
54
|
+
if (item?.type === "message" && item.role === "assistant") {
|
|
55
|
+
lastAssistantIdx = i;
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const lastAssistant = lastAssistantIdx >= 0 ? items[lastAssistantIdx] : void 0;
|
|
60
|
+
const startIdx = lastAssistant?.type === "message" && lastAssistant.role === "assistant" && lastAssistant.interrupted ? lastAssistantIdx : lastAssistantIdx + 1;
|
|
61
|
+
const messages = [];
|
|
62
|
+
for (const item of items.slice(startIdx)) {
|
|
63
|
+
if (item.type === "message" && item.id === "lk.agent_task.instructions") continue;
|
|
64
|
+
const message = toVoiceTurnMessage(item);
|
|
65
|
+
if (message) messages.push(message);
|
|
66
|
+
}
|
|
67
|
+
return messages;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Converts the full LiveKit chat context to Mastra messages. Used when the bridge runs
|
|
71
|
+
* without Mastra Memory and LiveKit's in-session context is the only history. The agent's
|
|
72
|
+
* LiveKit-level instructions are excluded — the Mastra agent applies its own instructions.
|
|
73
|
+
*/
|
|
74
|
+
function chatContextToMessages(chatCtx) {
|
|
75
|
+
const withoutInstructions = chatCtx.copy({
|
|
76
|
+
excludeInstructions: true,
|
|
77
|
+
excludeFunctionCall: true
|
|
78
|
+
});
|
|
79
|
+
const messages = [];
|
|
80
|
+
for (const item of withoutInstructions.items) {
|
|
81
|
+
const message = toVoiceTurnMessage(item);
|
|
82
|
+
if (message) messages.push(message);
|
|
83
|
+
}
|
|
84
|
+
return messages;
|
|
85
|
+
}
|
|
86
|
+
//#endregion
|
|
87
|
+
//#region src/bridge.ts
|
|
88
|
+
const DEFAULT_INSTRUCTIONS = "You are a helpful voice assistant powered by a Mastra agent.";
|
|
89
|
+
/**
|
|
90
|
+
* Tracks periodic AI re-disclosure for a single call. `due()` returns the reminder text once
|
|
91
|
+
* `everyMs` has elapsed since the last disclosure (resetting the clock), otherwise `undefined`.
|
|
92
|
+
* Time is injectable so the interval logic is deterministically testable.
|
|
93
|
+
*/
|
|
94
|
+
var DisclosureReminder = class {
|
|
95
|
+
everyMs;
|
|
96
|
+
text;
|
|
97
|
+
lastAt;
|
|
98
|
+
constructor(everyMs, text, now = Date.now()) {
|
|
99
|
+
this.everyMs = everyMs;
|
|
100
|
+
this.text = text;
|
|
101
|
+
this.lastAt = now;
|
|
102
|
+
}
|
|
103
|
+
/** Call once per turn: the reminder text if it's due, else `undefined`. Does not reset the clock —
|
|
104
|
+
* call {@link DisclosureReminder.markDelivered} once the reminder is actually threaded into the
|
|
105
|
+
* outgoing reply, so a reminder that never makes it out isn't silently skipped for a full interval. */
|
|
106
|
+
due(now = Date.now()) {
|
|
107
|
+
if (now - this.lastAt < this.everyMs) return void 0;
|
|
108
|
+
return this.text;
|
|
109
|
+
}
|
|
110
|
+
/** Resets the clock. Call only once the reminder text from {@link due} was actually emitted. */
|
|
111
|
+
markDelivered(now = Date.now()) {
|
|
112
|
+
this.lastAt = now;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
/**
|
|
116
|
+
* Wraps `source` in a stream that emits `text` as a single leading chunk (with a trailing space, so
|
|
117
|
+
* TTS pauses before the reply) before piping the rest of `source` through unchanged. Cancelling the
|
|
118
|
+
* wrapper cancels `source` — so barge-in still aborts the underlying generation.
|
|
119
|
+
*/
|
|
120
|
+
function prependText(source, text) {
|
|
121
|
+
const prefix = text.endsWith(" ") ? text : `${text} `;
|
|
122
|
+
const reader = source.getReader();
|
|
123
|
+
return new ReadableStream({
|
|
124
|
+
start(controller) {
|
|
125
|
+
controller.enqueue(prefix);
|
|
126
|
+
},
|
|
127
|
+
async pull(controller) {
|
|
128
|
+
try {
|
|
129
|
+
const { done, value } = await reader.read();
|
|
130
|
+
if (done) controller.close();
|
|
131
|
+
else controller.enqueue(value);
|
|
132
|
+
} catch (error) {
|
|
133
|
+
controller.error(error);
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
cancel(reason) {
|
|
137
|
+
return reader.cancel(reason);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Maps a Mastra `finish` chunk's usage (`payload.output.usage`, AI-SDK `LanguageModelUsage`) to the
|
|
143
|
+
* LiveKit-shaped {@link VoiceTurnUsage}, or `undefined` when the chunk carries no token counts.
|
|
144
|
+
* Handles both the flat V2 usage shape (`inputTokens`/`outputTokens`) and the nested V3 shape
|
|
145
|
+
* (`inputTokens.total`/`outputTokens.total`).
|
|
146
|
+
*/
|
|
147
|
+
function mapTurnUsage(usage) {
|
|
148
|
+
if (!usage || typeof usage !== "object") return void 0;
|
|
149
|
+
const u = usage;
|
|
150
|
+
const totalOf = (v) => {
|
|
151
|
+
if (typeof v === "number") return v;
|
|
152
|
+
if (v && typeof v === "object" && typeof v.total === "number") return v.total;
|
|
153
|
+
};
|
|
154
|
+
const cacheReadOf = (v) => v && typeof v === "object" && typeof v.cacheRead === "number" ? v.cacheRead : void 0;
|
|
155
|
+
const promptTokens = totalOf(u.inputTokens) ?? 0;
|
|
156
|
+
const completionTokens = totalOf(u.outputTokens) ?? 0;
|
|
157
|
+
const promptCachedTokens = (typeof u.cachedInputTokens === "number" ? u.cachedInputTokens : cacheReadOf(u.inputTokens)) ?? 0;
|
|
158
|
+
const totalTokens = typeof u.totalTokens === "number" ? u.totalTokens : promptTokens + completionTokens;
|
|
159
|
+
if (promptTokens === 0 && completionTokens === 0 && totalTokens === 0 && promptCachedTokens === 0) return;
|
|
160
|
+
return {
|
|
161
|
+
promptTokens,
|
|
162
|
+
completionTokens,
|
|
163
|
+
promptCachedTokens,
|
|
164
|
+
totalTokens
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* A {@link VoiceReplyGenerator} backed by a Mastra agent: runs the agent's full loop (model,
|
|
169
|
+
* tools, memory) and streams its text deltas. On barge-in the returned stream is cancelled,
|
|
170
|
+
* which aborts the in-flight `agent.stream()`.
|
|
171
|
+
*/
|
|
172
|
+
function createAgentReplyGenerator(options) {
|
|
173
|
+
const { agent, streamOptions, toolFeedback, onToolCall, onTurnComplete } = options;
|
|
174
|
+
return (ctx) => {
|
|
175
|
+
if (ctx.messages.length === 0) return null;
|
|
176
|
+
const abortController = new AbortController();
|
|
177
|
+
const mergedOptions = {
|
|
178
|
+
...streamOptions,
|
|
179
|
+
abortSignal: abortController.signal
|
|
180
|
+
};
|
|
181
|
+
if (ctx.memory) mergedOptions.memory = ctx.memory;
|
|
182
|
+
if (ctx.requestContext) mergedOptions.requestContext = ctx.requestContext;
|
|
183
|
+
let cancelled = false;
|
|
184
|
+
let replyText = "";
|
|
185
|
+
const toolCalls = [];
|
|
186
|
+
let usage;
|
|
187
|
+
const emitTurnComplete = (interrupted) => {
|
|
188
|
+
if (!onTurnComplete) return;
|
|
189
|
+
const completeCtx = {
|
|
190
|
+
...ctx,
|
|
191
|
+
result: {
|
|
192
|
+
text: replyText,
|
|
193
|
+
toolCalls,
|
|
194
|
+
interrupted,
|
|
195
|
+
usage
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
Promise.resolve().then(() => onTurnComplete(completeCtx)).catch((error) => {
|
|
199
|
+
console.warn("@mastra/livekit: onTurnComplete hook threw", error);
|
|
200
|
+
});
|
|
201
|
+
};
|
|
202
|
+
return new ReadableStream({
|
|
203
|
+
start: async (controller) => {
|
|
204
|
+
try {
|
|
205
|
+
const result = await agent.stream(ctx.messages, mergedOptions);
|
|
206
|
+
for await (const chunk of result.fullStream) {
|
|
207
|
+
if (cancelled) break;
|
|
208
|
+
if (chunk.type === "text-delta") {
|
|
209
|
+
if (chunk.payload.text) {
|
|
210
|
+
replyText += chunk.payload.text;
|
|
211
|
+
controller.enqueue(chunk.payload.text);
|
|
212
|
+
}
|
|
213
|
+
} else if (chunk.type === "tool-call") {
|
|
214
|
+
const toolCall = {
|
|
215
|
+
toolCallId: chunk.payload.toolCallId,
|
|
216
|
+
toolName: chunk.payload.toolName,
|
|
217
|
+
args: chunk.payload.args
|
|
218
|
+
};
|
|
219
|
+
toolCalls.push(toolCall);
|
|
220
|
+
try {
|
|
221
|
+
onToolCall?.(toolCall);
|
|
222
|
+
} catch (error) {
|
|
223
|
+
console.warn("@mastra/livekit: onToolCall hook threw", error);
|
|
224
|
+
}
|
|
225
|
+
if (toolFeedback) {
|
|
226
|
+
let filler;
|
|
227
|
+
try {
|
|
228
|
+
filler = toolFeedback(toolCall);
|
|
229
|
+
} catch (error) {
|
|
230
|
+
console.warn("@mastra/livekit: toolFeedback hook threw", error);
|
|
231
|
+
}
|
|
232
|
+
if (filler) controller.enqueue(filler.endsWith(" ") ? filler : `${filler} `);
|
|
233
|
+
}
|
|
234
|
+
} else if (chunk.type === "finish") {
|
|
235
|
+
const output = chunk.payload.output;
|
|
236
|
+
const turnUsage = mapTurnUsage(output?.usage);
|
|
237
|
+
if (turnUsage) {
|
|
238
|
+
usage = turnUsage;
|
|
239
|
+
try {
|
|
240
|
+
ctx.onUsage?.(turnUsage);
|
|
241
|
+
} catch (error) {
|
|
242
|
+
console.warn("@mastra/livekit: onUsage hook threw", error);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
} else if (chunk.type === "error") {
|
|
246
|
+
const error = chunk.payload.error;
|
|
247
|
+
throw error instanceof Error ? error : new Error(String(error));
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (!cancelled) controller.close();
|
|
251
|
+
emitTurnComplete(cancelled);
|
|
252
|
+
} catch (error) {
|
|
253
|
+
if (cancelled || abortController.signal.aborted) {
|
|
254
|
+
emitTurnComplete(true);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
controller.error(error);
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
cancel: () => {
|
|
261
|
+
cancelled = true;
|
|
262
|
+
abortController.abort();
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
function toRequestContext(value) {
|
|
268
|
+
if (!value) return void 0;
|
|
269
|
+
if (value instanceof RequestContext) return value;
|
|
270
|
+
return new RequestContext(Object.entries(value));
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* The session only runs its cascaded reply pipeline when an `llm` instance is present —
|
|
274
|
+
* `llmNode` replaces the inference step, but the gate checks `llm instanceof LLM`. This
|
|
275
|
+
* placeholder satisfies the gate; the Mastra agent/workflow does the actual generation.
|
|
276
|
+
*/
|
|
277
|
+
var MastraPlaceholderLLM = class extends llm.LLM {
|
|
278
|
+
label() {
|
|
279
|
+
return "mastra.MastraVoiceAgent";
|
|
280
|
+
}
|
|
281
|
+
get model() {
|
|
282
|
+
return "mastra-agent";
|
|
283
|
+
}
|
|
284
|
+
get provider() {
|
|
285
|
+
return "mastra";
|
|
286
|
+
}
|
|
287
|
+
chat() {
|
|
288
|
+
throw new Error("@mastra/livekit: reply generation runs through the Mastra agent via llmNode; the placeholder LLM cannot be used for inference.");
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
/**
|
|
292
|
+
* A LiveKit `voice.Agent` whose replies come from a Mastra agent or workflow.
|
|
293
|
+
*
|
|
294
|
+
* LiveKit keeps ownership of the audio loop (VAD, STT, turn detection, TTS, barge-in) and calls
|
|
295
|
+
* `llmNode` once per detected user turn; the node delegates to a {@link VoiceReplyGenerator}
|
|
296
|
+
* which streams text deltas back. On barge-in LiveKit cancels the returned stream, which aborts
|
|
297
|
+
* the in-flight generation.
|
|
298
|
+
*/
|
|
299
|
+
var MastraVoiceAgent = class extends voice.Agent {
|
|
300
|
+
mastraAgent;
|
|
301
|
+
memory;
|
|
302
|
+
requestContext;
|
|
303
|
+
streamOptions;
|
|
304
|
+
replyGenerator;
|
|
305
|
+
reminder;
|
|
306
|
+
constructor(options) {
|
|
307
|
+
if (options.agent && options.generate) throw new Error("@mastra/livekit: MastraVoiceAgent requires `agent` or `generate`, not both — they are mutually exclusive reply sources.");
|
|
308
|
+
super({
|
|
309
|
+
id: options.id,
|
|
310
|
+
instructions: options.instructions ?? DEFAULT_INSTRUCTIONS,
|
|
311
|
+
stt: options.stt,
|
|
312
|
+
vad: options.vad,
|
|
313
|
+
llm: new MastraPlaceholderLLM(),
|
|
314
|
+
tts: options.tts,
|
|
315
|
+
turnHandling: options.turnHandling
|
|
316
|
+
});
|
|
317
|
+
this.memory = options.memory ?? false;
|
|
318
|
+
this.requestContext = toRequestContext(options.requestContext);
|
|
319
|
+
this.streamOptions = options.streamOptions;
|
|
320
|
+
if (options.greetingReminder) this.reminder = new DisclosureReminder(options.greetingReminder.everyMs, options.greetingReminder.text?.trim() || "Just a reminder, you're speaking with an AI assistant.");
|
|
321
|
+
if (options.generate) this.replyGenerator = options.generate;
|
|
322
|
+
else if (options.agent) {
|
|
323
|
+
this.mastraAgent = options.agent;
|
|
324
|
+
this.replyGenerator = createAgentReplyGenerator({
|
|
325
|
+
agent: options.agent,
|
|
326
|
+
streamOptions: options.streamOptions,
|
|
327
|
+
toolFeedback: options.toolFeedback,
|
|
328
|
+
onToolCall: options.onToolCall,
|
|
329
|
+
onTurnComplete: options.onTurnComplete
|
|
330
|
+
});
|
|
331
|
+
} else throw new Error("@mastra/livekit: MastraVoiceAgent requires `agent` or `generate`.");
|
|
332
|
+
}
|
|
333
|
+
async llmNode(chatCtx, _toolCtx, _modelSettings) {
|
|
334
|
+
const messages = this.memory === false ? chatContextToMessages(chatCtx) : extractNewTurnMessages(chatCtx);
|
|
335
|
+
if (messages.length === 0) return null;
|
|
336
|
+
const reply = await this.replyGenerator({
|
|
337
|
+
messages,
|
|
338
|
+
chatCtx,
|
|
339
|
+
memory: this.memory,
|
|
340
|
+
requestContext: this.requestContext,
|
|
341
|
+
tracingContext: this.streamOptions?.tracingContext
|
|
342
|
+
});
|
|
343
|
+
if (!reply) return null;
|
|
344
|
+
const reminder = this.reminder?.due();
|
|
345
|
+
if (!reminder) return reply;
|
|
346
|
+
this.reminder?.markDelivered();
|
|
347
|
+
return prependText(reply, reminder);
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
function createMastraVoiceAgent(options) {
|
|
351
|
+
return new MastraVoiceAgent(options);
|
|
352
|
+
}
|
|
353
|
+
//#endregion
|
|
354
|
+
//#region src/remote.ts
|
|
355
|
+
const DEFAULT_API_PREFIX = "/api";
|
|
356
|
+
/** Connect + first-token budget when not overridden. Plugin mode passes `connOptions.timeoutMs`. */
|
|
357
|
+
const DEFAULT_REMOTE_TIMEOUT_MS = 1e4;
|
|
358
|
+
/** Thrown (as a plain, non-retryable error) when the server emits a chunk that needs client action. */
|
|
359
|
+
const 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.";
|
|
360
|
+
function trimTrailingSlash(url) {
|
|
361
|
+
return url.endsWith("/") ? url.slice(0, -1) : url;
|
|
362
|
+
}
|
|
363
|
+
function toMessage(error) {
|
|
364
|
+
return error instanceof Error ? error.message : String(error);
|
|
365
|
+
}
|
|
366
|
+
async function resolveHeaders(headers) {
|
|
367
|
+
if (!headers) return {};
|
|
368
|
+
if (typeof headers === "function") return await headers() ?? {};
|
|
369
|
+
return headers;
|
|
370
|
+
}
|
|
371
|
+
function serializeRequestContext(requestContext) {
|
|
372
|
+
if (!requestContext) return void 0;
|
|
373
|
+
if (requestContext instanceof RequestContext) return Object.fromEntries(requestContext.entries());
|
|
374
|
+
return requestContext;
|
|
375
|
+
}
|
|
376
|
+
async function safeReadBody(response) {
|
|
377
|
+
try {
|
|
378
|
+
const text = await response.text();
|
|
379
|
+
if (!text) return null;
|
|
380
|
+
try {
|
|
381
|
+
const parsed = JSON.parse(text);
|
|
382
|
+
return parsed && typeof parsed === "object" ? parsed : { message: String(parsed) };
|
|
383
|
+
} catch {
|
|
384
|
+
return { message: text };
|
|
385
|
+
}
|
|
386
|
+
} catch {
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Reads a Mastra SSE stream and yields each event's parsed JSON. Framing matches the server's
|
|
392
|
+
* `processMastraStream` (buffer, split on `\n\n`, strip `data: `, stop on `[DONE]`); undecodable
|
|
393
|
+
* `data:` lines are skipped. Aborting `signal` cancels the underlying reader.
|
|
394
|
+
*/
|
|
395
|
+
async function* readMastraSSE(body, signal) {
|
|
396
|
+
const reader = body.getReader();
|
|
397
|
+
const decoder = new TextDecoder();
|
|
398
|
+
let buffer = "";
|
|
399
|
+
const onAbort = () => void reader.cancel().catch(() => {});
|
|
400
|
+
if (signal.aborted) {
|
|
401
|
+
reader.cancel().catch(() => {});
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
405
|
+
try {
|
|
406
|
+
for (;;) {
|
|
407
|
+
const { done, value } = await reader.read();
|
|
408
|
+
if (done) break;
|
|
409
|
+
buffer += decoder.decode(value, { stream: true });
|
|
410
|
+
const events = buffer.split("\n\n");
|
|
411
|
+
buffer = events.pop() ?? "";
|
|
412
|
+
for (const event of events) {
|
|
413
|
+
if (!event.startsWith("data:")) continue;
|
|
414
|
+
const data = event.slice(event.startsWith("data: ") ? 6 : 5).trim();
|
|
415
|
+
if (data === "[DONE]") return;
|
|
416
|
+
if (!data) continue;
|
|
417
|
+
let json;
|
|
418
|
+
try {
|
|
419
|
+
json = JSON.parse(data);
|
|
420
|
+
} catch {
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
if (json && typeof json === "object") yield json;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
} finally {
|
|
427
|
+
signal.removeEventListener("abort", onAbort);
|
|
428
|
+
try {
|
|
429
|
+
reader.releaseLock();
|
|
430
|
+
} catch {}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* A {@link VoiceReplyGenerator} that runs the Mastra agent loop on a **remote** Mastra server over
|
|
435
|
+
* HTTP/SSE. Shaped exactly like the in-process `createAgentReplyGenerator`: it consumes the same
|
|
436
|
+
* chunk vocabulary, drives the same `toolFeedback` / `onToolCall` / `onTurnComplete` seams, and
|
|
437
|
+
* cancelling the returned stream (LiveKit does this on barge-in) tears down the HTTP request so the
|
|
438
|
+
* server aborts generation.
|
|
439
|
+
*
|
|
440
|
+
* Usable standalone via the worker's `generate:` hatch (a minimum-viable remote worker mode), and as
|
|
441
|
+
* the transport `MastraLLM` wraps. Errors are thrown as LiveKit `APIError` subclasses so the plugin's
|
|
442
|
+
* base-class retry loop and `FallbackAdapter` behave; a connect + first-token watchdog prevents
|
|
443
|
+
* indefinite dead air.
|
|
444
|
+
*/
|
|
445
|
+
function createRemoteAgentReplyGenerator(options) {
|
|
446
|
+
const { baseUrl, agentId, apiPrefix = DEFAULT_API_PREFIX, headers, fetch: fetchImpl = globalThis.fetch, timeoutMs = DEFAULT_REMOTE_TIMEOUT_MS, retries = 2, body: extraBody, toolFeedback, onToolCall, onTurnComplete } = options;
|
|
447
|
+
if (!fetchImpl) throw new Error("@mastra/livekit: no fetch implementation available; pass `fetch` or run on Node ≥ 22.");
|
|
448
|
+
const url = `${trimTrailingSlash(baseUrl)}${apiPrefix}/agents/${agentId}/stream`;
|
|
449
|
+
return (ctx) => {
|
|
450
|
+
if (ctx.messages.length === 0) return null;
|
|
451
|
+
let currentAbortController;
|
|
452
|
+
let cancelled = false;
|
|
453
|
+
let replyText = "";
|
|
454
|
+
const toolCalls = [];
|
|
455
|
+
let usage;
|
|
456
|
+
const emitTurnComplete = (interrupted) => {
|
|
457
|
+
if (!onTurnComplete) return;
|
|
458
|
+
const completeCtx = {
|
|
459
|
+
...ctx,
|
|
460
|
+
result: {
|
|
461
|
+
text: replyText,
|
|
462
|
+
toolCalls,
|
|
463
|
+
interrupted,
|
|
464
|
+
usage
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
Promise.resolve().then(() => onTurnComplete(completeCtx)).catch((error) => {
|
|
468
|
+
console.warn("@mastra/livekit: onTurnComplete hook threw", error);
|
|
469
|
+
});
|
|
470
|
+
};
|
|
471
|
+
const requestBody = {
|
|
472
|
+
messages: ctx.messages,
|
|
473
|
+
memory: ctx.memory ? {
|
|
474
|
+
thread: ctx.memory.thread,
|
|
475
|
+
resource: ctx.memory.resource ?? ctx.memory.thread
|
|
476
|
+
} : void 0,
|
|
477
|
+
requestContext: serializeRequestContext(ctx.requestContext),
|
|
478
|
+
...extraBody
|
|
479
|
+
};
|
|
480
|
+
return new ReadableStream({
|
|
481
|
+
start: async (controller) => {
|
|
482
|
+
let retryable = true;
|
|
483
|
+
try {
|
|
484
|
+
for (let attempt = 0;; attempt++) {
|
|
485
|
+
const abortController = new AbortController();
|
|
486
|
+
currentAbortController = abortController;
|
|
487
|
+
let timedOut = false;
|
|
488
|
+
let watchdog;
|
|
489
|
+
const clearWatchdog = () => {
|
|
490
|
+
if (watchdog) {
|
|
491
|
+
clearTimeout(watchdog);
|
|
492
|
+
watchdog = void 0;
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
try {
|
|
496
|
+
watchdog = setTimeout(() => {
|
|
497
|
+
timedOut = true;
|
|
498
|
+
abortController.abort();
|
|
499
|
+
}, timeoutMs);
|
|
500
|
+
watchdog.unref?.();
|
|
501
|
+
const resolvedHeaders = await resolveHeaders(headers);
|
|
502
|
+
if (cancelled) {
|
|
503
|
+
clearWatchdog();
|
|
504
|
+
break;
|
|
505
|
+
}
|
|
506
|
+
const response = await fetchImpl(url, {
|
|
507
|
+
method: "POST",
|
|
508
|
+
headers: {
|
|
509
|
+
"content-type": "application/json",
|
|
510
|
+
accept: "text/event-stream",
|
|
511
|
+
...resolvedHeaders
|
|
512
|
+
},
|
|
513
|
+
body: JSON.stringify(requestBody),
|
|
514
|
+
signal: abortController.signal
|
|
515
|
+
});
|
|
516
|
+
if (!response.ok) {
|
|
517
|
+
const errorBody = await safeReadBody(response);
|
|
518
|
+
throw new APIStatusError({
|
|
519
|
+
message: `@mastra/livekit: Mastra agent stream request failed with status ${response.status}`,
|
|
520
|
+
options: {
|
|
521
|
+
statusCode: response.status,
|
|
522
|
+
body: errorBody,
|
|
523
|
+
retryable
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
if (!response.body) throw new APIConnectionError({
|
|
528
|
+
message: "@mastra/livekit: Mastra agent stream returned an empty response body",
|
|
529
|
+
options: { retryable }
|
|
530
|
+
});
|
|
531
|
+
for await (const chunk of readMastraSSE(response.body, abortController.signal)) {
|
|
532
|
+
if (cancelled) break;
|
|
533
|
+
retryable = false;
|
|
534
|
+
const payload = chunk.payload ?? {};
|
|
535
|
+
switch (chunk.type) {
|
|
536
|
+
case "text-delta": {
|
|
537
|
+
const text = payload.text;
|
|
538
|
+
if (typeof text === "string" && text) {
|
|
539
|
+
clearWatchdog();
|
|
540
|
+
replyText += text;
|
|
541
|
+
controller.enqueue(text);
|
|
542
|
+
}
|
|
543
|
+
break;
|
|
544
|
+
}
|
|
545
|
+
case "tool-call": {
|
|
546
|
+
clearWatchdog();
|
|
547
|
+
const toolCall = {
|
|
548
|
+
toolCallId: String(payload.toolCallId ?? ""),
|
|
549
|
+
toolName: String(payload.toolName ?? ""),
|
|
550
|
+
args: payload.args
|
|
551
|
+
};
|
|
552
|
+
toolCalls.push(toolCall);
|
|
553
|
+
try {
|
|
554
|
+
onToolCall?.(toolCall);
|
|
555
|
+
} catch (error) {
|
|
556
|
+
console.warn("@mastra/livekit: onToolCall hook threw", error);
|
|
557
|
+
}
|
|
558
|
+
if (toolFeedback) {
|
|
559
|
+
let filler;
|
|
560
|
+
try {
|
|
561
|
+
filler = toolFeedback(toolCall);
|
|
562
|
+
} catch (error) {
|
|
563
|
+
console.warn("@mastra/livekit: toolFeedback hook threw", error);
|
|
564
|
+
}
|
|
565
|
+
if (filler) controller.enqueue(filler.endsWith(" ") ? filler : `${filler} `);
|
|
566
|
+
}
|
|
567
|
+
break;
|
|
568
|
+
}
|
|
569
|
+
case "finish": {
|
|
570
|
+
clearWatchdog();
|
|
571
|
+
const output = payload.output;
|
|
572
|
+
const turnUsage = mapTurnUsage(output?.usage);
|
|
573
|
+
if (turnUsage) {
|
|
574
|
+
usage = turnUsage;
|
|
575
|
+
try {
|
|
576
|
+
ctx.onUsage?.(turnUsage);
|
|
577
|
+
} catch (error) {
|
|
578
|
+
console.warn("@mastra/livekit: onUsage hook threw", error);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
break;
|
|
582
|
+
}
|
|
583
|
+
case "tool-call-approval":
|
|
584
|
+
case "tool-call-suspended": throw new Error(HITL_UNSUPPORTED_MESSAGE);
|
|
585
|
+
case "error": {
|
|
586
|
+
const error = payload.error;
|
|
587
|
+
throw error instanceof Error ? error : new Error(String(error));
|
|
588
|
+
}
|
|
589
|
+
default: break;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
clearWatchdog();
|
|
593
|
+
break;
|
|
594
|
+
} catch (error) {
|
|
595
|
+
clearWatchdog();
|
|
596
|
+
if (cancelled) break;
|
|
597
|
+
let typed;
|
|
598
|
+
if (timedOut) typed = new APITimeoutError({ options: { retryable } });
|
|
599
|
+
else if (error instanceof APIError) typed = error;
|
|
600
|
+
else if (retryable) typed = new APIConnectionError({
|
|
601
|
+
message: toMessage(error),
|
|
602
|
+
options: { retryable }
|
|
603
|
+
});
|
|
604
|
+
else throw error;
|
|
605
|
+
if (typed.retryable && attempt < retries) continue;
|
|
606
|
+
throw typed;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
if (!cancelled) controller.close();
|
|
610
|
+
emitTurnComplete(cancelled);
|
|
611
|
+
} catch (error) {
|
|
612
|
+
if (cancelled) {
|
|
613
|
+
emitTurnComplete(true);
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
controller.error(error);
|
|
617
|
+
}
|
|
618
|
+
},
|
|
619
|
+
cancel: () => {
|
|
620
|
+
cancelled = true;
|
|
621
|
+
currentAbortController?.abort();
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
//#endregion
|
|
627
|
+
export { chatContextToMessages as a, createMastraVoiceAgent as i, MastraVoiceAgent as n, extractNewTurnMessages as o, createAgentReplyGenerator as r, createRemoteAgentReplyGenerator as t };
|
|
628
|
+
|
|
629
|
+
//# sourceMappingURL=remote-D7n50m8S.js.map
|